AWS LambdaカスタムリソースでCloudFrontオリジンフェイルオーバー用S3バケットを作成するAWS Cloudformationスタックを別リージョンにデプロイする

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

また、そのカスタムリソースの使用例として次の記事を書きました。
* AWS LambdaカスタムリソースでセカンダリS3バケットを作成するAWS Cloudformationスタックを別リージョンにデプロイする
* AWS Lambdaカスタムリソースで基本認証用Lambda@Edgeを作成するAWS Cloudformationスタックを別リージョンにデプロイする


今回はさらにその続編という位置づけでカスタムリソースを使用してAmazon CloudFrontオリジンフェイルオーバー用S3バケットを作成し、マルチリージョンオリジングループを作成する方法を紹介します。

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

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

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

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

  • 呼出元Cloudformationスタックは呼出元とカスタムリソースの双方のリージョンで使用するOriginAccessIdentity(OAI)を作成する
  • 呼出元Cloudformationスタックはカスタムリソースを呼び出し、作成したOriginAccessIdentity(OAI)のS3CanonicalUserId(Amazon S3正規ユーザーID)などのパラメータを渡す
  • カスタムリソースはセカンダリオリジン用のAmazon S3バケットとバケットポリシーをCloudformationスタックでus-east-1に作成してARN、RegionalDomainNameなどを受け取る
  • 呼出元Cloudformationスタックは同リージョンのS3バケットを作成し、カスタムリソースで作成したus-east-1リージョンのS3バケットに対してクロスリージョンレプリケーション設定をする
  • 呼出元Cloudformationスタックは同リージョンのS3バケットとカスタムリソースで作成したus-east-1リージョンのS3バケットでオリジングループを作成し、Amazon CloudFrontに関連付けて作成する

Lambdaカスタムリソースによる別リージョンへのセカンダリS3バケット作成スタックデプロイと関連付けの例
Lambdaカスタムリソースによる別リージョンへのセカンダリS3バケット作成スタックデプロイと関連付けの例

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

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

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

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

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

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

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

#作成するS3バケットのサフィックスに追加する環境名
env: dev
#静的ウェブサイトホスティングに使用するS3バケット名
bucketName: cfn2s3cloudfront-20210822000000-hostingbucket1
#セカンダリS3バケットを作成するスタックをus-east1にデプロイするLambdaカスタムリソースのARN
runCfnInUsEast1LambdaARN: 'arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:CustomResourceToDeployCloudformationStack'
#セカンダリS3バケットを作成するためにus-east1にデプロイするCloudformationスタック名
runCfnInUsEast1StackName4S3BK: CfnS3Secondary4CloudFrontDemo
#セカンダリS3バケットを作成するCloudformationテンプレートを保存しているバケット名
cfnTplS3Bucket4S3BK: h-o2k
#セカンダリS3バケットを作成するCloudformationテンプレートを保存しているバケット以降のオブジェクトキー
cfnTplS3Key4S3BK: CfnS3OtherRegion.yml
#静的ウェブサイトホスティングのバックアップに使用するセカンダリS3バケット名
bucketName4S3BK: cfn2s3cloudfront-20210822000000-hostingbucket2

呼出元AWS Cloudformationテンプレート

ファイル名:OrgSourceOfRunCfnS3OtherRegion.yml

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

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

AWSTemplateFormatVersion: '2010-09-09'
Description: 'Hosting and Secondary Amazon S3 Bucket resource stack creation'
Parameters:
    env: #作成するS3バケットのサフィックスに追加する環境名
        Type: String
    bucketName: #静的ウェブサイトホスティングに使用するS3バケット名
        Type: String
    runCfnInUsEast1LambdaARN: #セカンダリS3バケットを作成するスタックをus-east1にデプロイするLambdaカスタムリソースのARN
        Type: String
    runCfnInUsEast1StackName4S3BK: #セカンダリS3バケットを作成するためにus-east1にデプロイするCloudformationスタック名
        Type: String
    cfnTplS3Bucket4S3BK: #セカンダリS3バケットを作成するCloudformationテンプレートを保存しているバケット名
        Type: String
    cfnTplS3Key4S3BK: #セカンダリS3バケットを作成するCloudformationテンプレートを保存しているバケット以降のオブジェクトキー
        Type: String
    bucketName4S3BK: #静的ウェブサイトホスティングのバックアップに使用するセカンダリS3バケット名
        Type: String
