NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

注目のタグ

    AWS LambdaカスタムリソースでSSL証明書・基本認証・CloudFrontオリジンフェイルオーバーを作成するAWS Cloudformationスタックを別リージョンにデプロイする

    小西秀和です。
    AWS LambdaカスタムリソースでAWS Cloudformationスタックを別リージョンにデプロイする」の記事でAWS Cloudformationスタックを別リージョンにデプロイするAWS Lambdaカスタムリソースの実装例を説明しました。

    また、そのカスタムリソースの使用例として次の記事を書きました。

    今回は総まとめという位置づけでカスタムリソースを使用して、今までの記事で紹介したSSL証明書・基本認証・フェイルオーバー構成をまとめてAmazon S3+Amazon CloudFrontに追加する方法を紹介します。

    AWS Cloudformationスタックを別リージョンにデプロイするAWS Lambdaカスタムリソースについては元記事を参照してください。

    ※本記事および当執筆者のその他の記事で掲載されているソースコードは自主研究活動の一貫として作成したものであり、動作を保証するものではありません。使用する場合は自己責任でお願い致します。また、予告なく修正することもありますのでご了承ください。

    本記事で試す内容の概要図

    今回は次の内容をAWS Cloudformationとカスタムリソースを中心に試しています。
    【初回作成時】

    • 呼出元Cloudformationスタックは呼出元とカスタムリソースの双方のリージョンで使用するOriginAccessIdentity(OAI)を作成する
    • 呼出元Cloudformationスタックは次の内容をus-east-1にデプロイするカスタムリソースをそれぞれ呼び出し、作成した結果を呼出元Cloudformationスタックに返却する
      • AWS Certificate Manager(ACM)証明書
      • フェイルオーバー用セカンダリAmazon S3バケット
    • 呼出元Cloudformationスタックは呼出元リージョンのリソースを作成し、カスタムリソースからの返却値を使用してus-east-1リージョンのリソースを関連付ける

    【2回目更新時】

    • 呼出元Cloudformationスタックは次の内容をus-east-1にデプロイするカスタムリソースを呼び出し、作成した結果を呼出元Cloudformationスタックに返却する
      • 基本認証をするAWS Lambda@Edgeとそのバージョン
    • 呼出元Cloudformationスタックは呼出元リージョンのリソースを更新し、カスタムリソースからの返却値を使用してus-east-1リージョンのリソースを関連付ける

    LambdaカスタムリソースによるSSL証明書・基本認証・フェイルオーバー構成のスタックデプロイと関連付けの例
    LambdaカスタムリソースによるSSL証明書・基本認証・フェイルオーバー構成のスタックデプロイと関連付けの例

    各ファイル(テンプレート、関数)の名称設定

    AWS CloudformationテンプレートやAWS Lambdaカスタムリソースそれぞれが一意に必要なものを識別して連携できれば各ファイルの名称は任意ですが、説明のために次のように各ファイルの名称を設定します。

    • 呼出元AWS Cloudformationテンプレート
      ⇒ OrgSourceOfRunCfnACMS3LambdaEdgeOtherRegion.yml
    • us-east-1にセカンダリオリジン用Amazon S3バケットを作成するAWS CloudformationスタックをデプロイするAWS Lambdaカスタムリソース
      ⇒ CustomResourceToDeployCloudformationStack
    • ACM証明書を作成するAWS Cloudformationテンプレート
      ⇒ CfnACMCertificate.yml
    • セカンダリオリジン用Amazon S3バケットを作成するAWS Cloudformationテンプレート
      ⇒ CfnS3OtherRegion.yml
    • Amazon CloudFront用のLambda@Edgeを作成するAWS Cloudformationテンプレート
      ⇒ CfnBasicAuthLambdaEdge.yml

    各ファイルやパラメータの例

    各ファイルやパラメータの例を記載していきます。
    特に入力パラメータに関しては入力形式を知っていただくための例なので、使用する場合は各パラメータを要件に合わせて設定する必要があります。

    呼出元AWS Cloudformationテンプレートへの入力パラメータ例

    後述するカスタムリソースの呼出元となるAWS Cloudformationテンプレートに入力するパラメータの例です。
    入力値は例なので使用する場合は各パラメータを要件に合わせて設定する必要があります。
    次にYAML形式にコメントする形で説明しています。
    これらのパラメータはAWSマネジメントコンソールから呼出元AWS Cloudformationテンプレートを実行する場合は、各パラメータを手動入力する必要があります。

    #【共通】作成するS3バケットのサフィックスに追加する環境名
    env: dev
    #【共通】静的ウェブサイトホスティングに使用するS3バケット名
    bucketName: cfn-acm-s3-edge-cloudfront-20210829000000-hostingbucket1
    #【共通】セカンダリS3バケットを作成するスタックをus-east1にデプロイするLambdaカスタムリソースのARN
    runCfnInUsEast1LambdaARN: 'arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:CustomResourceToDeployCloudformationStack'
    #【ACM証明書用】ACM証明書を作成するためにus-east1にデプロイするCloudformationスタック名
    runCfnInUsEast1StackName4ACM: CfnAcm4CloudFrontDemo
    #【ACM証明書用】ACM証明書を作成するCloudformationテンプレートを保存しているバケット名
    cfnTplS3Bucket4ACM: h-o2k
    #【ACM証明書用】ACM証明書を作成するCloudformationテンプレートを保存しているバケット以降のオブジェクトキー
    cfnTplS3Key4ACM: CfnACMCertificate.yml
    #【ACM証明書用】ACM証明書を発行する対象ドメイン名
    customDomainName4ACM: cfn-acm-s3-lambdaedge-cloudfront.h-o2k.com
    #【ACM証明書用】ACM証明書を発行する対象ドメイン名を管理するRoute53のホストゾーンID
    hostedZoneId4ACM: XXXXXXXXXXXXXXXXXXXXX
    #【セカンダリS3用】セカンダリS3バケットを作成するためにus-east1にデプロイするCloudformationスタック名
    runCfnInUsEast1StackName4S3BK: Cfn2ndS34CloudFrontDemo
    #【セカンダリS3用】セカンダリS3バケットを作成するCloudformationテンプレートを保存しているバケット名
    cfnTplS3Bucket4S3BK: h-o2k
    #【セカンダリS3用】セカンダリS3バケットを作成するCloudformationテンプレートを保存しているバケット以降のオブジェクトキー
    cfnTplS3Key4S3BK: CfnS3OtherRegion.yml
    #【セカンダリS3用】静的ウェブサイトホスティングのバックアップに使用するセカンダリS3バケット名
    bucketName4S3BK: cfn-acm-s3-edge-cloudfront-20210829000000-hostingbucket2
    #【Lambda@Edge用】Lambda@Edgeを作成するためにus-east1にデプロイするCloudformationスタック名
    runCfnInUsEast1StackName4LambdaEdge: CfnLambdaEdge4CloudFrontDemo
    #【Lambda@Edge用】Lambda@Edgeを作成するCloudformationテンプレートを保存しているバケット名
    cfnTplS3Bucket4LambdaEdge: h-o2k
    #【Lambda@Edge用】Lambda@Edgeを作成するCloudformationテンプレートを保存しているバケット以降のオブジェクトキー
    cfnTplS3Key4LambdaEdge: CfnBasicAuthLambdaEdge.yml
    #【Lambda@Edge用】基本認証をするLambda@Edgeの関数名
    basicAuthFuncName4LambdaEdge: BasicAuthWithLambdaEdge
    #【Lambda@Edge用】基本認証をするLambda@Edgeを関連付けるAmazon CloudFrontのDistribution ID。AWS Secrets Managerシークレットの一意識別で使用する。
    #※スタックの初回Create処理でAmazon CloudFrontを作成してから、2回目のUpdate処理で入力する
    basicAuthCloudFrontDistId4LambdaEdge: XXXXXXXXXXXXX
    #【Lambda@Edge用】基本認証をするLambda@Edgeで認証するID。AWS Secrets Managerシークレットとして保管する。
    basicAuthID4LambdaEdge: Iam
    #【Lambda@Edge用】基本認証をするLambda@Edgeで認証するパスワード。AWS Secrets Managerシークレットとして保管する。
    basicAuthPW4LambdaEdge: Nobody
    

    呼出元AWS Cloudformationテンプレート

    OrgSourceOfRunCfnACMS3LambdaEdgeOtherRegion.yml

    カスタムリソースの呼出元となるAWS Cloudformationテンプレートの例を記載します。
    このテンプレートはAWS Amplify CLIでAmazon S3+Amazon CloudFrontの静的ウェブサイトホスティング環境を作成したときに生成されるJSONテンプレートをYAMLに変換し、ACM証明書、基本認証用Lambda@Edge、セカンダリS3バケットの作成処理および、これらのリソースのCloudFrontへの関連付け処理を追加したものです。
    このYAMLファイルに追加した処理やポイントとなる箇所についてコメントを追記しておきます。
    AWS Amplify CLIによるAmazon S3+Amazon CloudFrontの作成については次の記事を参照してください。

    AWSの静的ウェブサイトホスティングで入門するAWS Amplify(Console、CLI) - 構築編(Amplify CLI)

    AWSTemplateFormatVersion: '2010-09-09'
    Description: 'Hosting and ACM, Secondary S3 Bucket, Lambda@Edge resource stack creation'
    Parameters:
        env: #【共通】作成するS3バケットのサフィックスに追加する環境名
            Type: String
            Default: dev
        bucketName: #【共通】静的ウェブサイトホスティングに使用するS3バケット名
            Type: String
        runCfnInUsEast1LambdaARN: #【共通】ACM証明書、セカンダリS3バケット、Lambda@Edgeを作成するスタックをus-east1にデプロイするLambdaカスタムリソースのARN
            Type: String
        runCfnInUsEast1StackName4ACM: #【ACM用】ACM証明書を作成するためにus-east1にデプロイするCloudformationスタック名
            Type: String
        cfnTplS3Bucket4ACM: #【ACM用】ACM証明書を作成するCloudformationテンプレートを保存しているバケット名
            Type: String
        cfnTplS3Key4ACM: #【ACM用】ACM証明書を作成するCloudformationテンプレートを保存しているバケット以降のオブジェクトキー
            Type: String
        customDomainName4ACM: #【ACM用】ACM証明書を発行する対象ドメイン名
            Type: String
        hostedZoneId4ACM: #【ACM用】ACM証明書を発行する対象ドメイン名を管理するRoute53のホストゾーンID
            Type: String
        runCfnInUsEast1StackName4S3BK: #【セカンダリS3用】セカンダリS3バケットを作成するためにus-east1にデプロイするCloudformationスタック名
            Type: String
        cfnTplS3Bucket4S3BK: #【セカンダリS3用】セカンダリS3バケットを作成するCloudformationテンプレートを保存しているバケット名
            Type: String
        cfnTplS3Key4S3BK: #【セカンダリS3用】セカンダリS3バケットを作成するCloudformationテンプレートを保存しているバケット以降のオブジェクトキー
            Type: String
        bucketName4S3BK: #【セカンダリS3用】静的ウェブサイトホスティングのバックアップに使用するセカンダリS3バケット名
            Type: String
        runCfnInUsEast1StackName4LambdaEdge: #【Lambda@Edge用】Lambda@Edgeを作成するためにus-east1にデプロイするCloudformationスタック名
            Type: String
        cfnTplS3Bucket4LambdaEdge: #【Lambda@Edge用】Lambda@Edgeを作成するCloudformationテンプレートを保存しているバケット名
            Type: String
        cfnTplS3Key4LambdaEdge: #【Lambda@Edge用】Lambda@Edgeを作成するCloudformationテンプレートを保存しているバケット以降のオブジェクトキー
            Type: String
        basicAuthFuncName4LambdaEdge: #【Lambda@Edge用】基本認証をするLambda@Edgeの関数名
            Type: String
        basicAuthCloudFrontDistId4LambdaEdge: #【Lambda@Edge用】基本認証をするLambda@Edgeを関連付けるAmazon CloudFrontのDistribution ID。AWS Secrets Managerシークレットの一意識別で使用する。
            Type: String                      #※スタックの初回Create処理でAmazon CloudFrontを作成してから、2回目のUpdate処理で入力する
        basicAuthID4LambdaEdge: #【Lambda@Edge用】基本認証をするLambda@Edgeで認証するID。AWS Secrets Managerシークレットとして保管する。
            Type: String
        basicAuthPW4LambdaEdge: #【Lambda@Edge用】基本認証をするLambda@Edgeで認証するパスワード。AWS Secrets Managerシークレットとして保管する。
            Type: String
    Conditions:
        ShouldNotCreateEnvResources:
            'Fn::Equals':
                -
                    Ref: env
                - NONE
        ShouldCreateBasicAuthLambdaEdge: #入力パラメータにAmazon CloudFrontのDistribution IDがある場合はTrueでLambda@Edgeを作成、ない場合はFalseでLambda@Edge作成をスキップ。
            'Fn::Not': ['Fn::Equals':[{Ref: basicAuthCloudFrontDistId4LambdaEdge}, '']]
    Resources:
        S3Bucket:
            Type: 'AWS::S3::Bucket'
            DependsOn:
                #カスタムリソースでセカンダリS3バケットが作成されてからクロスリージョンレプリケーションを設定するためDependsOnにカスタムリソースを追加
                - RunCfnInUsEast1Func4S3BK
                - BucketBackupRole
            #DeletionPolicy: Retain
            Properties:
                BucketName:
                    'Fn::If':
                        - ShouldNotCreateEnvResources
                        - {Ref: bucketName}
                        - {'Fn::Join': ["", [{Ref: bucketName}, '-', {Ref: env}]]}
                WebsiteConfiguration:
                    IndexDocument: index.html
                    ErrorDocument: index.html
                CorsConfiguration:
                    CorsRules:
                        - {AllowedHeaders: [Authorization, Content-Length], AllowedMethods: [GET], AllowedOrigins: ['*'], MaxAge: 3000}
                VersioningConfiguration:
                    Status: Enabled
                ReplicationConfiguration: #カスタムリソースで作成したセカンダリS3バケットとのクロスリージョンレプリケーション設定をする
                    Role: !GetAtt
                        - BucketBackupRole
                        - Arn
                    Rules:
                        - Destination:
                            Bucket: !GetAtt
                                - RunCfnInUsEast1Func4S3BK
                                - Arn
                            StorageClass: STANDARD
                          Id: ReplicationRule
                          Prefix: ''
                          Status: Enabled
        BucketBackupRole: #クロスリージョンレプリケーションで使用するIAMロールを設定する
            Type: 'AWS::IAM::Role'
            Properties:
                AssumeRolePolicyDocument:
                    Statement:
                        - Action:
                            - 'sts:AssumeRole'
                          Effect: Allow
                          Principal:
                            Service:
                            - s3.amazonaws.com
        BucketBackupPolicy: #クロスリージョンレプリケーションで使用するIAMロールに適用するIAMポリシーを設定する
            Type: 'AWS::IAM::Policy'
            DependsOn:
                - RunCfnInUsEast1Func4S3BK
                - S3Bucket
            Properties:
                PolicyDocument:
                    Statement:
                        - Action:
                            - 's3:GetReplicationConfiguration'
                            - 's3:ListBucket'
                          Effect: Allow
                          Resource:
                            - !Join
                                - ''
                                - - 'arn:aws:s3:::'
                                  - !Ref S3Bucket
                        - Action:
                            - 's3:GetObjectVersion'
                            - 's3:GetObjectVersionAcl'
                          Effect: Allow
                          Resource:
                            - !Join
                                - ''
                                - - 'arn:aws:s3:::'
                                  - !Ref S3Bucket
                                  - /*
                        - Action:
                            - 's3:ReplicateObject'
                            - 's3:ReplicateDelete'
                          Effect: Allow
                          Resource:
                            - !Join
                                - ''
                                - - {'Fn::GetAtt': [RunCfnInUsEast1Func4S3BK, Arn]}
                                  - /*
                PolicyName: BucketBackupPolicy
                Roles:
                    - !Ref BucketBackupRole
        PrivateBucketPolicy:
            Type: 'AWS::S3::BucketPolicy'
            DependsOn:
                - OriginAccessIdentity
                - S3Bucket
            Properties:
                PolicyDocument:
                    Id: MyPolicy
                    Version: '2012-10-17'
                    Statement:
                        - {Sid: APIReadForGetBucketObjects, Effect: Allow, Principal: {CanonicalUser: {'Fn::GetAtt': [OriginAccessIdentity, S3CanonicalUserId]}}, Action: 's3:GetObject', Resource: {'Fn::Join': ["", ['arn:aws:s3:::', {Ref: S3Bucket}, '/*']]}}
                Bucket:
                    Ref: S3Bucket
        OriginAccessIdentity:
            Type: 'AWS::CloudFront::CloudFrontOriginAccessIdentity'
            Properties:
                CloudFrontOriginAccessIdentityConfig:
                    Comment: CloudFrontOriginAccessIdentityConfig
        #ACM証明書を発行するために呼び出すカスタムリソース
        RunCfnInUsEast1Func4ACM:
            Type: 'Custom::RunCfnInUsEast1Func4ACM'
            Properties:
                ServiceToken:
                    Ref: runCfnInUsEast1LambdaARN
                StackName:
                    Ref: runCfnInUsEast1StackName4ACM
                CfnTplS3Bucket:
                    Ref: cfnTplS3Bucket4ACM
                CfnTplS3Key:
                    Ref: cfnTplS3Key4ACM
                CustomDomainName:
                    Ref: customDomainName4ACM
                HostedZoneId:
                    Ref: hostedZoneId4ACM
        #CloudFrontをエイリアスレコードとしてRoute53ホストゾーンに登録するRoute53レコードセット
        Route53RecordSetGroup:
            Type: 'AWS::Route53::RecordSetGroup'
            DependsOn:
                - CloudFrontDistribution
            Properties:
                HostedZoneId:
                    Ref: hostedZoneId4ACM
                RecordSets:
                    -
                        Name: {Ref: customDomainName4ACM}
                        Type: A
                        #CloudFrontをエイリアスレコードとして登録する場合はエイリアスターゲットのHostedZoneIdが次の固定値となる
                        #参考:https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/quickref-route53.html#w2aac27c21c80c11
                        AliasTarget: {HostedZoneId: Z2FDTNDATAQYW2, DNSName: {'Fn::GetAtt': [CloudFrontDistribution, DomainName]}}
        #セカンダリS3バケットを発行するために呼び出すカスタムリソース
        RunCfnInUsEast1Func4S3BK:
            DependsOn:
                #OriginAccessIdentityが作成されてから別リージョンのセカンダリS3バケットを作成する
                - OriginAccessIdentity
            Type: 'Custom::RunCfnInUsEast1Func4S3BK'
            Properties:
                ServiceToken:
                    Ref: runCfnInUsEast1LambdaARN
                StackName:
                    Ref: runCfnInUsEast1StackName4S3BK
                CfnTplS3Bucket:
                    Ref: cfnTplS3Bucket4S3BK
                CfnTplS3Key:
                    Ref: cfnTplS3Key4S3BK
                Env:
                    Ref: env
                BucketName:
                    Ref: bucketName4S3BK
                S3CanonicalUserId: 
                    {'Fn::GetAtt': [OriginAccessIdentity, S3CanonicalUserId]}
        #Lambda@Edgeを作成するために呼び出すカスタムリソース
        RunCfnInUsEast1Func4LambdaEdge:
            Type: 'Custom::RunCfnInUsEast1Func4LambdaEdge'
            Properties:
                ServiceToken:
                    Ref: runCfnInUsEast1LambdaARN
                StackName:
                    Ref: runCfnInUsEast1StackName4LambdaEdge
                CfnTplS3Bucket:
                    Ref: cfnTplS3Bucket4LambdaEdge
                CfnTplS3Key:
                    Ref: cfnTplS3Key4LambdaEdge
                CloudFrontDistId:
                    Ref: basicAuthCloudFrontDistId4LambdaEdge
                BasicAuthFuncName:
                    Ref: basicAuthFuncName4LambdaEdge
                BasicAuthID:
                    Ref: basicAuthID4LambdaEdge
                BasicAuthPW:
                    Ref: basicAuthPW4LambdaEdge
                IsSkip: ##入力パラメータにAmazon CloudFrontのDistribution IDがない場合はカスタムリソースにIsSkip:trueを送信し、Lambda@Edge作成処理をスキップする。
                    'Fn::If':
                        - ShouldCreateBasicAuthLambdaEdge
                        - "false"
                        - "true"
        CloudFrontDistribution:
            Type: 'AWS::CloudFront::Distribution'
            DependsOn:
              - OriginAccessIdentity
              #カスタムリソースでセカンダリS3バケットが作成されてからCloudFrontに関連付けるためDependsOnにカスタムリソースを追加
              - RunCfnInUsEast1Func4S3BK
              #カスタムリソースでACM証明書が発行されてからCloudFrontに関連付けるためDependsOnにカスタムリソースを追加
              - RunCfnInUsEast1Func4ACM
              - S3Bucket
              #カスタムリソースで基本認証用Lambda@Edgeバージョンが作成されてからCloudFrontに関連付けるためDependsOnにカスタムリソースを追加
              - RunCfnInUsEast1Func4LambdaEdge
            Properties:
                DistributionConfig:
                    #CloudFrontのエイリアスにACM証明書を発行したドメイン名を設定する
                    Aliases:
                        - {Ref: customDomainName4ACM}
                    #CloudFrontにカスタムリソースで作成したACM証明書を設定する
                    ViewerCertificate:
                        SslSupportMethod: sni-only
                        MinimumProtocolVersion: TLSv1.2_2021
                        AcmCertificateArn: {'Fn::GetAtt': [RunCfnInUsEast1Func4ACM, AcmCertificateArn]}
                    HttpVersion: http2
                    Origins:
                        - {DomainName: {'Fn::GetAtt': [S3Bucket, RegionalDomainName]}, Id: hostingS3Bucket, S3OriginConfig: {OriginAccessIdentity: {'Fn::Join': ["", [origin-access-identity/cloudfront/, {Ref: OriginAccessIdentity}]]}}}
                        #カスタムリソースで作成したus-east-1のS3バケットをオリジンとして登録する
                        - {DomainName: {'Fn::GetAtt': [RunCfnInUsEast1Func4S3BK, RegionalDomainName]}, Id: hostingS3BucketSecondary, S3OriginConfig: {OriginAccessIdentity: {'Fn::Join': ["", [origin-access-identity/cloudfront/, {Ref: OriginAccessIdentity}]]}}}
                    #呼出元リージョンで作成したS3バケットとカスタムリソースで作成したus-east-1のS3バケットでオリジングループを作成する
                    OriginGroups: 
                      Items: 
                        - FailoverCriteria: 
                            StatusCodes: 
                              Items: 
                                  - 500
                                  - 502
                                  - 503
                                  - 504
                              Quantity: 4
                          Id: hostingS3BucketGroup
                          Members: 
                            Items: 
                              - OriginId: hostingS3Bucket
                              - OriginId: hostingS3BucketSecondary
                            Quantity: 2
                      Quantity: 1
                    Enabled: 'true'
                    DefaultCacheBehavior:
                        AllowedMethods: [GET, HEAD, OPTIONS]
                        TargetOriginId: hostingS3BucketGroup #オリジングループをターゲットに指定する
                        ForwardedValues: {QueryString: 'false'}
                        ViewerProtocolPolicy: redirect-to-https
                        DefaultTTL: 86400
                        MaxTTL: 31536000
                        MinTTL: 60
                        Compress: true
                        LambdaFunctionAssociations:
                          'Fn::If':
                            - ShouldCreateBasicAuthLambdaEdge
                            - [{EventType: viewer-request, IncludeBody: 'true', LambdaFunctionARN: {'Fn::GetAtt': [RunCfnInUsEast1Func4LambdaEdge, LambdaFunctionVersionArn]}}]
                            - { Ref: 'AWS::NoValue' }
                    DefaultRootObject: index.html
                    CustomErrorResponses:
                        - {ErrorCachingMinTTL: 300, ErrorCode: 400, ResponseCode: 200, ResponsePagePath: /}
                        - {ErrorCachingMinTTL: 300, ErrorCode: 403, ResponseCode: 200, ResponsePagePath: /}
                        - {ErrorCachingMinTTL: 300, ErrorCode: 404, ResponseCode: 200, ResponsePagePath: /}
    Outputs:
        Region:
            Value:
                Ref: 'AWS::Region'
        HostingBucketName:
            Description: 'Hosting bucket name'
            Value:
                Ref: S3Bucket
        WebsiteURL:
            Value:
                'Fn::GetAtt':
                    - S3Bucket
                    - WebsiteURL
            Description: 'URL for website hosted on S3'
        S3BucketSecureURL:
            Value:
                'Fn::Join':
                    - ""
                    -
                        - 'https://'
                        - {'Fn::GetAtt': [S3Bucket, DomainName]}
            Description: 'Name of S3 bucket to hold website content'
        CloudFrontDistributionID:
            Value:
                Ref: CloudFrontDistribution
        CloudFrontDomainName:
            Value:
                'Fn::GetAtt':
                    - CloudFrontDistribution
                    - DomainName
        CloudFrontSecureURL:
            Value:
                'Fn::Join':
                    - ""
                    -
                        - 'https://'
                        - {'Fn::GetAtt': [CloudFrontDistribution, DomainName]}
        CloudFrontOriginAccessIdentity:
            Value:
                Ref: OriginAccessIdentity
        CustomResourceSkiped:
            Value:
                'Fn::If':
                    - ShouldCreateBasicAuthLambdaEdge
                    - "false"
                    - "true"
    

    呼出元AWS Cloudformationテンプレートからカスタムリソースに渡される引数

    前述の呼出元AWS Cloudformationテンプレートのカスタムリソースから渡される引数はそれぞれ、次の記事に記載されたものとなります。

    カスタムリソースではこれらのイベントをそれぞれ受けて処理を実行します。

    ACM証明書、基本認証用Lambda@Edge、セカンダリS3バケットを作成するAWS Cloudformationスタックをus-east-1にデプロイするAWS Lambdaカスタムリソース

    関数名:CustomResourceToDeployCloudformationStack

    カスタムリソースはAWS LambdaカスタムリソースでAWS Cloudformationスタックを別リージョンにデプロイするで作成したものを使用します。
    このカスタムリソース内でACM証明書、基本認証用Lambda@Edge、セカンダリS3バケットを作成するCloudformationテンプレートをそれぞれ読み込み、呼出元Cloudformationスタックから渡された引数を使用してus-east-1にリソースを作成するCloudformationスタックをデプロイします。
    実行したスタックが完了し、リソースが作成されるとARNなど関連付けに必要な情報を呼出元に返却して呼出元Cloudformationスタックで作成されたリソースに関連付けます。

    ACM証明書、基本認証用Lambda@Edge、セカンダリS3バケットを作成するAWS Cloudformationテンプレート

    ACM証明書、基本認証用Lambda@Edge、セカンダリS3バケットを作成するAWS Cloudformationテンプレートはそれぞれ次の記事で記載のテンプレートを使用しています。
    呼出元Cloudformationスタックから、これらのテンプレートをそれぞれ実行するカスタムリソースを呼び出して対応するリソースを作成します。

    ACM証明書を作成するAWS Cloudformationテンプレート

    ファイル名:CfnACMCertificate.yml

    基本認証用Lambda@Edgeを作成するAWS Cloudformationテンプレート

    ファイル名:CfnBasicAuthLambdaEdge.yml

    セカンダリS3バケットを作成するAWS Cloudformationテンプレート

    ファイル名:CfnS3OtherRegion.yml

    構築手順

    1. AWS Lambda関数「CustomResourceToDeployCloudformationStack」を用意しておく
      基本認証用Lambda@Edgeバージョンを作成するAWS Cloudformationスタックをus-east-1にデプロイするAWS Lambdaカスタムリソースを予め準備しておく
    2. ACM証明書、基本認証用Lambda@Edge、セカンダリS3バケットを作成するCloudformationテンプレートファイルをS3バケットに置く
      「CfnACMCertificate.yml」「CfnBasicAuthLambdaEdge.yml」「CfnS3OtherRegion.yml」を作成するAWS CloudformationテンプレートをAmazon S3バケット(前述のパラメータ例では「h-o2k」)に予め配置しておく
    3. 呼出元Cloudformationテンプレートファイル「OrgSourceOfRunCfnACMS3LambdaEdgeOtherRegion.yml 」をAWS Cloudformationで実行する
      呼出元CloudformationテンプレートをAWSマネジメントコンソールなどからパラメータを入力の上でAWS Cloudformationスタックとして実行する
    4. 静的ウェブサイトホスティングに使用するS3バケットにコンテンツを追加する
      静的ウェブサイトホスティング用S3バケット(前述のパラメータ例では「cfn-acm-s3-edge-cloudfront-20210829000000-hostingbucket1-dev」)にindex.htmlなどコンテンツを追加する。
      呼出元Cloudformationスタックで作成したAmazon S3バケットに追加したコンテンツはカスタムリソースで作成したAmazon S3バケットにクロスリージョンレプリケーションされます。
      (今回の例では「CfnACMS3LambdaEdge4CloudFrontDemo」とだけ表示されるindex.htmlを追加しました。)
    5. 初回実行(Create処理でAmazon CloudFrontが作成され、Distribution IDが発行される。カスタムリソースによる基本認証用Lambda@Edgeバージョン作成は実行しない。)
      最初の呼出元AWS Cloudformationスタックの実行では「basicAuthCloudFrontDistId4LambdaEdge」にAmazon CloudFrontのDistribution IDを入力しないことで、基本認証用Lambda@Edgeを作成するAWS Cloudformationスタックの作成処理はスキップして、それ以外のリソースを作成します。

      ■初回実行時(Create)のパラメータ入力例
      ~省略~
      basicAuthCloudFrontDistId4LambdaEdge: #初回時は入力しない(Distribution IDが発行されていないので入力できない)
      ~省略~
      
    6. 2回目実行(Update処理でAmazon CloudFrontのDistribution IDをカスタムリソースに渡し、基本認証用Lambda@Edgeバージョンを作成する。)
      最初の呼出元AWS Cloudformationスタックの実行では「basicAuthCloudFrontDistId4LambdaEdge」に初回実行で発行されたAmazon CloudFrontのDistribution IDを入力することで、基本認証用Lambda@Edgeを作成するAWS Cloudformationスタックの作成処理を実行して、基本認証用Lambda@Edgeのバージョンと呼出元のAmazon CloudFrontを関連付けます。

      ■2回目実行時(Update)のパラメータ入力例
      #2回目実行時(Update)はAmazon CloudFrontのDistribution IDを含め、すべてのパラメータを入力
      ~省略~
      basicAuthCloudFrontDistId4LambdaEdge: XXXXXXXXXXXXX
      ~省略~
      
    7. 結果確認
      正しく実行されていればパラメータで指定したACM証明書を発行する対象ドメインにhttpsでアクセスすると基本認証のダイアログが表示され、パラメータで指定した基本認証のIDとパスワードで認証を通過します。
      (今回の例では「https://cfn-acm-s3-lambdaedge-cloudfront.h-o2k.com/」にアクセスすると「CfnACMS3LambdaEdge4CloudFrontDemo」と表示されます。)
      また、呼出元Cloudformationスタックで作成したAmazon S3バケットを指すオリジンで5xx系のエラーが発生する場合は、カスタムリソースで作成したAmazon S3バケットを指すオリジンにCloudFrontオリジンフェイルオーバーで参照先が切り替わります。
      正しく動作しない場合は呼出元AWS Cloudformationスタックのイベント内容、カスタムリソースでデプロイしたスタックのイベント内容、AWS LambdaカスタムリソースのAmazon CloudWatch Logsの内容、AWS CloudTrailのログから原因を特定して不具合を修正します。

    削除手順

    今回のAWS CloudformationテンプレートではLambda@EdgeとAmazon CloudFrontの関連付け解除とカスタムリソースの削除の順序制御をしていないため、一度にすべてのスタックを削除することができません。
    そのため、削除する場合は次の手順のように関連付けを解除した後、呼出元AWS Cloudformationスタックと基本認証用Lambda@Edgeバージョンを作成したスタックをそれぞれ削除する必要があります。

    1. 呼出元AWS CloudformationスタックのパラメータでAmazon CloudFrontのDistribution IDを空にしてUpdate処理をする
      呼出元AWS Cloudformationスタックのパラメータ「basicAuthCloudFrontDistId4LambdaEdge」を空にしてスタックを更新する。
      us-east-1の基本認証用Lambda@Edgeのスタックの削除はCloudFrontとの関連付けがあるので失敗するが、呼び出し元のスタックではAmazon CloudFrontとLambda@Edgeの関連付けが削除される。
    2. 呼出元AWS Cloudformationスタックを削除する
      呼出元AWS Cloudformationスタックと基本認証用Lambda@Edgeのスタックの関連付けが解除されているため、呼出元AWS Cloudformationスタックが削除できる。
    3. us-east-1で基本認証用Lambda@Edgeバージョンを作成したCloudformationスタックを削除する
      呼出元AWS Cloudformationスタックと基本認証用Lambda@Edgeのスタックの関連付けが解除されているため、基本認証用Lambda@Edgeバージョンを作成したCloudformationスタックを単独で削除する。


    参考:
    What is AWS CloudFormation? - AWS CloudFormation
    Tech Blog with related articles referenced
    AWS CloudFormation Templates and AWS Lambda Custom Resources for Associating AWS Certificate Manager, Lambda@Edge, and AWS WAF with a Website on Amazon S3 and Amazon CloudFront Cross-Region
    Deploy AWS Cloudformation Stack Cross-Region with AWS Lambda Custom Resources

    まとめ

    今回は「AWS LambdaカスタムリソースでAWS Cloudformationスタックを別リージョンにデプロイする」の記事で説明したAWS Cloudformationスタックを別リージョンにデプロイするAWS Lambdaカスタムリソースを使用し、ACM証明書、基本認証用Lambda@Edge、セカンダリS3バケットをそれぞれus-east-1で作成して、呼出元AWS Cloudformationスタックで作成したリソースと関連付ける例を紹介しました。
    次回はAWS Amplify CLIで作成したAmazon S3+Amazon CloudFrontのホスティング環境に今回の内容を適用して、AWS Amplify Consoleのホスティング環境と同じような環境を作成する例を紹介しようと考えています。

    Written by Hidekazu Konishi
    Hidekazu Konishi (小西秀和), a Japan AWS Top Engineer and a Japan All AWS Certifications Engineer

    執筆者小西秀和

    Japan AWS Top Engineer / Japan All AWS Certifications Engineer(AWS認定全冠)として、知識と実践的な経験を活かし、AWSの活用に取り組んでいます。
    Personal Tech Blog | Web Tools Collection | 
    Link Kit | 小西 秀和 - Amazon著者ページ |