Serverless Frameworkのプラグインを利用したLambda@Edgeのデプロイ

serverless-framework

はじめに

こんにちは、中山です。

少し前の話になりますが、CloudFrontのエッジロケーションでLambdaを動作させられるLambda@EdgeがGAになりました。めでたい。エッジコンピューティングのビックウェーブを感じます。

非常に可能性のあるLambda@Edge、とても便利なのですが個人的に少しだけ不満点があります。それはデプロイに手間がかかるという点です。Lambda@Edgeをデプロイするためには通常以下のフローを実施する必要があります。

  1. CloudFrontディストリビューションの作成
  2. Lambdaを作成してディストリビューションと関連付ける
  3. 適宜パーミッションを設定
  4. 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でもそのうちサポートされていくでしょう。

今回ご紹介したプラグインを使っていて気付いたのですが、イベントタイプの削除には現状対応してないようです。例えば、関連付けるイベントタイプを変更した場合、古い設定が残ってしまっています。このあたり今後に期待したいですね。

本エントリがみなさんの参考になれば幸いに思います。