NRIネットコム Blog

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

AWS Amplify CLIとAWS CloudformationでAmplify Console Hostingと同じ機能の再現を試みる - AWS CloudFormationによるAWS Amplify CLIの拡張

小西秀和です。
こちらの記事は以前の記事「AWSの静的ウェブサイトホスティングで入門するAWS Amplify(Console、CLI) - 構築編(Amplify CLI)」の続編という位置づけで、次の記事で作成したAWS CloudformationテンプレートでAWS Amplify CLIのAmazon S3+Amazon CloudFrontのホスティング環境を拡張しようというものです。

AWSの静的ウェブサイトホスティングで入門するAWS Amplify(Console、CLI) - 概要編」にも書きましたが、AWS Amplify CLIはバックエンドの作成時にAWS Cloudformationテンプレートを自動生成してCloudformationスタックとしてデプロイしています。
そのため、AWS Amplify CLIで自動生成されたAWS Cloudformationテンプレートを修正することで機能を追加することができます。
今回、追加しようとしている機能は次の3つになります。

  1. AWS Certificate Manager(ACM)によるSSL証明書
  2. Lambda@Edgeによる基本認証
  3. セカンダリAmazon S3バケットの作成とクロスリージョンのCloudFrontオリジンフェイルオーバー構成

このうち、1.と2.は「AWSの静的ウェブサイトホスティングで入門するAWS Amplify(Console、CLI) - 構築編(Amplify Console)」で紹介したAWS Amplify ConsoleのManaged Hostingにもある機能です。
つまり、Amplify Consoleのホスティングと同じような機能+αをAmplify CLIとCloudformationでやってみようということです。

この記事の本題は以前の記事「AWSの静的ウェブサイトホスティングで入門するAWS Amplify(Console、CLI) - 構築編(Amplify CLI)」の続きから入りますので、必要であればこちらの記事も参照してください。

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

Amplify CLI+Cloudformationでデプロイする環境の構成図

今回、Amplify CLIのCloudformationテンプレート修正で構築しようとしている構成は次のようになります。

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

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

関数名:CustomResourceToDeployCloudformationStack

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

Amplify CLIで生成されたパラメータファイルの修正例

前述のCloudformationテンプレートに必要なパラメータを渡してスタックをデプロイするのですが、Amplify CLIの場合はパラメータは次のパスにあるファイルに記述されています。
今回の修正にあわせてこちらのパラメータについても必要なものを追加します。

<Amplify CLIプロジェクトのディレクトリパス>/amplify/backend/hosting/S3AndCloudFront/parameters.json

■各パラメータの説明

bucketName: #【共通】静的ウェブサイトホスティングに使用するS3バケット名
runCfnInUsEast1LambdaARN: #【共通】セカンダリS3バケットを作成するスタックをus-east1にデプロイするLambdaカスタムリソースのARN
runCfnInUsEast1StackName4ACM: #【ACM証明書用】ACM証明書を作成するためにus-east1にデプロイするCloudformationスタック名
cfnTplS3Bucket4ACM: #【ACM証明書用】ACM証明書を作成するCloudformationテンプレートを保存しているバケット名
cfnTplS3Key4ACM: #【ACM証明書用】ACM証明書を作成するCloudformationテンプレートを保存しているバケット以降のオブジェクトキー
customDomainName4ACM: #【ACM証明書用】ACM証明書を発行する対象ドメイン名
hostedZoneId4ACM: #【ACM証明書用】ACM証明書を発行する対象ドメイン名を管理するRoute53のホストゾーンID
runCfnInUsEast1StackName4S3BK: #【セカンダリS3用】セカンダリS3バケットを作成するためにus-east1にデプロイするCloudformationスタック名
cfnTplS3Bucket4S3BK: #【セカンダリS3用】セカンダリS3バケットを作成するCloudformationテンプレートを保存しているバケット名
cfnTplS3Key4S3BK: #【セカンダリS3用】セカンダリS3バケットを作成するCloudformationテンプレートを保存しているバケット以降のオブジェクトキー
bucketName4S3BK: #【セカンダリS3用】静的ウェブサイトホスティングのバックアップに使用するセカンダリS3バケット名
runCfnInUsEast1StackName4LambdaEdge: #【Lambda@Edge用】Lambda@Edgeを作成するためにus-east1にデプロイするCloudformationスタック名
cfnTplS3Bucket4LambdaEdge: #【Lambda@Edge用】Lambda@Edgeを作成するCloudformationテンプレートを保存しているバケット名
cfnTplS3Key4LambdaEdge: #【Lambda@Edge用】Lambda@Edgeを作成するCloudformationテンプレートを保存しているバケット以降のオブジェクトキー

