[AWS CDK] Lambda@Edge関数の作成または更新時に自動的に最新バージョンを発行し、CloudFront Distributionに紐付ける

2022.02.27

こんにちは、CX事業本部 IoT事業部の若槻です。

今回は、AWS CDKで、Lambda@Edge関数の作成または更新時に自動的に関数の最新バージョンの発行とCloudFront Distributionへの紐付けを行う方法を確認してみました。

Lambda@Edge関数の作成または更新時の手順

CloudFront Distributionに紐付けているLambda@Edge関数を作成または更新する際は、次の手順を踏む必要があります。

  1. Lambda@Edge関数の$LATESTバージョンを作成または更新する。
  2. 関数の最新バージョンを発行する。
  3. 発行した関数の最新バージョンをCloudFront Distributionに紐付けする。

Lambda@Edge関数およびCloudFront DistributionをAWS CDKで構築している場合に、上記の手順を行う方法を確認してみます。

環境

  • typescript@3.9.10
  • aws-cdk@1.145.0

やってみた

スタックの定義は次のようになります。ポイントとしては、Lambda@Edge関数との紐付け設定(edgeLambdas)でcurrentVersionにより関数バージョンを指定することです。

lib/aws-cdk-app-stack.ts

import * as cdk from '@aws-cdk/core';
import * as cloudfront from '@aws-cdk/aws-cloudfront';
import * as iam from '@aws-cdk/aws-iam';
import * as s3 from '@aws-cdk/aws-s3';
import * as s3deploy from '@aws-cdk/aws-s3-deployment';
import * as cloudfront_origins from '@aws-cdk/aws-cloudfront-origins';
import * as path from 'path';
import * as lambda from '@aws-cdk/aws-lambda';

export class AwsCdkAppStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props: cdk.StackProps) {
    super(scope, id, props);

