ALB Ingress Controller で SSL Redirect を設定する方法
はじめに
ALB Ingress Controllerで作成されたALBでどのようにSSL Redirectを設定するのかを調べたのでメモ。 github.com
設定方法
以下の公式ドキュメントがそのままずばりだった。 kubernetes-sigs.github.io
ドキュメントを参照すれば十分ではあるのだけど、感想や注意点をいくつかメモしておく。
annotations について
ALBでHTTPSのリスナーを有効にする為には alb.ingress.kubernetes.io/certificate-arn
が必要となる。また、HTTPからHTTPSへのリダイレクトとなるので alb.ingress.kubernetes.io/listen-ports
はHTTPとHTTPSどちらもリッスンする必要がある。
リダイレクトを行うためのALBのアクションとして、以下のような定義も必要となる。(ssl-redirectはアクション名で任意の名付けが可能)
alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig": { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}'
spec.rules.http.paths の書き方について
ALBとしてSSL Redirectを実現するためにはHTTPとHTTPSでそれぞれ異なるリスナールールを追加する必要がある。しかし以下はALB Ingress Controllerのドキュメントから引用したSSL Redirectの定義例なのだが、httpとhttpsのように個別に定義している訳では無かった。
apiVersion: extensions/v1beta1 kind: Ingress metadata: namespace: default name: ingress annotations: kubernetes.io/ingress.class: alb alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-west-2:xxxx:certificate/xxxxxx alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]' alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig": { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}' spec: rules: - http: paths: - path: /* backend: serviceName: ssl-redirect servicePort: use-annotation - path: /users/* backend: serviceName: user-service servicePort: 80 - path: /* backend: serviceName: default-service servicePort: 80
ALB Ingress Controllerでは以下のロジックで処理することにより、ALBのHTTPとHTTPSのリスナールールを定義しているようだ。
- デフォルトではspecで指定された全てのリスナールールをALBの全てのリスナーに適用する
- リダイレクトルールが存在する場合、全てのリスナーでリダイレクトループになるかを判定し、リダイレクトループになる場合はルールを無視する
結構トリッキーだなというのが感想だが、これでALB Ingress ControllerでSSL Redirectを行う方法が分かった。
RDSの証明書更新が無停止で出来ると聞いたので試した
はじめに
RDSのTLS証明書の更新が無停止で出来ると聞いたので、調べた結果のメモです。
ソースはこちらのドキュメントです。
aws.amazon.com
--no-certificate-rotation-restart
オプションについて以下のように言及がなされています。
I don’t use SSL/TLS, can I rotate the certificate without restarting my database?
If you do not want to restart your database, you can use a new CLI option for the modify-db-instance CLI command (--no-certificate-rotation-restart) specifically to rotate and stage the new certificates on the database host to avoid a restart. However, new certificates will be picked up by the database only when a planned or unplanned database restart happens.
結果
結論からいうと、TLS証明書の更新が不要な場合に限り無停止でメンテナンスを行う事が可能でした。
以下、検証結果を記します。
検証結果
まずは更新前のRDSの証明書を確認します。確認したところ、有効期限が2020/3/5の状態でした。
openssl s_client -connect ${RDS_ENDPOINT}:${RDS_PORT} -starttls mysql | openssl x509 -noout -enddate depth=1 C = US, ST = Washington, L = Seattle, O = "Amazon Web Services, Inc.", OU = Amazon RDS, CN = Amazon RDS ap-northeast-1 CA depth=0 CN = xxxxx.ap-northeast-1.rds.amazonaws.com, OU = RDS, O = Amazon.com, L = Seattle, ST = Washington, C = US verify return:1 notAfter=Mar 5 22:03:06 2020 GMT
次に、このRDSに対して以下のようにヘルスチェックを行うスクリプトを作成しました。
#!/bin/bash while true; do d=$(date) if mysql -e 'show databases' > /dev/null 2>&1 ; then echo "${d}: connect success" else echo "${d}: connect failed" fi sleep 1 done
上記のスクリプトを実行した状態で、以下のように変更を実施しました。
aws rds modify-db-instance --db-instance-identifier ${RDS_IDENTIFIER} --no-certificate-rotation-restart --ca-certificate-identifier rds-ca-2019 # レスポンスから抜粋 "PendingModifiedValues": { "CACertificateIdentifier": "rds-ca-2019" },
PendingModifiedValuesがCACertificateIdentifierだけでしたので、即時反映をしてみます。
aws rds modify-db-instance --db-instance-identifier ${RDS_IDENTIFIER} --no-certificate-rotation-restart --ca-certificate-identifier rds-ca-2019 --apply-immediately
無事に完了し、このRDSインスタンスが aws rds describe-pending-maintenance-actions
の対象から除外されたことも確認できました。もちろんこの間にmysqlのヘルスチェックは失敗しませんでした。
最後に証明書の状態を確認してみたところ、ドキュメントの通り証明書は更新されていませんでした。
openssl s_client -connect ${RDS_ENDPOINT}:${RDS_PORT} -starttls mysql | openssl x509 -noout -enddate depth=1 C = US, ST = Washington, L = Seattle, O = "Amazon Web Services, Inc.", OU = Amazon RDS, CN = Amazon RDS ap-northeast-1 CA depth=0 CN = xxxxx.ap-northeast-1.rds.amazonaws.com, OU = RDS, O = Amazon.com, L = Seattle, ST = Washington, C = US verify return:1 notAfter=Mar 5 22:03:06 2020 GMT
証明書を更新する方法
CLIのリファレンスを確認したところ --no-certificate-rotation-restart
を指定した場合でも、RDSインスタンスを再起動すれば証明書が更新されるようなので試してみました。
docs.aws.amazon.com
さきほどの検証に用いたRDSインスタンスを再起動します。
aws rds reboot-db-instance --db-instance-identifier ${RDS_IDENTIFIER}
再起動後、TLS証明書の状態を確認した結果がこちらです。無事にTLS証明書が更新されていますね。
openssl s_client -connect ${RDS_ENDPOINT}:${RDS_PORT} -starttls mysql | openssl x509 -noout -enddate depth=1 C = US, ST = Washington, L = Seattle, O = "Amazon Web Services, Inc.", OU = Amazon RDS, CN = Amazon RDS ap-northeast-1 2019 CA depth=0 CN = xxxxx.ap-northeast-1.rds.amazonaws.com, OU = RDS, O = Amazon.com, L = Seattle, ST = Washington, C = US verify return:1 notAfter=Aug 22 17:08:50 2024 GMT
terraform planで意図せず差分が検出される件と向き合ってみた
はじめに
良く terraform
を利用するのですが、定義変更がない状態でplan
を実行した際差分が検出される(ことがある)事象に悩まされていました。遭遇頻度がそこまで高くなかったために、しばらく放置していたのですがterraformもCI/CDをまわしたい欲に駆られてしっかりと向き合ってみました。
どんな事象が起きるのか
たとえば、Datadogと連携するためのiAM Roleを以下のように定義してみます。
resource "aws_iam_role" "dd" { name = "dd" description = "dd" assume_role_policy = <<EOF { "Version": "2012-10-17", "Statement": [ { "Sid": "", "Effect": "Allow", "Principal": { "AWS": "123451234512" }, "Action": "sts:AssumeRole" } ] } EOF }
おもむろにapply
します。
$ terraform apply <snip> Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
無事にIAM Roleが作成されました。それでは、定義を変更せずにplan
を実行してみます。
$ terraform plan <snip> ~ aws_iam_role.dd assume_role_policy: "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam::123451234512:root\"},\"Action\":\"sts:AssumeRole\"}]}" => "{\n \"Version\": \"2012-10-17\",\n \"Statement\": [\n {\n \"Sid\": \"\",\n \"Effect\": \"Allow\",\n \"Principal\": {\n \"AWS\": \"123451234512\"\n },\n \"Action\": \"sts:AssumeRole\"\n }\n ]\n}\n"
定義の変更が無いにも関わらず、差分が検出されてしまいます。
向き合い方
それではこの差分と向き合ってみましょう。今回はterraform-landscapeを利用してplan
の可読性を高めています。
使い方は簡単でplan
の結果をパイプでlandscape
に渡してあげるだけです。それでは実行してみます。
$ terraform plan | landscape <snip> ~ aws_iam_role.dd assume_role_policy: "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { - "AWS": "arn:aws:iam::123451234512:root" + "AWS": "123451234512" }, "Sid": "" } ], "Version": "2012-10-17"
Principalの値に差分が見られますね。123451234512
として定義しましたが、tfstateファイルではarn:aws:iam::123451234512:root
と記録されているようです。
一方plan
で表示された設定は表記方法が異なるだけで、設定としては問題がなさそうです。そのためこのままapply
しても問題はなさそう。ですがこれでは都度差分を確認することとなり、精神的にも消耗しますし、最悪plan
をしっかりと確認しない文化が作られる可能性があります。
このようなケースで差分として検出されないために、定義ファイルを少しだけ修正してみます。
resource "aws_iam_role" "dd" { name = "dd" description = "dd" assume_role_policy = <<EOF { "Version": "2012-10-17", "Statement": [ { "Sid": "", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::123451234512:root" }, "Action": "sts:AssumeRole" } ] } EOF } `
Principalの定義をtfstateファイルに合わせ修正しています。この状態でplan
を実行して差分が検出されないことを確認してみます。
$ terraform plan Refreshing Terraform state in-memory prior to plan... The refreshed state will be used to calculate this plan, but will not be persisted to local or remote state storage. aws_iam_role.dd: Refreshing state... (ID: dd) ------------------------------------------------------------------------ No changes. Infrastructure is up-to-date. This means that Terraform did not detect any differences between your configuration and real physical resources that exist. As a result, no actions need to be performed.
無事に差分が検出されなくなりました。
terreformでは定義ファイルの通りにtfstateファイルへと記録されないこともあるようですね。そのため、定義ファイルで変更が無い場合でもplan
の際に差分として検出されることがあります。このような差分はplan
のノイズになるので排除したいところです。
この辺りはplan
にヒントが多く表示されていると思われますので、やはりplan
は大事です。また、plan
の差分としてJSONが表示されるとかなり視認性が低くツラかったのですが、terraform-landscapeの力を借りることでかなり視認性が高くなりました。
この調子で意図しない差分はどんどん排除したいと思います。
Golang dep ensureがハングした時にやったこと
はじめに
たまにGolangを使います。最近は依存関係のマネージメントツールは Glide
ではなく dep
を使っているのですが dep ensure
が延々と終わらない状況となり、一応の解決を迎えたので備忘録です。
起こったこと
以下のようなコードを書き dep ensure
をしたところ、10分以上処理が終わらない状況になりました。
package main import ( "fmt" _ "github.com/aws/aws-sdk-go" ) func main() { fmt.Println("hello world") }
depのバージョンはこちらです。
$ dep version dep: version : v0.4.1-161-g8b166f24 build date : 2018-05-02 git hash : 8b166f24 go version : go1.10 go compiler : gc platform : darwin/amd64 features : ImportDuringSolve=false
対応
まずは詳細情報を得るため dep ensure -v
を実行してみたところ、以下の表示でハングしていました。
$ dep ensure -v Root project is "xxxx/xxxx" 1 transitively valid internal packages 1 external packages imported from 1 projects (0) ✓ select (root)
この状態でどのようなプロセスが起動されているのか調査した結果がこちらです。
$ pstree | \-+= 94723 ginyon dep ensure -v | \-+= 94805 ginyon git ls-remote ssh://git@github.com/aws/aws-sdk-go | \--- 94806 ginyon /usr/bin/ssh git@github.com git-upload-pack '/aws/aws-sdk-go'
試しに git ls-remote ssh://git@github.com/aws/aws-sdk-go
を叩いたところホストキーの受け入れを確認されました。
$ git ls-remote ssh://git@github.com/aws/aws-sdk-go The authenticity of host 'github.com (192.30.255.112)' can't be established. RSA key fingerprint is SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8. Are you sure you want to continue connecting (yes/no)?
yes
としてホストキー受け入れ後、再度 dep ensure -v
を実行した結果がこちらです。
$ dep ensure -v Root project is "xxxx/xxxx" 1 transitively valid internal packages 1 external packages imported from 1 projects (0) ✓ select (root) (1) ? attempt github.com/aws/aws-sdk-go with 1 pkgs; 410 versions to try (1) try github.com/aws/aws-sdk-go@v1.13.54 (1) ✓ select github.com/aws/aws-sdk-go@v1.13.54 w/1 pkgs ✓ found solution with 1 packages from 1 projects Solver wall times by segment: b-source-exists: 5.001233802s b-list-pkgs: 3.256008604s b-gmal: 3.198661178s b-list-versions: 2.016181548s satisfy: 13.335691ms select-atom: 11.367302ms new-atom: 82.554µs select-root: 61.103µs other: 18.659µs b-deduce-proj-root: 2.296µs TOTAL: 13.496952737s (1/1) Wrote github.com/aws/aws-sdk-go@v1.13.54
無事に導入できました。というメモです。
ElasticBeantalkで作成されるリソース間の参照方法
はじめに
今年はブログたくさん書きたいなぁと思っていたらもう4月でした。
これで今年2本目のブログです。やばい。
直近でAWSのElasticBeanstalkで使える ebextensions
について調べたのでメモしておきます。
ebextensionsのこと
そもそもみなさんはElasticBeanstalkを使ったことはありますか? docs.aws.amazon.com
たまにPaaSと表現されたりもしますが、僕はAWSリソースのプロビジョニングとアプリケーションのデプロイを統合して面倒を見てくれる、それはそれは素敵で業の深いサービスだと思っています。
ElasticBeanstalkの酸いも甘いも経験された方、一杯飲みに行きましょう。
さて、肝心の ebextensions
について。
これはアプリケーションのソースコード内に .ebextensions
というフォルダを作り、そこへ xxx.config
という感じで命名した設定ファイルを放り込むことで、環境設定、パッケージの導入、CloudFormation記法を使ってのリソース作成、などが出来る控えめに言って黒魔法な存在です。
個人的にはあまり黒魔法すぎない程度(パッケージの導入やロケールの設定)に使うと良いと思っているのですが、バランス良く使うのはとても難しいです。
そんな ebextensions
の、リソース作成にフォーカスした話題です。
本題
ElasticBeanstalkで作成されるリソース間の参照方法をまとめたいと思います。
まずElasticBeanstalkのリソースには、以下の2つのタイプが存在する事を意識しておくと良いです。
- ElasticBeanstalkによって作成される標準リソース
ebextensions
のResources
プロパティによって作成されるカスタムリソース
標準リソースはこちらのドキュメントを参考にしてください。 docs.aws.amazon.com
カスタムリソースはこちらのドキュメントを参考にしてください。 docs.aws.amazon.com
さて、もちろんAWSリソースを作成する際には依存関係のあるリソース定義も必要になります。
この時は、標準リソースであっても、カスタムリソースであってもリソース情報を参照する術が必要となります。
上述したドキュメントには両者の答えとなる素晴らしいサンプルが記述されています。
そちらを抜粋したのが下記となります。
Resources: hooktopic: Type: AWS::SNS::Topic Properties: Subscription: - Endpoint: "my-email@example.com" Protocol: email lifecyclehook: Type: AWS::AutoScaling::LifecycleHook Properties: AutoScalingGroupName: { "Ref" : "AWSEBAutoScalingGroup" } LifecycleTransition: autoscaling:EC2_INSTANCE_TERMINATING NotificationTargetARN: { "Ref" : "hooktopic" } RoleARN: { "Fn::GetAtt" : [ "hookrole", "Arn"] }
これはLifeCyclehookとSNS Topicの作成例です。
着眼していただきたいのは、カスタムリソースの lifecyclehook
作成部分になります。
ここには、標準リソースの参照方法と、カスタムリソースの参照方法のエッセンスが詰まっています。
標準リソースの参照方法
カスタムリソースの lifecyclehook
を作成する為に、ElasticBeanstalkが作成する標準リソースの AutoScalingName
が必要としています。
ここの部分です。
AutoScalingGroupName: { "Ref" : "AWSEBAutoScalingGroup" }
通常 AutoScalingName
はElasticBeanstalkにより作成されるため不定の値となるので、これを標準リソース AWSEBAutoScalingGroup
から Ref
関数で参照するようにしています。
つまり、標準リソースからは Ref
関数を用いることでリソース情報を参照できます。
カスタムリソースの参照方法
またまたカスタムリソースの lifecyclehook
を作成する部分を見ていくと、今度はカスタムリソースの hooktopic
を必要としている記述があります。
ここの部分です。
NotificationTargetARN: { "Ref" : "hooktopic" }
これは直前で定義されている以下 hooktopic
のARNを Ref
で参照しています。
hooktopic: Type: AWS::SNS::Topic Properties: Subscription: - Endpoint: "my-email@example.com" Protocol: email
また、データを参照する方法は Ref
以外にもあります。
以下のように Fn::GetAtt
を使って各リソースのアトリビュートも取得可能です。(この辺りはCloudFormationの世界ですね)
RoleARN: { "Fn::GetAtt" : [ "hookrole", "Arn"] }
つまり、カスタムリソースからは Ref
や Fn::GetAtt
でリソース情報が参照できます。
WAFを適用するスニペット
最後に、ググってもヒットしなかったのでElasticBeanstalkにWAFを適用できるスニペットを貼っておきます。
Resources: webACLAssociation: Type: AWS::WAFRegional::WebACLAssociation Properties: ResourceArn: { "Ref": "AWSEBV2LoadBalancer" } WebACLId: { "Ref": "wafACLId" }
AWSEBV2LoadBalancerは標準リソースからの参照です。
wafACLIdは別途パラメーターに定義するなどしてご利用ください。
S3の署名付きURLを短縮URLでアクセス可能にするプロキシサービスをサーバーレスに構築する
はじめに
S3の署名付きURLをご存知でしょうか?
署名を付けたURLにアクセスすることで、プライベートなS3オブジェクトに対してダウンロードやアップロードを許可することが可能なしくみです。
ただ、この署名部分って結構長いんです。
この署名付きURLをどのように短縮できるのか、というのが今回のテーマです。
アーキテクチャ
[s3 presignedurl shortener]でググってみると、AWSのcompute blogにいい感じの記事が見つかりました。
このアーキテクチャは素晴らしくて、S3のリダイレクト機能を上手く利用してなんとサーバーレスで短縮URLの仕組みを実現しています。
通常、短縮URLといえばリダイレクトで実現することが多いと思いますので、上記のブログも是非見ていただきたいです。
私は都合によりリダイレクトは利用できなかったため、この構成は見送りました。
リダイレクトをせずに署名付きURLを短縮したい。できればサーバーレスで。ということで今回のアーキテクチャはこちらです。
CloudFrontをS3の前段に配置、Lambda@EdgeでCloudFrontからS3へのリクエストをインターセプトしてリクエストを加工することで、CloudFront - S3間の通信は署名付きURLによる通信としています。ユーザーは短縮URLでCloudFrontにアクセスするだけです。
Lambda@Egdeを利用するアイデアはid:kikumotoさんから頂きました!ありがとうございます。
構築手順
CloudFormationのテンプレートを用意しました。
AWSコンソールにログインしていれば、こちらのボタンからStackの作成が可能です。
また、こちらのテンプレートを使うと、DynamoDBへ短縮URLを含んだItemを登録するLambdaが生成されるため、動作確認が簡単に行えます。
(このLambdaは構成図には含まれていません)
一応テンプレートをテキストでも貼っておきます。
AWSTemplateFormatVersion: "2010-09-09" Description: Serverless Presinged URL Proxy. Must be deploy to us-east-1. Outputs: S3BucketName: Description: "Amazon S3 bucket name." Value: !Ref S3Bucket CloudFrontURL: Description: CloudFront Distribution DomainName. Value: !Sub "https://${CloudFrontDistrib.DomainName}/" DynamoDBTableName: Description: DynamoDB TableName. Value: !Ref DynamoDBTable Resources: S3Bucket: Type: "AWS::S3::Bucket" DeletionPolicy: Delete DependsOn: LambdaInvokePermission Properties: BucketName: !Ref "AWS::NoValue" NotificationConfiguration: LambdaConfigurations: - Event: "s3:ObjectCreated:*" Function: !GetAtt LambdaGenerateS3PresingedURL.Arn DynamoDBTable: Type: "AWS::DynamoDB::Table" Properties: AttributeDefinitions: - AttributeName: ShortURL AttributeType: S KeySchema: - AttributeName: ShortURL KeyType: HASH ProvisionedThroughput: ReadCapacityUnits: "5" WriteCapacityUnits: "5" LambdaExecRole: Type: "AWS::IAM::Role" Properties: Policies: - PolicyName: LambdaExecRolePresignedURLProxy PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: "logs:*" Resource: "arn:aws:logs:*:*:*" - Effect: Allow Action: [ "s3:GetObject", "s3:PutObject", "s3:DeleteObject" ] Resource: !Sub "arn:aws:s3:::*" - Effect: Allow Action: [ "dynamodb:GetItem", "dynamodb:PutItem" ] Resource: !GetAtt DynamoDBTable.Arn AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Sid: "" Effect: Allow Principal: Service: [ "lambda.amazonaws.com", "edgelambda.amazonaws.com"] Action: "sts:AssumeRole" CloudFrontDistrib: Type: "AWS::CloudFront::Distribution" Properties: DistributionConfig: Origins: - DomainName: !GetAtt S3Bucket.DomainName Id: S3Origin S3OriginConfig: OriginAccessIdentity: "" Comment: CloudFront distribution used as a front end to the server-less Presinged URL Proxy. Enabled: true DefaultCacheBehavior: AllowedMethods: [ HEAD, DELETE, POST, GET, OPTIONS, PUT, PATCH ] CachedMethods: [ HEAD, GET ] Compress: true DefaultTTL: 60 TargetOriginId: "S3Origin" ViewerProtocolPolicy: "redirect-to-https" ForwardedValues: QueryString: 'false' LambdaFunctionAssociations: - EventType: origin-request LambdaFunctionARN: !Ref LambdaProxyDeployVersion LambdaProxy: Type: "AWS::Lambda::Function" Properties: Handler: index.handler MemorySize: 512 Role: !GetAtt LambdaExecRole.Arn Runtime: nodejs6.10 Timeout: 3 Code: ZipFile: !Sub - | 'use strict'; const aws = require('aws-sdk'); const docClient = new aws.DynamoDB.DocumentClient({region: '${AWS::Region}'}); exports.handler = (event, context, callback) => { const request = event.Records[0].cf.request; const httpVersion = request.httpVersion; const uri = request.uri; var params = { TableName: '${DynamoDBTableName}', Key: { 'ShortURL': uri.slice(1) } }; docClient.get(params, function(err, data) { const isEmpty = function(obj) { for(var key in obj) { if(obj.hasOwnProperty(key)) { return false; } } return true; }; if (err || isEmpty(data)) { const response = { status: '404', statusDescription: 'Not Found.', httpVersion: httpVersion, body: "404 Not Found.", }; callback(null, response); return; } const util = require('util'); /* Rewrite uri */ request.uri = '/' + data.Item.S3Key; /* Added Query String */ request.querystring = data.Item.S3PrisignedQuery; callback(null, request); }); }; - { DynamoDBTableName: !Ref DynamoDBTable } LambdaProxyDeployVersion: Type: "AWS::Lambda::Version" Properties: FunctionName: !Ref LambdaProxy LambdaGenerateS3PresingedURL: Type: "AWS::Lambda::Function" Properties: Handler: index.handler MemorySize: 512 Role: !GetAtt LambdaExecRole.Arn Runtime: nodejs6.10 Timeout: 30 Code: ZipFile: !Sub - | 'use strict'; const aws = require('aws-sdk'); const endpoint = new aws.Endpoint("s3.amazonaws.com"); const s3 = new aws.S3({ apiVersion: '2006-03-01', signatureVersion: 'v4', endpoint: endpoint }); const docClient = new aws.DynamoDB.DocumentClient({region: '${AWS::Region}'}); const url = require('url'); exports.handler = (event, context, callback) => { const bucket = event.Records[0].s3.bucket.name; const key = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, ' ')); const expireSec = 60 * 10; const presingedURL = s3.getSignedUrl('getObject', { Bucket: bucket, Key: key, Expires: expireSec }); console.log('Generate PresingedURL.'); console.log(presingedURL); const presignedQuery = url.parse(presingedURL).query; const params = { TableName: '${DynamoDBTableName}', Item: { 'ShortURL': Math.random().toString(36).slice(-8), 'S3Key': key, 'S3PrisignedQuery': presignedQuery } }; docClient.put(params, function(err, data) { if (err) { console.error("Unable to add item. Error JSON:", JSON.stringify(err, null, 2)); } else { console.log("Added item:", JSON.stringify(data, null, 2)); } }); }; - { DynamoDBTableName: !Ref DynamoDBTable } LambdaInvokePermission: Type: "AWS::Lambda::Permission" Properties: FunctionName: !GetAtt LambdaGenerateS3PresingedURL.Arn Action: "lambda:InvokeFunction" Principal: s3.amazonaws.com SourceAccount: !Ref AWS::AccountId
動作確認
上記テンプレートを使用した事を前提として動作確認を行っていきます。
Cloud FormationのOutputタブを確認
出力されている項目を控えておきましょう。
- CloudFrontURL
- DynamoDBTableName
- S3BucketName
S3バケットにオブジェクトをアップロード
S3BucketNameで表示されたバケットにオブジェクトをアップロードします。
今回はhello worldと書かれただけのhtmlをアップロードしました。
$ echo "hello world" > hello.html $ aws s3 cp hello.html s3://presingedurlproxy2-s3bucket-s98otf4r11ov upload: ./hello.html to s3://presingedurlproxy2-s3bucket-s98otf4r11ov/hello.html
DynamoDBの登録内容を確認する
DynamoDBTableNameで表示されたテーブルを確認します。
S3 PUTをトリガーに起動されたLambdaにより、新しいItemが登録されていることが確認できます。
$ aws dynamodb scan --table-name PresingedURLProxy2-DynamoDBTable-6PNHTTGS8MNM { "Items": [ { "S3Key": { "S": "hello.html" }, "ShortURL": { "S": "pee6zuxr" }, "S3PrisignedQuery": { "S": "X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ASIAJTONTYX635YGUGFQ%2F20180110%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20180110T081138Z&X-Amz-Expires=600&X-Amz-Security-Token=FQoDYXdzEOn%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaDIm2aaU%2BCSkXPOo8IyKWAim%2B6fln3qiscBisqXqbF09Ofxn62mNkovBdkxw1riLMxKM%2BumxIwUq19QppQNrmnSyhKBjfvpM5FnLKB441Pc0bmzcuaykjfKUHmiekTtqxKj03gbM%2B8BgwAdaXJbS%2BgeDU%2BB9sAwl2w9j%2BzsqtYSuNF8MtPwmner2eV0jxineURCNIODeklgk%2FyRUKz4U%2BssIbk4%2Bf6qdyNcFBkHV8JO5wsXfduSEjxUPBrqTFt%2FlFFK8QoE01x1b9rl8jZ%2BCfsWMj%2FN6c8boY1QBwQspg4gTd9wC31ROvRc1QCq9B7hU0ie78DPS%2BkGBt%2B5m2Xbt0oAlpgnMr8sPRvKN2wXA2mvY1jYvUeiFoxmMjG0d2ngob7xVrkKCwKImI19IF&X-Amz-Signature=218d4973fc2502ae942f06002ee17f1a582ddf711265da583858ec1ff5b86e67&X-Amz-SignedHeaders=host" } } ], "Count": 1, "ScannedCount": 1, "ConsumedCapacity": null }
ShortURLを控えましょう。
CloudFrontに短縮URLでアクセスする
それでは短縮URLにアクセスしてみます。
ドメインにはCloudFrontURLを使ってください。
$ curl -vvv -X GET https://d14gehme3xkgip.cloudfront.net/pee6zuxr * Trying 13.32.50.11... * Connected to d14gehme3xkgip.cloudfront.net (13.32.50.11) port 443 (#0) * TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 * Server certificate: *.cloudfront.net * Server certificate: Symantec Class 3 Secure Server CA - G4 * Server certificate: VeriSign Class 3 Public Primary Certification Authority - G5 > GET /pee6zuxr HTTP/1.1 > Host: d14gehme3xkgip.cloudfront.net > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: text/html < Content-Length: 12 < Connection: keep-alive < Date: Wed, 10 Jan 2018 08:21:28 GMT < Last-Modified: Wed, 10 Jan 2018 08:11:37 GMT < ETag: "6f5902ac237024bdd0c176cb93063dc4" < Accept-Ranges: bytes < Server: AmazonS3 < X-Cache: Miss from cloudfront < Via: 1.1 9063af643f5f74dbc0e44494f142a87f.cloudfront.net (CloudFront) < X-Amz-Cf-Id: VYFNZjPSCdntReqTnIGdx6jnD76Tz2lFrMConknNiTnVveRp4xKVRg== < hello world * Connection #0 to host d14gehme3xkgip.cloudfront.net left intact
成功です。
それではS3オブジェクトのパスであるhello.htmlでアクセスするとどうなるか。該当するShortURLがDynamoDBに登録されていないため404になるはずです。
$ curl -vvv -X GET https://d14gehme3xkgip.cloudfront.net/hello.html * Trying 54.192.233.250... * Connected to d14gehme3xkgip.cloudfront.net (54.192.233.250) port 443 (#0) * TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 * Server certificate: *.cloudfront.net * Server certificate: Symantec Class 3 Secure Server CA - G4 * Server certificate: VeriSign Class 3 Public Primary Certification Authority - G5 > GET /hello.html HTTP/1.1 > Host: d14gehme3xkgip.cloudfront.net > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 404 Not Found < Content-Length: 14 < Connection: keep-alive < Server: CloudFront < Date: Wed, 10 Jan 2018 08:23:09 GMT < X-Cache: Error from cloudfront < Via: 1.1 2fe788985cac89c0ef661ff7cd8edf63.cloudfront.net (CloudFront) < X-Amz-Cf-Id: tyQof7QnAQy9p5XzryvT0F6jyEFdoBx2dzYoor3H0PmA-bKvSin_AQ== < * Connection #0 to host d14gehme3xkgip.cloudfront.net left intact 404 Not Found.
404になりましたね。
なお、今回は試していませんが同じ要領でS3へPUTすることも可能です。
興味がある方はぜひ試してみてください。
解説
今回の構成についての解説です。
署名計算 + DynamoDBへのItem登録処理
S3 Putをトリガーとして起動したLambdaの処理について少しだけ解説です。
まずは、短縮URLに相当する文字列の生成と署名の計算を行います。
そして、DynamoDBへ短縮URL、署名、S3オブジェクトのパスを保存して終了です。
やっていることはこれだけなのですが、署名計算時に一点注意点があります。
署名計算時のホスト名は必ず、バケット名.s3.amazonaws.comとして計算をしてください。
Cloud FrontがS3 Originにアクセスする時にホスト名のリライトをしてくれるのですが、この時に使われるホスト名の形式が上記となるようです。
この形式以外のホスト名を使うと、署名が合わなかったり(署名計算にホスト名を使うため)と色々とハマるポイントが生まれます。
検証時、ap-northeast-1のエンドポイントを使ってしまった為、随分とハマりました。。。
署名付きURLプロキシ
CloudFront + Lambda@Edge + DynamoDB + S3 の構成について。
CloudFront + Lambda@Edge
CloudFrontのオリジンがS3Originとして構成されていると、CloudFrontによりHost名のリライト(cloud frontからs3への変換)が自動的に行われます。
まずはここで第一のリクエストの加工が発生します。
CloudFrontでは、リクエストに該当するキャッシュが無かった場合のみLambda@Edgeがトリガーされます。これはオリジンリクエストで動くLambda@Edgeの仕様です。
トリガーされたLambda@Edgeは、短縮URLをキーにDynamoDBからアイテムを取得します。
該当するアイテムが無ければ404のレスポンスを返しますが、アイテムが取得できた場合はS3へ渡すリクエストを更に加工していきます。
加工するポイントは2つです。
まず、リクエストのURIは短縮URLのままなので、S3オブジェクトへのパスへと変換します。
最後に、リクエストのクエリストリングに署名情報を突っ込みます。
これでCloudFrontからS3へのリクエストは、署名付きURLによるリクエストに置き換わりました。
実際に運用される場合は、DynamoDBのTTLや署名の期限なども考慮した方が良いと思います。
さいごに
Lambda@Edgeの上手い使い方がわからなかったのですが、今回はなかなか面白い構成ができたと思っています。
他にもS3にベーシック認証掛けたりもできるようなので、アーキテクチャの選定時にはLambda@Edgeを忘れないでおこうと思います。
EC2-Classicな環境のハマりどころ(EC2編)
はじめに
EC2-Classicをご存知でしょうか。
まだVPCがデフォルトでは無かった時代に作成したAWSアカウントで利用可能な環境で、EC2-Classic環境上で構築したELBのセキュリティグループは基本フルオープンだったりします。
Classicを冠しているその名からも解る通り、今となっては古い環境ですので名前だけは知っているが実際に触れた事の無い方も多いのではないかと思います。
本題
とはいえ、今もどこかで元気に稼働しているEC2-Classicな環境もあるわけで、当然EC2のSystem Rebootなどのメンテナンス通知なども届くわけですね。
そんな時VPC Defaultな感覚でrebootやstop->startをすると少しハマるかもしれない、というのが今回のお話です。
上記ドキュメントの、インスタンスの停止と起動を見てみましょう。
インスタンスがEC2-Classicで実行されている場合、インスタンスは新しいプライベートIPv4アドレスを受け取ります。
そうなんです、プライベートIPが変わるんですね。
VPCの場合は停止していてもIPアドレスが保持されますが、EC2-Classicの場合は保持されません。
次、
つまり、プライベート IPv4 アドレスに関連付けられていた Elastic IP アドレスは、インスタンスとの関連付けが解除されます。
そう、パブリックIPも解除されてしまうんですね。EIP付けてるから安心だろう、というのは甘いということですね。これは意外とハマるのではないでしょうか。
なお、これはEC2の停止と起動を行った場合のお話で、再起動の場合は問題ありません。
System Rebootの対応として、EC2-Classicな環境でEC2のstop->startをすることがあれば思い出したいポイントですね。