basicAuthFuncName4LambdaEdge: #【Lambda@Edge用】基本認証をするLambda@Edgeの関数名

basicAuthCloudFrontDistId4LambdaEdge: #【Lambda@Edge用】基本認証をするLambda@Edgeを関連付けるAmazon CloudFrontのDistribution ID。AWS Secrets Managerシークレットの一意識別で使用する。※スタックの初回Create処理でAmazon CloudFrontを作成してから、2回目のUpdate処理で入力する
basicAuthID4LambdaEdge: #【Lambda@Edge用】基本認証をするLambda@Edgeで認証するID。AWS Secrets Managerシークレットとして保管する。
basicAuthPW4LambdaEdge: #【Lambda@Edge用】基本認証をするLambda@Edgeで認証するパスワード。AWS Secrets Managerシークレットとして保管する。

■パラメータファイル修正例

{
    "bucketName": "amplifyclidemo-20210717003719-hostingbucket", 
    "runCfnInUsEast1LambdaARN": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:CustomResourceToDeployCloudformationStack", 
    "runCfnInUsEast1StackName4ACM": "CfnAcm4CloudFrontOfAmplify", 
    "cfnTplS3Bucket4ACM": "h-o2k", 
    "cfnTplS3Key4ACM": "CfnACMCertificate.yml", 
    "customDomainName4ACM": "amplify-cli-with-cfn.h-o2k.com", 
    "hostedZoneId4ACM": "XXXXXXXXXXXXXXXXXXXXX", 
    "runCfnInUsEast1StackName4S3BK": "CfnS3Secondary4CloudFrontOfAmplify", 
    "cfnTplS3Bucket4S3BK": "h-o2k", 
    "cfnTplS3Key4S3BK": "CfnS3OtherRegion.yml", 
    "bucketName4S3BK": "amplifyclidemo-20210717003719-hostingbucket2", 
    "runCfnInUsEast1StackName4LambdaEdge": "CfnLambdaEdge4CloudFrontOfAmplify", 
    "cfnTplS3Bucket4LambdaEdge": "h-o2k", 
    "cfnTplS3Key4LambdaEdge": "CfnBasicAuthLambdaEdge.yml", 
    "basicAuthFuncName4LambdaEdge": "BasicAuthLambdaEdgeOfAmplify", 
    "basicAuthCloudFrontDistId4LambdaEdge": "XXXXXXXXXXXXX", 
    "basicAuthID4LambdaEdge": "Iam", 
    "basicAuthPW4LambdaEdge": "Nobody"
}

Amplify CLIで生成されたCloudformationテンプレートの修正例

Amplify CLIで作成したAmazon S3+Amazon CloudFrontのCloudformationテンプレートはJSON形式で次のパスに自動生成されます。
<Amplify CLIプロジェクトのディレクトリパス>/amplify/backend/hosting/S3AndCloudFront/template.json
JSONファイルはコメントを追加することができないため、各追加機能の詳細は同様のCloudformationテンプレートをYAML形式で使用している「AWS LambdaカスタムリソースでSSL証明書・基本認証・CloudFrontオリジンフェイルオーバーを作成するAWS Cloudformationスタックを別リージョンにデプロイする」の記事を参照してください。

