[AWS CDK] Lambda@Edge Functionを含んだスタックの削除が失敗する際の対処

2021.07.12

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

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

今回は、Lambda@Edge FunctionのConstructを含んだAWS CDKスタックの削除でエラーが発生して失敗する際の対処についてです。

エラーが発生したCDK定義

CloudFront Distribution + S3 Bucket + Lambda@Edge Functionから成る静的ホスティングされたWebサイトを構築しているスタックです。

lib/sample-app-stack.ts

import * as cdk from "@aws-cdk/core";
import * as cloudfront from "@aws-cdk/aws-cloudfront";
import * as s3 from "@aws-cdk/aws-s3";
import * as s3deploy from "@aws-cdk/aws-s3-deployment";
import * as iam from "@aws-cdk/aws-iam";
import * as lambda from "@aws-cdk/aws-lambda";

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

    const accountId = cdk.Stack.of(this).account;

    const websiteBucket = new s3.Bucket(this, "WebsiteBucket", {
      bucketName: `sample-app-stack-website-${accountId}`,
      websiteErrorDocument: "index.html",
      websiteIndexDocument: "index.html",
    });

    const websiteIdentity = new cloudfront.OriginAccessIdentity(
      this,
      "WebsiteIdentity",
      {
        comment: `sample-stack-identity`,
      }
    );

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

    websiteBucket.addToResourcePolicy(webSiteBucketPolicyStatement);

    const websiteAddHeaderFunction = new lambda.Function(
      this,
      "WebsiteAddHeaderFunction",
      {
        code: lambda.Code.asset("lambda/add-header"),
        functionName: "add-header-function",
        handler: "index.handler",
        role: new iam.Role(this, "AllowLambdaServiceToAssumeRole", {
          assumedBy: new iam.CompositePrincipal(
            new iam.ServicePrincipal("lambda.amazonaws.com"),
            new iam.ServicePrincipal("edgelambda.amazonaws.com")
          ),
          managedPolicies: [
            {
              managedPolicyArn:
                "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
            },
          ],
        }),
        runtime: lambda.Runtime.NODEJS_14_X,
      }
    );

    const websiteAddHeaderFunctionVersion =
      websiteAddHeaderFunction.addVersion("v3");

    const websiteDistribution = new cloudfront.CloudFrontWebDistribution(
      this,
      "WebsiteDistribution",
      {
        comment: `sample-distribution`,
        errorConfigurations: [
          {
            errorCachingMinTtl: 300,
            errorCode: 403,
            responseCode: 200,
            responsePagePath: "/index.html",
          },
          {
            errorCachingMinTtl: 300,
            errorCode: 404,
            responseCode: 200,
            responsePagePath: "/index.html",
          },
        ],
        originConfigs: [
          {
            s3OriginSource: {
              s3BucketSource: websiteBucket,
              originAccessIdentity: websiteIdentity,
            },
            behaviors: [
              {
                isDefaultBehavior: true,
                lambdaFunctionAssociations: [
                  {
                    eventType: cloudfront.LambdaEdgeEventType.ORIGIN_RESPONSE,
                    lambdaFunction: lambda.Version.fromVersionArn(
                      this,
                      "WebsiteAddHeaderFunctionVersion",
                      websiteAddHeaderFunctionVersion.functionArn
                    ),
                  },
                ],
              },
            ],
          },
        ],
        priceClass: cloudfront.PriceClass.PRICE_CLASS_ALL,
      }
    );

    new s3deploy.BucketDeployment(this, "WebsiteDeploy", {
      sources: [s3deploy.Source.asset("./web/build")],
      destinationBucket: websiteBucket,
      distribution: websiteDistribution,
      distributionPaths: ["/*"],
    });
  }
}

スタックを削除しようとすると失敗する

Webサイトを削除するためにcdk destroyによりスタックを削除しようとすると下記のようにエラーとなり削除が失敗しました。

% cdk destroy
Are you sure you want to delete: SampleAppStack (y/n)? y
SampleAppStack: destroying...
11:13:34 PM | DELETE_FAILED        | AWS::Lambda::Function                           | WebsiteAddHeaderFunctionE7F821C0
Resource handler returned message: "Lambda was unable to delete arn:aws:lambda:us-east-1:XXXXXXXXXXXX:function:add-header-function:1 because it is a replicated function. Please see our documentation for Deleting Lambda@Edge Functions and Replicas. (Service: Lambda, Status Code: 400, Request ID: 5dde41cf-48a8-450f-9691-01c5a768a9d2, Extended Request ID: null)"

CloudFormationのコンソールからも同様のエラーが確認できます。

原因

調べると下記のドキュメントが見つかりました。

You can delete a Lambda@Edge function only when the replicas of the function have been deleted by CloudFront. Replicas of a Lambda function are automatically deleted in the following situations:

- After you remove the last association for the function from all of your CloudFront distributions. If more than one distribution uses a function, the replicas are deleted only after you remove the function association from the last distribution.
- After you delete the last distribution that a function was associated with.

Lambda@EdgeではFunctionのレプリカが作成され、Functionの削除はそのレプリカが削除されている必要があり、レプリカが削除されるためにはCloudFront Distributionとの関連付けが削除される必要があるとのことです。

対処

Lambda@Edge Function以外のリソースを先に削除し、その後にLambda@Edge Functionを削除するようにしてみます。

まずスタックのDELETE_FAILEDを解消します。CloudFormationのコンソールで該当のDELETE_FAILEDとなっているスタックを選択し、[削除]をクリックします。

削除に失敗しているリソースの保持にチェックを入れ、[スタックの削除]をクリックします。

スタックの削除が開始され、少しするとDELETE_COMPLETEとなり削除が完了しました。

Lambda@EdgeのFunctionはまだ残っています。コンソールから手動削除してみます。

削除できました。CloudFront Distributionとの関連付け削除から10分ほどしか経っていませんが、すでにFunctionのレプリカは削除されていたようです。

DELETE_FAILED発生時はCDKデプロイできない

ちなみに、CloudFront Distributionとの関連付けをCDKスタック上で削除して再デプロイすれば削除可能となるのでは?と思い、スタックを修正してcdk deployしたら下記のようにエラーとなりました。

% cdk deploy
SampleAppStack: deploying...
[0%] start: Publishing e121c7550422d68a5a46ba7cc1a34840337180d668d962e42fec3ac53f0681a8:current
[100%] success: Published e121c7550422d68a5a46ba7cc1a34840337180d668d962e42fec3ac53f0681a8:current
SampleAppStack: creating CloudFormation changeset...

 ❌  SampleAppStack failed: Error [ValidationError]: Stack:arn:aws:cloudformation:us-east-1:XXXXXXXXXXXX:stack/SampleAppStack/c43ef540-e319-11eb-bcf8-0e688fb565d9 is in DELETE_FAILED state and can not be updated.

DELETE_FAILEDとなったスタックはコンソールから手動で手当してあげる必要があるんですね。

CloudFront Functionでは同事象は発生しなかった

掲題通りですが、Lambda@Edge Functionと同じくCloudFront Distributionと関連付けられてエッジで動作するCloudFront Functionの場合は、スタックを削除しても同事象は発生しませんでした。

参考

以上