Conditions:
    ShouldNotCreateEnvResources:
        'Fn::Equals':
            -
                Ref: env
            - NONE
Resources:
    S3Bucket:
        Type: 'AWS::S3::Bucket'
        DependsOn:
            #カスタムリソースでセカンダリS3バケットが作成されてからクロスリージョンレプリケーションを設定するためDependsOnにカスタムリソースを追加
            - RunCfnInUsEast1Func4S3BK
        #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'
        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
        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
    #セカンダリ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]}
    CloudFrontDistribution:
        Type: 'AWS::CloudFront::Distribution'
        DependsOn:
            #カスタムリソースでセカンダリS3バケットが作成されてからCloudFrontに関連付けるためDependsOnにカスタムリソースを追加
            - RunCfnInUsEast1Func4S3BK
            - S3Bucket
            - OriginAccessIdentity
        Properties:
            DistributionConfig:
                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
                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

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

前述の呼出元AWS Cloudformationテンプレートのカスタムリソース(RunCfnInUsEast1Func4S3BK)から渡される引数は以下になります。
StackName、cfnTplS3Bucket、cfnTplS3Key、BucketName、S3CanonicalUserId

StackName:セカンダリS3バケットなどリソースを作成するCloudformationスタックの名称
CfnTplS3Bucket:セカンダリS3バケットなどリソースを作成するCloudformationテンプレートがあるS3バケット
CfnTplS3Key:セカンダリS3バケットなどリソースを作成するCloudformationテンプレートがあるS3バケット配下のオブジェクトキー
BucketName:静的ウェブサイトホスティングに使用するセカンダリS3バケット名 S3CanonicalUserId:呼出元Cloudformationスタックで作成されたOriginAccessIdentity(OAI)のS3CanonicalUserId(Amazon S3正規ユーザーID)

これらを設定した上で呼出元AWS Cloudformationテンプレートからカスタムリソースに渡るイベントのフォーマット例を次に示します。