{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Description": "Hosting resource stack creation using Amplify CLI",
    "Parameters": {
        "env": {
            "Type": "String"
        },
        "bucketName": {
            "Type": "String"
        },
        "runCfnInUsEast1LambdaARN": {
            "Type": "String"
        },
        "runCfnInUsEast1StackName4ACM": {
            "Type": "String"
        },
        "cfnTplS3Bucket4ACM": {
            "Type": "String"
        },
        "cfnTplS3Key4ACM": {
            "Type": "String"
        },
        "customDomainName4ACM": {
            "Type": "String"
        },
        "hostedZoneId4ACM": {
            "Type": "String"
        },
        "runCfnInUsEast1StackName4S3BK": {
            "Type": "String"
        },
        "cfnTplS3Bucket4S3BK": {
            "Type": "String"
        },
        "cfnTplS3Key4S3BK": {
            "Type": "String"
        },
        "bucketName4S3BK": {
            "Type": "String"
        },
        "runCfnInUsEast1StackName4LambdaEdge": {
            "Type": "String"
        },
        "cfnTplS3Bucket4LambdaEdge": {
            "Type": "String"
        },
        "cfnTplS3Key4LambdaEdge": {
            "Type": "String"
        },
        "basicAuthFuncName4LambdaEdge": {
            "Type": "String"
        },
        "basicAuthCloudFrontDistId4LambdaEdge": {
            "Type": "String"
        },
        "basicAuthID4LambdaEdge": {
            "Type": "String"
        },
        "basicAuthPW4LambdaEdge": {
            "Type": "String"
        }
    },
    "Conditions": {
        "ShouldNotCreateEnvResources": {
            "Fn::Equals": [
                {
                    "Ref": "env"
                },
                "NONE"
            ]
        },
        "ShouldCreateBasicAuthLambdaEdge": {
            "Fn::Not": [
                {
                    "Fn::Equals": [
                        {
                            "Ref": "basicAuthCloudFrontDistId4LambdaEdge"
                        },
                        ""
                    ]
                }
            ]
        }
    },
    "Resources": {
        "S3Bucket": {
            "Type": "AWS::S3::Bucket",
            "DeletionPolicy": "Retain",
            "DependsOn": [
                "RunCfnInUsEast1Func4S3BK",
                "BucketBackupRole"
            ],
            "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": {
                    "Role": {
                        "Fn::GetAtt": [
                            "BucketBackupRole",
                            "Arn"
                        ]
                    },
                    "Rules": [
                        {
                            "Destination": {
                                "Bucket": {
                                    "Fn::GetAtt": [
                                        "RunCfnInUsEast1Func4S3BK",
                                        "Arn"
                                    ]
                                },
                                "StorageClass": "STANDARD"
                            },
                            "Id": "ReplicationRule",
                            "Prefix": "",
                            "Status": "Enabled"
                        }
                    ]
                }
            }
        },
        "BucketBackupRole": {
            "Type": "AWS::IAM::Role",
            "Properties": {
                "AssumeRolePolicyDocument": {
                    "Statement": [
                        {
                            "Action": [
                                "sts:AssumeRole"
                            ],
                            "Effect": "Allow",
                            "Principal": {
                                "Service": [
                                    "s3.amazonaws.com"
                                ]
                            }
                        }
                    ]
                }
            }
        },
        "BucketBackupPolicy": {
            "Type": "AWS::IAM::Policy",
            "DependsOn": [
                "RunCfnInUsEast1Func4S3BK",
                "S3Bucket"
            ],
            "Properties": {
                "PolicyDocument": {
                    "Statement": [
                        {
                            "Action": [
                                "s3:GetReplicationConfiguration",
                                "s3:ListBucket"
                            ],
                            "Effect": "Allow",
                            "Resource": {
                                "Fn::Join": [
                                    "",
                                    [
                                        "arn:aws:s3:::",
                                        {
                                            "Ref": "S3Bucket"
                                        }
                                    ]
                                ]
                            }
                        },
                        {
                            "Action": [
                                "s3:GetObjectVersion",
                                "s3:GetObjectVersionAcl"
                            ],
                            "Effect": "Allow",
                            "Resource": {
                                "Fn::Join": [
                                    "",
                                    [
                                        "arn:aws:s3:::",
                                        {
                                            "Ref": "S3Bucket"
                                        },
                                        "/*"
                                    ]
                                ]
                            }
                        },
                        {
                            "Action": [
                                "s3:ReplicateObject",
                                "s3:ReplicateDelete"
                            ],
                            "Effect": "Allow",
                            "Resource": {
                                "Fn::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"
                }
            }
        },
        "RunCfnInUsEast1Func4ACM": {
            "Type": "Custom::RunCfnInUsEast1Func4ACM",
            "Properties": {
                "ServiceToken": {
                    "Ref": "runCfnInUsEast1LambdaARN"
                },
                "StackName": {
                    "Ref": "runCfnInUsEast1StackName4ACM"
                },
                "CfnTplS3Bucket": {
                    "Ref": "cfnTplS3Bucket4ACM"
                },
                "CfnTplS3Key": {
                    "Ref": "cfnTplS3Key4ACM"
                },
                "CustomDomainName": {
                    "Ref": "customDomainName4ACM"
                },
                "HostedZoneId": {
                    "Ref": "hostedZoneId4ACM"
                }
            }
        },
        "Route53RecordSetGroup": {
            "Type": "AWS::Route53::RecordSetGroup",
            "DependsOn": [
                "CloudFrontDistribution"
            ],
            "Properties": {
                "HostedZoneId": {
                    "Ref": "hostedZoneId4ACM"
                },
                "RecordSets": [
                    {
                        "Name": {
                            "Ref": "customDomainName4ACM"
                        },
                        "Type": "A",
                        "AliasTarget": {
                            "HostedZoneId": "Z2FDTNDATAQYW2",
                            "DNSName": {
                                "Fn::GetAtt": [
                                    "CloudFrontDistribution",
                                    "DomainName"
                                ]
                            }
                        }
                    }
                ]
            }
        },
        "RunCfnInUsEast1Func4S3BK": {
            "DependsOn": [
                "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"
                    ]
                }
            }
        },
        "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": {
                    "Fn::If": [
                        "ShouldCreateBasicAuthLambdaEdge",
                        "false",
                        "true"
                    ]
                }
            }
        },
        "CloudFrontDistribution": {
            "Type": "AWS::CloudFront::Distribution",
            "DependsOn": [
                "OriginAccessIdentity",
                "RunCfnInUsEast1Func4S3BK",
                "RunCfnInUsEast1Func4ACM",
                "S3Bucket",
                "RunCfnInUsEast1Func4LambdaEdge"
            ],
            "Properties": {
                "DistributionConfig": {
                    "Aliases": [
                        {
                            "Ref": "customDomainName4ACM"
                        }
                    ],
                    "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"
                                            }
                                        ]
                                    ]
                                }
                            }
                        },
                        {
                            "DomainName": {
                                "Fn::GetAtt": [
                                    "RunCfnInUsEast1Func4S3BK",
                                    "RegionalDomainName"
                                ]
                            },
                            "Id": "hostingS3BucketSecondary",
                            "S3OriginConfig": {
                                "OriginAccessIdentity": {
                                    "Fn::Join": [
                                        "",
                                        [
                                            "origin-access-identity/cloudfront/",
                                            {
                                                "Ref": "OriginAccessIdentity"
                                            }
                                        ]
                                    ]
                                }
                            }
                        }
                    ],
                    "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"
                ]
            }
        }
    }
}

