Serverless Frameworkのプラグインを利用したLambda@Edgeのデプロイ
はじめに
こんにちは、中山です。
少し前の話になりますが、CloudFrontのエッジロケーションでLambdaを動作させられるLambda@EdgeがGAになりました。めでたい。エッジコンピューティングのビックウェーブを感じます。
非常に可能性のあるLambda@Edge、とても便利なのですが個人的に少しだけ不満点があります。それはデプロイに手間がかかるという点です。Lambda@Edgeをデプロイするためには通常以下のフローを実施する必要があります。
- CloudFrontディストリビューションの作成
- Lambdaを作成してディストリビューションと関連付ける
- 適宜パーミッションを設定
- Lambdaのコードを修正する度にディストリビューションを更新する
特に4番目が厳しい。執筆時点(2017/08/28)でLambdaとCloudFrontの関連付けはQualified ARNで指定する必要があります。新しいバージョンをPublishする度(つまりLambdaのソースを修正する度)にこの作業を実施しなければならないということです。
サーバレスアーキテクチャを採用している場合、恐らく多くの方はServerless FrameworkやAWS SAMなどのツールを利用していると思います。こういったツールを利用することで簡単にサーバレスアーキテクチャをデプロイ可能です。ただし、残念ながら執筆時点でLambda@Edgeをコア機能としてサポートしているツールは(自分の観測範囲では)無いようでした。
どうしたものかと思っていたのですが、先日Serverless FrameworkでLambda@Edgeをデプロイするためのプラグインが公開されました。これを利用すれば面倒な作業から開放されそうですね。
早速使ってみたので本エントリでご紹介したいと思います。
検証環境
- Serverless Framework: 1.20.2
- serverless-plugin-cloudfront-lambda-edge: 1.0.0
- serverless-s3-sync: 1.3.0
- Node.js: node:8.2.1-alpine(Dockerを利用)
やってみた
詳細な設定方法については該当リポジトリのREADMEを参照してください。といっても使い方は通常のプラグインと同じです。
設定ファイル
今回は以下のような設定にしてみました。
serverless.yml
service: lambda-at-edge frameworkVersion: ">=1.20.2 <2.0.0" custom: config: ${{file(config.yml)}} s3Sync: - bucketName: ${{env:S3_BUCKET_NAME}} localDir: static provider: name: aws runtime: nodejs6.10 stage: ${{self:custom.config.stage}} region: us-east-1 memorySize: 128 timeout: 1 variableSyntax: "\\${{([ :a-zA-Z0-9._,\\-\\/\\(\\)]+?)}}" package: individually: true exclude: - "**" plugins: - serverless-s3-sync - serverless-plugin-cloudfront-lambda-edge functions: rewriter: handler: src/handlers/rewriter/index.handler lambdaAtEdge: distribution: WebsiteDistribution eventType: viewer-request package: include: - src/handlers/rewriter/*.js resources: ${{file(resources.yml)}}
27と32 - 34行目が serverless-plugin-cloudfront-edge
の設定です。 eventType
でviewer requestにLambdaを指定しています。 distribution
に設定する文字列はCloudFormationの論理リソース名です。
Lambda@Edgeの場合、デプロイ先となるリージョンは北部バージニアになります。このリージョンから各リージョンにレプリカが作成され、アクセス先となるエッジロケーションでLambdaが実行されるという仕組みになっているようです。
本題から少しはずれますが、7 - 9と26行目はserverless-s3-syncプラグインの設定です。 sls deploy
などのタイミングでローカルディレクトリのファイルをS3バケットにsyncしてくれます。Serverless Framework内でアップロードするオブジェクトも管理したい場合に便利です。今回は static
ディレクトリ以下にHTMLファイルを設置するようにしました。
resources.yml
--- AWSTemplateFormatVersion: "2010-09-09" Description: Lambda@Edge Sample Stack Parameters: RetentionInDays: Type: Number Default: ${{self:custom.config.logGroup.retentionInDays}} S3BucketName: Type: String Default: ${{env:S3_BUCKET_NAME}} AcmIdentifier: Type: String Default: ${{env:ACM_IDENTIFIER}} HostedZoneId: Type: AWS::Route53::HostedZone::Id Default: ${{env:HOSTED_ZONE_ID}} Resources: RewriterLogGroup: Type: AWS::Logs::LogGroup Properties: RetentionInDays: Ref: RetentionInDays WebsiteBucket: Type: AWS::S3::Bucket Properties: BucketName: Ref: S3BucketName AccessControl: PublicRead WebsiteConfiguration: IndexDocument: index.html ErrorDocument: error.html WebsiteBucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: Ref: WebsiteBucket PolicyDocument: Statement: - Effect: Allow Action: s3:GetObject Resource: Fn::Sub: ${WebsiteBucket.Arn}/* Principal: "*" WebsiteDistribution: Type: AWS::CloudFront::Distribution Properties: DistributionConfig: Enabled: true Comment: Ref: AWS::StackName PriceClass: PriceClass_200 Aliases: - Ref: S3BucketName HttpVersion: http2 DefaultRootObject: index.html Origins: - Id: Fn::Sub: S3-${WebsiteBucket} DomainName: Fn::GetAtt: [ WebsiteBucket, DomainName ] S3OriginConfig: {} DefaultCacheBehavior: TargetOriginId: Fn::Sub: S3-${WebsiteBucket} ForwardedValues: QueryString: false Cookies: Forward: none ViewerProtocolPolicy: redirect-to-https DefaultTTL: 600 MaxTTL: 600 Compress: true ViewerCertificate: SslSupportMethod: sni-only AcmCertificateArn: Fn::Sub: arn:aws:acm:${AWS::Region}:${AWS::AccountId}:certificate/${AcmIdentifier} WebSiteRecordSet: Type: AWS::Route53::RecordSet Properties: HostedZoneId: Ref: HostedZoneId Name: Ref: S3BucketName Type: A AliasTarget: HostedZoneId: Z2FDTNDATAQYW2 DNSName: Fn::GetAtt: [ WebsiteDistribution, DomainName ]
静的ウェブサイト機能を有効化したS3を作成し、それをオリジンとしたCloudFrontディストリビューションを設定しています。今回はカスタムドメインでディストリビューションにアクセスできるよう、レコードセットリソースも作ってみました。
Lambdaのコード
index.html
以外でアクセスしてきた場合は rewrite.html
へ書き換えているだけです。
src/handlers/rewriter/index.js
module.exports.handler = (event, context, callback) => { console.log(JSON.stringify(event)); const request = event.Records[0].cf.request; if (request.uri !== '/index.html') { console.log(`changing ${request.uri} to rewrite.html`); request.uri = '/rewrite.html'; } callback(null, request); };
デプロイ
いつもと同じようにデプロイします。すると以下のような出力が表示されると思います。
$ $(yarn bin)/sls deploy -v Serverless: Packaging service... Serverless: Excluding development dependencies... Serverless: Updated Lambda assume role policy to allow Lambda@Edge to assume the role <snip> Serverless: Checking to see if 1 function needs to be associated to CloudFront Serverless: Waiting for CloudFront distribution "WebsiteDistribution" to be deployed . Serverless: Distribution "WebsiteDistribution" is now in "Deployed" state Serverless: Adding new Lamba@Edge association for origin-request: arn:aws:lambda:us-east-1:************:function:lambda-at-edge-v1-rewriter:2 Serverless: Updating distribution "WebsiteDistribution" because we updated Lambda@Edge associations on it Serverless: Waiting for CloudFront distribution "WebsiteDistribution" to be deployed ............................................................................................................................................................................................................................................................ Serverless: Distribution "WebsiteDistribution" is now in "Deployed" state Serverless: Done updating distribution "WebsiteDistribution" Done in 528.17s.
ハイライトした箇所に実行内容を記載してくれていますが、まずLambdaのIAM Roleを更新した後、デプロイが終わった段階でディストリビューションを更新、Lambdaとの関連付けを実施しています。
逆に、Lambdaのコードを変更していない(新しいバージョンがPublishされていない)状態で sls deploy
してもディストリビューションの設定は変更されないようです。親切設計です。
$ $(yarn bin)/sls deploy -v <snip> Serverless: Waiting for CloudFront distribution "WebsiteDistribution" to be deployed . Serverless: Distribution "WebsiteDistribution" is now in "Deployed" state Serverless: The distribution is already configured with the current versions of each Lambda@Edge function it needs
動作確認
- ディストリビューションにLambdaが関連付けされている
$ aws cloudfront get-distribution-config \ --id E3L5K2SO5WFZ65 \ --query 'DistributionConfig.DefaultCacheBehavior.LambdaFunctionAssociations' { "Items": [ { "EventType": "viewer-request", "LambdaFunctionARN": "arn:aws:lambda:us-east-1:************:function:lambda-at-edge-v1-rewriter:3" } ], "Quantity": 1 }
- IAM Roleの信頼関係に
edgelambda.amazonaws.com
が追加されている
$ aws iam get-role \ --role-name lambda-at-edge-v1-us-east-1-lambdaRole \ --query 'Role.AssumeRolePolicyDocument.Statement[].Principal' [ { "Service": [ "edgelambda.amazonaws.com", "lambda.amazonaws.com" ] } ]
- Lambdaにリソースポリシーが設定されている
$ aws lambda get-policy \ --function-name arn:aws:lambda:us-east-1:************:function:lambda-at-edge-v1-rewriter:4 \ --region us-east-1 \ --output text | jq { "Version": "2012-10-17", "Id": "default", "Statement": [ { "Sid": "replicator.lambda.GetFunction", "Effect": "Allow", "Principal": { "Service": "replicator.lambda.amazonaws.com" }, "Action": "lambda:GetFunction", "Resource": "arn:aws:lambda:us-east-1:************:function:lambda-at-edge-v1-rewriter:4" } ] }
test.html
へアクセスするとrewrite.html
が表示される
$ curl https://<FQDN>/test.html rewirte.html
index.html
にアクセスするとそのまま
$ curl https://<FQDN>/index.html index.html
まとめ
いかがだったでしょうか。
Serverless Frameworkのプラグインを利用したLambda@Edgeのデプロイについてご紹介しました。今はまだプラグインという位置付けですが、今後コア機能としてLambda@Edgeがイベントタイプにサポートされていくと思います。GitHub上のイシューで議論されているようです。あるいはAWS SAMでもそのうちサポートされていくでしょう。
今回ご紹介したプラグインを使っていて気付いたのですが、イベントタイプの削除には現状対応してないようです。例えば、関連付けるイベントタイプを変更した場合、古い設定が残ってしまっています。このあたり今後に期待したいですね。
本エントリがみなさんの参考になれば幸いに思います。