{
  "RequestType": "Create",
  "ResponseURL": "http://pre-signed-S3-url-for-response",
  "StackId": "arn:aws:cloudformation:ap-northeast-1:123456789012:stack/stack-name/guid",
  "RequestId": "unique id for this create request",
  "ResourceType": "Custom::TestResource",
  "LogicalResourceId": "MyTestResource",
  "ResourceProperties": {
    "ServiceToken": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:CustomResourceToDeployCloudformationStack",
    "StackName": "CfnS3Secondary4CloudFrontDemo",
    "CfnTplS3Bucket": "h-o2k",
    "CfnTplS3Key": "CfnS3OtherRegion.yml",
    "BucketName": "cfn2s3cloudfront-20210822000000-hostingbucket2",
    "S3CanonicalUserId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  }
}

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

セカンダリS3バケットを作成するAWS Cloudformationスタックをus-east-1にデプロイするAWS Lambdaカスタムリソース

関数名:CustomResourceToDeployCloudformationStack

カスタムリソースはAWS LambdaカスタムリソースでAWS Cloudformationスタックを別リージョンにデプロイするで作成したものを使用します。
このカスタムリソース内でAmazon S3バケットに保存されているセカンダリS3バケットを作成するCloudformationテンプレートを読み込み、呼出元Cloudformationスタックから渡された引数を使用してセカンダリS3バケットを作成するCloudformationスタックをデプロイします。
実行したスタックが完了し、セカンダリS3バケットが作成されるとそのARNなどを呼出元に返却して呼出元で作成しているAWS CloudFrontに関連付けます。

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

ファイル名:CfnS3OtherRegion.yml

バージョニング設定がされたセカンダリS3バケットを作成し、呼出元Cloudformationで作成されたOAIのS3CanonicalUserIdでS3バケットポリシーを設定するAWS Cloudformationテンプレートは次のようになります。
これをS3バケットに保存しておき、カスタムリソースから読み込んでAWS Cloudformationスタックをus-east-1にデプロイします。

AWSTemplateFormatVersion: '2010-09-09'
Description: 'Stack creation of Secondary Amazon S3 Bucket'
Parameters:
    Env: #作成するS3バケットのサフィックスに追加する環境名
        Type: String
    BucketName: #静的ウェブサイトホスティングに使用するセカンダリS3バケット名
        Type: String
    S3CanonicalUserId: #呼出元Cloudformationスタックで作成されたOAIのS3CanonicalUserId
        Type: String
Conditions:
    ShouldNotCreateEnvResources:
        'Fn::Equals':
            -
                Ref: Env
            - NONE
Resources:
    S3Bucket:
        Type: 'AWS::S3::Bucket'
        #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
    PrivateBucketPolicy:
        Type: 'AWS::S3::BucketPolicy'
        Properties:
            PolicyDocument:
                Id: MyPolicy
                Version: '2012-10-17'
                Statement:
                    - {Sid: APIReadForGetBucketObjects, Effect: Allow, Principal: {CanonicalUser: {Ref: S3CanonicalUserId}}, Action: 's3:GetObject', Resource: {'Fn::Join': ["", ['arn:aws:s3:::', {Ref: S3Bucket}, '/*']]}}
            Bucket:
                Ref: S3Bucket
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'
    DomainName:
        Value:
            'Fn::GetAtt':
                - S3Bucket
                - DomainName
    RegionalDomainName:
        Value:
            'Fn::GetAtt':
                - S3Bucket
                - RegionalDomainName
    Arn:
        Value:
            'Fn::Join':
                - ""
                -
                    - 'arn:aws:s3:::'
                    - {Ref: S3Bucket}

構築手順

  1. AWS Lambda関数「CustomResourceToDeployCloudformationStack」を用意しておく
    セカンダリS3バケットを作成するAWS Cloudformationスタックをus-east-1にデプロイするAWS Lambdaカスタムリソースを予め準備しておく
  2. セカンダリS3バケットを作成するCloudformationテンプレートファイル「CfnS3OtherRegion.yml」をS3バケットに置く
    セカンダリS3バケットを作成するAWS CloudformationテンプレートをAmazon S3バケット(前述のパラメータ例では「h-o2k」)に予め配置しておく
  3. 呼出元Cloudformationテンプレートファイル「OrgSourceOfRunCfnS3OtherRegion.yml 」をAWS Cloudformationで実行する
    呼出元CloudformationテンプレートをAWSマネジメントコンソールなどからパラメータを入力の上でAWS Cloudformationスタックとして実行する
  4. 静的ウェブサイトホスティングに使用するS3バケットにコンテンツを追加する
    静的ウェブサイトホスティング用S3バケット(前述のパラメータ例では「cfn2s3cloudfront-20210822000000-hostingbucket1-dev」)にindex.htmlなどコンテンツを追加する。
    (今回の例では「CfnS3Secondary4CloudFrontDemo」とだけ表示されるindex.htmlを追加しました。)
    呼出元Cloudformationスタックで作成したAmazon S3バケットに追加したコンテンツはカスタムリソースで作成したAmazon S3バケットにクロスリージョンレプリケーションされます。
  5. 結果確認
    正しく実行されていればAmazon CloudFrontのURLにアクセスするとindex.htmlが表示されます。
    また、呼出元Cloudformationスタックで作成したAmazon S3バケットを指すオリジンで5xx系のエラーが発生する場合は、カスタムリソースで作成したAmazon S3バケットを指すオリジンにCloudFrontオリジンフェイルオーバーで参照先が切り替わります。
    正しく動作しない場合は呼出元AWS Cloudformationスタックのイベント内容、カスタムリソースでデプロイしたスタックのイベント内容、AWS LambdaカスタムリソースのAmazon CloudWatch Logsの内容、AWS CloudTrailのログから原因を特定して不具合を修正します。

まとめ

今回は「AWS LambdaカスタムリソースでAWS Cloudformationスタックを別リージョンにデプロイする」の記事で説明したAWS Cloudformationスタックを別リージョンにデプロイするAWS Lambdaカスタムリソースを使用し、セカンダリS3バケットをus-east-1で作成後、呼出元AWS Cloudformationスタックで作成したAmazon S3とクロスリージョンレプリケーションを設定してAmazon CloudFrontにオリジングループとして関連付ける例を紹介しました。
次回はこれまでのカスタムリソースを使用したACM証明書の発行、Lambda@Edgeによる基本認証、マルチリージョンS3バケットによるオリジンフェイルオーバーをまとめてAmazon CloudFrontに関連付ける例を紹介しようと考えています。

小西秀和

執筆者小西秀和

ALL AWS Certifications Engineer(AWS認定全取得)の知識をベースにAWSクラウドの活用に取り組んでいます。