実行手順

  1. AWS Amplify CLIで次の記事のようにAmazon S3+Amazon CloudFrontの静的ウェブサイトホスティング環境を構築する
    AWSの静的ウェブサイトホスティングで入門するAWS Amplify(Console、CLI) - 構築編(Amplify CLI)
  2. 次のJSON形式のCloudformationテンプレートファイルを前述したように修正する
    <Amplify CLIプロジェクトのディレクトリパス>/amplify/backend/hosting/S3AndCloudFront/template.json
  3. 次のJSON形式のパラメータファイルを前述したように修正する
    <Amplify CLIプロジェクトのディレクトリパス>/amplify/backend/hosting/S3AndCloudFront/parameters.json
  4. 次のAWS Amplify CLIコマンドで修正した内容を反映させる amplify publish

確認手順

  1. パラメータファイルで指定したドメイン名にHTTPSでアクセスする
    (今回の例では「https://amplify-cli-with-cfn.h-o2k.com/」にアクセスすると「AWS Amplify CLI Hosting S3+CloudFront Demo」と表示されます。)
    ⇒ ドメインに有効な証明書が適用されていること、基本認証ダイアログが表示されることが確認できる
  2. 基本認証ダイアログにパラメータファイルで指定した基本認証用のID、パスワードを入力して認証する
    ⇒ 認証後、デモ用のページが表示されることが確認できる
  3. Amazon S3のAWSマネジメントコンソールでパラメータファイルに指定したAmazon S3バケットを検索する
    ⇒ 2つのAmazon S3バケットが<指定したAmazon S3バケット名>+<envの環境名>で作成されていることが確認できる
  4. Amazon CloudFrontのAWSマネジメントコンソールでAmplify CLI実行後に表示されるCloudFrontのドメイン名(例:xxxxxxxxxxxxx.cloudfront.net)を検索・参照し、「オリジン」タブで詳細を確認する
    ⇒ 2つのオリジンとそれらで構成されたオリジングループが作成されていることが確認できる

削除手順

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

  1. 呼出元AWS CloudformationスタックのパラメータでAmazon CloudFrontのDistribution IDを空にしてUpdate処理をする
    呼出元AWS Cloudformationスタックのパラメータ「basicAuthCloudFrontDistId4LambdaEdge」を空にしてamplify publishを実行する。
    us-east-1の基本認証用Lambda@Edgeのスタックの削除はCloudFrontとの関連付けがあるので失敗するが、呼び出し元のスタックではAmazon CloudFrontとLambda@Edgeの関連付けが削除される。
  2. 呼出元AWS Cloudformationスタックを削除する
    呼出元AWS Cloudformationスタックと基本認証用Lambda@Edgeのスタックの関連付けが解除されているため、呼出元AWS Cloudformationスタックが削除できる。
    Amplifyプロジェクトからホスティング環境を削除する場合はamplify remove hostingを、Amplifyプロジェクトごと削除する場合はamplify deleteを実行する。
  3. us-east-1で基本認証用Lambda@Edgeバージョンを作成したCloudformationスタックを削除する
    呼出元AWS Cloudformationスタックと基本認証用Lambda@Edgeのスタックの関連付けが解除されているため、基本認証用Lambda@Edgeバージョンを作成したCloudformationスタックを単独で削除する。
    AWSマネジメントコンソールでus-east-1リージョンのAWS Cloudformation画面に遷移し、「runCfnInUsEast1StackName4LambdaEdge」で指定したスタック名と一致するスタックを削除する。


参考:
Amplify Documentation - AWS Amplify Documentation
What is AWS CloudFormation? - AWS CloudFormation
Tech Blog with related articles referenced

まとめ

今回はAWS Amplify CLIのAWS Cloudformationテンプレートを修正し、AWS LambdaカスタムリソースでSSL証明書・基本認証・CloudFrontオリジンフェイルオーバーを作成して関連付けることでAWS Amplify ConsoleのManaged Hostingと同様の構成構築を試みました。
このようにAWS Cloudformationテンプレートを修正することでリソースに対して細かい設定ができることがAWS Amplify CLIのAWS Amplify Consoleと比較した場合の利点と言えます。
例えば、次のようなAWS CloudFormationベースで要件に必要な機能を追加するケースが挙げられます。

  • Amazon CloudFrontオリジンフェイルオーバーを設定する(今回の構築例でも紹介)
  • Amazon Route 53へAliasレコードとしてAmazon CloudFrontディストリビューションを登録する(今回の構築例でも紹介)
  • Amazon CloudFrontにキャッシュ設定や地域制限などのパラメータを設定する
  • 基本認証以外を目的とするLambda@EdgeやCloudFront Functionsを追加する
  • Amazon CloudFrontにAWS WAFを適用してIP制限など詳細なルールを適用する

ただ、AWS Cloudformationはリソースの細かい設定をコード化できる反面、AWS Amplify Consoleの手順に比べるとかなりの作業量です。
そのため、Amazon S3+Amazon CloudFrontに対する詳細設定や追加機能など特に詳細な要件がなく、AWS Amplify ConsoleのManaged Hostingで要件が満たされる場合はそちらを利用する方が良いでしょう。
その判断をするためにもAWS Amplify Consoleを一度使ってみることをおすすめします。AWS Amplify Consoleの簡単な使い方は次の記事にまとめています。

逆に作成したリソースに対して詳細設定や追加機能が多すぎる場合には一般的なアーキテクチャを迅速にデプロイするAWS Amplify CLIの利点があまり活かされません。
そのため、このような場合には今後の記事で紹介するAWS Cloud Development Kit(AWS CDK)を使う
ことをおすすめします。

今まで、書いてきた静的ウェブサイトホスティングをテーマにAWS AmplifyとAWS Cloudformationについて記事を書いてきましたが、それらの内容は関連しているため、参考までにリンクをまとめておきます。

<静的ウェブサイトホスティングで入門するAWS Amplifyシリーズ>

<LambdaカスタムリソースでCloudformationスタックをデプロイするシリーズ>

このように、これまでは静的ウェブサイトホスティングをテーマにAWS AmplifyとAWS Cloudformationについて書いてきました。
次回からはプログラミング言語でクラウドインフラストラクチャを定義し、それをAWS CloudFormationに変換してデプロイするフレームワークであるAWS Cloud Development Kit(AWS CDK)について書いていきます。
AWS CDKの導入によって、静的ウェブサイトホスティングのコード化がAWS AmplifyやAWS Cloudformationとどう変わるか見ていきたいと思います。

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

執筆者小西秀和

Japan AWS All Certifications Engineer(AWS認定全冠)の知識をベースにAWSクラウドの活用に取り組んでいます。
Amazon.co.jp: 小西 秀和: books, biography, latest update
NRIネットコムBlog: 小西 秀和: 記事一覧