CNAMEを設定したCloudFront DistributionのCDK Constructを「CloudFrontWebDistribution」から「Distribution」にダウンタイムなしで置き換えたい場合は、Logical IDのオーバーライドが選択肢になりそう

2022.02.23

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

前回のエントリでは、Amazon CloudFrontのディストリビューションの新しいCDK Constructである「Distribution」を使って静的サイト配信の構築を行いました。

一方で、ディストリビューションのリソースが従来の「CloudFrontWebDistribution」で既に構築されてしまっているが、これを「Distribution」に置き換えたい!という状況もあるかと思います。

しかしCNAME(カスタムドメイン)が設定されているディストリビューションの場合、一度のCDKデプロイで「CloudFrontWebDistribution」から「Distribution」へ置き換えをしようとすると、複数ディストリビューション間でのCNAMEの重複エラーが発生します。

11:47:20 PM | CREATE_FAILED        | AWS::CloudFront::Distribution                   | distribution114A0A2A
Resource handler returned message: "Invalid request provided: One or more of the CNAMEs you provided are already associated with a different resource. (Service: CloudFront, Status Code: 409, Request ID: f79e9134-f2b4-43c4-8a1f-55a
9223fba29, Extended Request ID: null)" (RequestToken: a94e7024-6264-7670-d64c-248571bbf56f, HandlerErrorCode: InvalidRequest)

重複エラーの回避策としては、既存のディストリビューションの削除と、新しいディストリビューションの作成を2回に分けてCDKデプロイを行う方法もありますが、その場合はWebサイトのダウンタイムが発生してしまいます。

ダウンタイムの回避策としては、下記の方法も紹介されていますが、手動での手当が必要となるため、CDKによる自動デプロイを徹底したい場合は相容れません。

そこで今回は、CNAMEを設定したCloudFront DistributionのCDK Constructを「CloudFrontWebDistribution」から「Distribution」にダウンタイムなしで置き換える方法があったので共有します。

ダウンタイムなしで置き換えできた方法

既存の「CloudFrontWebDistribution」で作成したリソースのLogical IDを「Distribution」へオーバーライド(ハイライト部分の記述)することにより、ダウンタイムなしでの置き換えを実現できました。

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 acm from '@aws-cdk/aws-certificatemanager';

interface AwsCdkAppStackProps extends cdk.StackProps {
  certArn: string;
  customDomain: string;
}

export class AwsCdkAppStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props: AwsCdkAppStackProps) {
    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);

    //従来の「CloudFrontWebDistribution」によるディストリビューションの定義
    //記述を残すためコメントアウト
    /*
    const distribution = new cloudfront.CloudFrontWebDistribution(
      this,
      'Distribution',
      {
        aliasConfiguration: {
          acmCertRef: props.certArn,
          names: [props.customDomain],
          securityPolicy: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021,
        },
        defaultRootObject: 'index.html',
        comment: 'website-distribution',
        errorConfigurations: [
          {
            errorCachingMinTtl: 300,
            errorCode: 403,
            responseCode: 403,
            responsePagePath: '/error.html',
          },
          {
            errorCachingMinTtl: 300,
            errorCode: 404,
            responseCode: 404,
            responsePagePath: '/error.html',
          },
        ],
        originConfigs: [
          {
            s3OriginSource: {
              s3BucketSource: websiteBucket,
              originAccessIdentity: originAccessIdentity,
            },
            behaviors: [
              {
                isDefaultBehavior: true,
              },
            ],
          },
        ],
        priceClass: cloudfront.PriceClass.PRICE_CLASS_ALL,
      }
    );
    */

    //新しい「Distribution」によるディストリビューションの定義
    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,
        }),
      },
      priceClass: cloudfront.PriceClass.PRICE_CLASS_ALL,
      certificate: acm.Certificate.fromCertificateArn(
        this,
        'CustomDomainCertificate',
        props.certArn
      ),
      domainNames: [props.customDomain],
      minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021,
    });

    //Logical IDのオーバーライド
    (
      distribution.node.defaultChild as cloudfront.CfnDistribution
    ).overrideLogicalId('DistributionCFDistribution882A7313');

    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>'
        ),
      ],
      destinationBucket: websiteBucket,
      distribution: distribution,
      distributionPaths: ['/*'],
    });
  }
}

DistributionのLogical IDは、CLoudFormtionコンソールで確認することができます。

CDKデプロイを実施すると、正常に行うことができました。

またデプロイ後のWebサイトにカスタムドメインで正常にアクセスできています。

置き換え後に、Logical IDのオーバーライドの記述は削除せずに残す必要がある

さてConstructの「Distribution」への置き換えができた後は、次のデプロイ時にLogical IDのオーバーライドの記述を削除しても良さそうに思えます。

lib/aws-cdk-app-stack.ts

    //Logical IDのオーバーライド -- この記述は削除して良さそう?
    (
      distribution.node.defaultChild as cloudfront.CfnDistribution
    ).overrideLogicalId('DistributionCFDistribution882A7313');

しかし、記述を削除してデプロイをすると次のようなエラーとなりました。冒頭でも示した複数ディストリビューション間でのCNAMEの重複エラーと同じものです。

lib/aws-cdk-app-stack.ts

12:30:43 AM | CREATE_FAILED        | AWS::CloudFront::Distribution                   | Distribution830FAC52
Resource handler returned message: "Invalid request provided: One or more of the CNAMEs you provided are already associated with a different resource. (Service: CloudFront, Status Code: 409, Request ID: 7efcef8f-a1f3-48c0-afb8-acf
e13f27fc7, Extended Request ID: null)" (RequestToken: 8af4b31e-c72e-bf1e-77f3-c61585522aa0, HandlerErrorCode: InvalidRequest)

よって、今回示した方法では、ディストリビューションの置き換え後に、Logical IDのオーバーライドの記述は削除せずに残す必要があるという結論となりました。

内部で元のディストリビューションリソースを保持しているのでしょうか?削除する方法が分かる方がいたら教えてください。

参考

以上