    const websiteBucket = new s3.Bucket(this, 'WebsiteBucket', {
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    const originAccessIdentity = new cloudfront.OriginAccessIdentity(
      this,
      'OriginAccessIdentity',
      {
        comment: 'website-distribution-originAccessIdentity',
      }
    );

    const webSiteBucketPolicyStatement = new iam.PolicyStatement({
      actions: ['s3:GetObject'],
      effect: iam.Effect.ALLOW,
      principals: [
        new iam.CanonicalUserPrincipal(
          originAccessIdentity.cloudFrontOriginAccessIdentityS3CanonicalUserId
        ),
      ],
      resources: [`${websiteBucket.bucketArn}/*`],
    });

    websiteBucket.addToResourcePolicy(webSiteBucketPolicyStatement);

    //Lambda@Edge関数
    const websiteAddHeaderFunction = new cloudfront.experimental.EdgeFunction(
      this,
      'WebsiteAddHeaderFunction',
      {
        code: lambda.Code.fromAsset(
          path.join(__dirname, '../src/lambda/website-add-header')
        ),
        handler: 'index.handler',
        runtime: lambda.Runtime.NODEJS_14_X,
      }
    );

    const distribution = new cloudfront.Distribution(this, 'Distribution', {
      comment: 'website-distribution',
      defaultRootObject: 'index.html',
      errorResponses: [
        {
          ttl: cdk.Duration.seconds(300),
          httpStatus: 403,
          responseHttpStatus: 403,
          responsePagePath: '/error.html',
        },
        {
          ttl: cdk.Duration.seconds(300),
          httpStatus: 404,
          responseHttpStatus: 404,
          responsePagePath: '/error.html',
        },
      ],
      defaultBehavior: {
        allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD,
        cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD,
        cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
        viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        origin: new cloudfront_origins.S3Origin(websiteBucket, {
          originAccessIdentity,
        }),
        //Lambda@Edge関数との紐付け設定
        edgeLambdas: [
          {
            eventType: cloudfront.LambdaEdgeEventType.ORIGIN_RESPONSE,
            functionVersion: websiteAddHeaderFunction.currentVersion,
          },
        ],
      },
      priceClass: cloudfront.PriceClass.PRICE_CLASS_ALL,
    });

    new s3deploy.BucketDeployment(this, 'WebsiteDeploy', {
      sources: [
        s3deploy.Source.data(
          '/index.html',
          `<html><body><h1>Hello World</h1></body></html>`
        ),
        s3deploy.Source.data(
          '/error.html',
          `<html><body><h1>Error!!!!!!!!!!!!!</h1></body></html>`
        ),
        s3deploy.Source.data('/favicon.ico', ''),
      ],
      destinationBucket: websiteBucket,
      distribution: distribution,
      distributionPaths: ['/*'],
    });
  }
}

動作

初回作成時

まずは初回作成時の動作を見てみます。

CDKデプロイを行い、Lambda@Edge関数およびDistributionへの紐付けを作成します。

作成後に、関数のバージョン一覧を確認すると、$LATEST1が発行されています。

$ aws lambda list-versions-by-function \
  --region us-east-1 \
  --function-name ${functionName}
{
    "Versions": [
        {
            "FunctionName": "websiteAddHeaderFunction",
            "FunctionArn": "arn:aws:lambda:us-east-1:XXXXXXXXXXXX:function:websiteAddHeaderFunction:$LATEST",
            "Runtime": "nodejs14.x",
            "Role": "arn:aws:iam::XXXXXXXXXXXX:role/AwsCdkAppStack2-websiteAddHeaderFunctionFnServiceR-1LJSCURJNT1ZD",
            "Handler": "index.handler",
            "CodeSize": 308,
            "Description": "",
            "Timeout": 3,
            "MemorySize": 128,
            "LastModified": "2022-02-27T15:41:58.463+0000",
            "CodeSha256": "8Ux3E5BZPzZ8zf53zCb5sJ5R57r5iKCT/GXhW7k6qSA=",
            "Version": "$LATEST",
            "TracingConfig": {
                "Mode": "PassThrough"
            },
            "RevisionId": "8563719c-ea6a-4460-a150-20be01c2dc2a",
            "PackageType": "Zip",
            "Architectures": [
                "x86_64"
            ]
        },
        {
            "FunctionName": "websiteAddHeaderFunction",
            "FunctionArn": "arn:aws:lambda:us-east-1:XXXXXXXXXXXX:function:websiteAddHeaderFunction:1",
            "Runtime": "nodejs14.x",
            "Role": "arn:aws:iam::XXXXXXXXXXXX:role/AwsCdkAppStack2-websiteAddHeaderFunctionFnServiceR-1LJSCURJNT1ZD",
            "Handler": "index.handler",
            "CodeSize": 308,
            "Description": "",
            "Timeout": 3,
            "MemorySize": 128,
            "LastModified": "2022-02-27T15:42:19.808+0000",
            "CodeSha256": "8Ux3E5BZPzZ8zf53zCb5sJ5R57r5iKCT/GXhW7k6qSA=",
            "Version": "1",
            "TracingConfig": {
                "Mode": "PassThrough"
            },
            "RevisionId": "0c280f9f-5d63-464a-b3c2-93d2370147c2",
            "PackageType": "Zip",
            "Architectures": [
                "x86_64"
            ]
        }
    ]
}

またDistributionの関数紐付け設定を見ると、関数のバージョン1が設定されています。

$ aws cloudfront get-distribution \
  --region us-east-1 \
  --id ${distributionId} \
  --query 'Distribution.DistributionConfig.DefaultCacheBehavior.LambdaFunctionAssociations'
{
    "Quantity": 1,
    "Items": [
        {
            "LambdaFunctionARN": "arn:aws:lambda:us-east-1:XXXXXXXXXXXX:function:websiteAddHeaderFunction:1",
            "EventType": "viewer-response",
            "IncludeBody": false
        }
    ]
}

関数更新時

Lambda@Edge関数を何かしら更新した際の動きも見てみます。関数のソースコードに変更を加え、CDKデプロイします。

デプロイ後の関数のバージョン一覧を確認すると、新しく2が発行されています。

$ aws lambda list-versions-by-function \
  --region us-east-1 \
  --function-name ${functionName} \
  --query 'Versions[2]'
{
    "FunctionName": "websiteAddHeaderFunction",
    "FunctionArn": "arn:aws:lambda:us-east-1:XXXXXXXXXXXX:function:websiteAddHeaderFunction:2",
    "Runtime": "nodejs14.x",
    "Role": "arn:aws:iam::XXXXXXXXXXXX:role/AwsCdkAppStack2-websiteAddHeaderFunctionFnServiceR-1LJSCURJNT1ZD",
    "Handler": "index.handler",
    "CodeSize": 321,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2022-02-27T15:56:28.725+0000",
    "CodeSha256": "1J6HzcVop9XOdXWhJgUIdjaz585LK6BDhhcUPPYNuRE=",
    "Version": "2",
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "e6dc5927-155a-49e7-8dc6-2d70d812722a",
    "PackageType": "Zip",
    "Architectures": [
        "x86_64"
    ]
}

またDistributionの関数紐付け設定を見ると、関数のバージョン2が設定されています。

$  aws cloudfront get-distribution \
  --region us-east-1 \
  --id ${distributionId} \
  --query 'Distribution.DistributionConfig.DefaultCacheBehavior.LambdaFunctionAssociations'
{
    "Quantity": 1,
    "Items": [
        {
            "LambdaFunctionARN": "arn:aws:lambda:us-east-1:XXXXXXXXXXXX:function:websiteAddHeaderFunction:2",
            "EventType": "viewer-response",
            "IncludeBody": false
        }
    ]
}

また、関数の設定(タイムアウト値など)を更新した場合でも、同様の動作となることを確認できました。

その他

$LATESTバージョンは紐付けできない

関数の$LATESTバージョンはDistributionに紐付けできないことを確認してみます。

$LATESTバージョンの指定はlatestVersionを使用します。

lib/aws-cdk-app-stack.ts

        edgeLambdas: [
          {
            eventType: cloudfront.LambdaEdgeEventType.ORIGIN_RESPONSE,
            functionVersion: websiteAddHeaderFunction.latestVersion,
          },
        ],

変更をCDKデプロイすると、$LATESTは設定できない旨のエラーとなりました。想定通りの動作です。

$  cdk deploy

/Users/wakatsuki.ryuta/projects/cm-rwakatsuki/aws-cdk-app/node_modules/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts:101
    throw new Error('$LATEST function version cannot be used for Lambda@Edge');

現在はaddVersionメソッドはDeprecated

aws-lambda/Functionクラスでは、今まで関数の新しいバージョンを発行する際にはaddVersionを使用してきたかと思いますが、現在はDeprecatedとなっています。そして代わりにcurrentVersionを使用してバージョンを取得するようにとあります。

⚠️ Deprecated: This method will create an AWS::Lambda::Version resource which snapshots the AWS Lambda function at the time of its creation and it won't get updated when the function changes. Instead, use this.currentVersion to obtain a reference to a version resource that gets automatically recreated when the function configuration (or code) changes.

一方で今回Lambda@Edge関数の作成に使用したaws-cloudfront-origins/experimental/EdgeFunctionでは、addVersionは使えません。currentVersionなどでバージョンを指定する必要があります。

参考

以上