小西秀和です。
この記事は過去に投稿した次の記事の続編で、SSL/TLS証明書(AWS Certificate Manager)、基本認証(Lambda@Edge)に加えてIP制限(AWS WAF)を追加したパターンでAmazon S3とAmazon CloudFrontによる静的ウェブサイトホスティングをAWS CloudFormationテンプレートとAWS Lambdaカスタムリソースを使用して構成するものです。
今回は更にOrigin Access Identity(OAI)をOrigin Access Control (OAC)に変更し、キャッシュポリシー(CachePolicy)、オリジンリクエストポリシー(OriginRequestPolicy)、レスポンスヘッダーポリシー(ResponseHeaderPolicy)をマネージドポリシーから選択式で設定できるようにしています。
ここではAWSマネジメントコンソールでの操作とAWS WAFを柔軟に設定することを重視し、AWS AmplifyやAWS CDKはあえて使用していません。
AWSマネジメントコンソールから特定のリージョンにAmazon AWS CloudFormationをデプロイし、us-east-1で作成が必要なAWSリソースをAWS Lambdaカスタムリソースを使用してクロスリージョンで作成します。
AWS Amplify、AWS CDKを使用した静的ウェブサイトホスティングについては以下の記事に基本的な内容を書いていますので、そちらをご参考に御覧ください。
* AWSの静的ウェブサイトホスティングで入門するAWS Amplify(Console、CLI) - 概要編
* AWSの静的ウェブサイトホスティングで入門するAWS Amplify(Console、CLI) - 構築編(Amplify Console)
* AWSの静的ウェブサイトホスティングで入門するAWS Amplify(Console、CLI) - 構築編(Amplify CLI)
* AWS Amplify CLIとAWS CloudFormationでAmplify Console Hostingと同じ機能の再現を試みる - AWS CloudFormationによるAWS Amplify CLIの拡張
* AWS CDKで別リージョンにSSL証明書・基本認証・レプリケーション用S3バケットを作成するスタックをデプロイしてAmazon CloudFrontオリジンフェイルオーバーを設定する
※本記事および当執筆者のその他の記事で掲載されているソースコードは自主研究活動の一貫として作成したものであり、動作を保証するものではありません。使用する場合は自己責任でお願い致します。また、予告なく修正することもありますのでご了承ください。
今回の記事の内容は次のような構成になっています。
本記事で試す構成の概要図
今回は次の手順で概要図の構成をAWS CloudFormationとカスタムリソースを使用して作成することを試しています。
初回作成時と2回目更新時で処理を分けている理由はAmazon CloudFrontのDistribution IDがAmazon CloudFront作成後にしか取得できず、Distribution IDが必要なAWSリソースを作成した後に再び関連付けのためにAmazon CloudFrontを更新する必要があるからです。
【初回作成時】
- 呼出元AWS CloudFormationスタックをパラメータにAmazon CloudFrontのDistribution IDを入力せずに作成する
- 呼出元AWS CloudFormationスタックはデプロイしたリージョンでOriginAccessControl(OAC)、Amazon S3バケット、Amazon CloudFrontディストリビューションを作成する
- 呼出元AWS CloudFormationスタックはカスタムリソースを使用して次の内容をus-east-1へデプロイし、作成した結果を呼出元AWS CloudFormationスタックに返却する
- AWS Certificate Manager(ACM)証明書
- 呼出元AWS CloudFormationスタックは呼出元リージョンのリソースを作成し、カスタムリソースからの返却値を使用してus-east-1リージョンの「AWS Certificate Manager(ACM)証明書」をAmazon CloudFrontに関連付ける
【2回目更新時】
- 呼出元AWS CloudFormationスタックをパラメータにAmazon CloudFrontのDistribution IDを入力して更新する
- 呼出元AWS CloudFormationスタックは次の内容をus-east-1にデプロイするカスタムリソースを呼び出し、作成した結果を呼出元AWS CloudFormationスタックに返却する
- IP制限をするAWS WAF Web ACL
- 基本認証をするAWS Lambda@Edgeとそのバージョン
- 呼出元AWS CloudFormationスタックは呼出元リージョンのリソースを更新してOriginAccessControl(OAC)を関連付け、カスタムリソースからの返却値を使用してus-east-1リージョンの「基本認証をするAWS Lambda@Edgeとそのバージョン」、「IP制限をするAWS WAF Web ACL」をAmazon CloudFrontに関連付ける
テンプレートの名称設定
AWS CloudFormationテンプレートに定義するAWSリソースは一意に必要なものを識別して連携できれば各ファイルの名称は任意ですが、説明のために次のように各ファイルの名称を設定します。
- 任意のリージョンへAmazon S3とAmazon CloudFrontによる静的ウェブサイトを作成し、カスタムリソースを使用してSSL/TLS証明書(AWS Certificate Manager)・基本認証(Lambda@Edge)・IP制限(AWS WAF)をus-east-1リージョンにデプロイして関連付けるAWS CloudFormationテンプレート(呼び出し元)
⇒WebHostCFnS3CloudFrontWithAcmLambdaEdgeWaf.yml
- 呼び出し元テンプレートから呼び出され、指定したAmazon S3にあるAWS CloudFormationテンプレートを使用してリソースをus-east-1リージョンへデプロイするカスタムリソースを作成するAWS CloudFormationテンプレート(カスタムリソース作成用)
⇒WebHostCFnCustomResourceToDeployAWS CloudFormationStack.yml
- カスタムリソースからデプロイし、us-east-1リージョンへSSL/TLS証明書用AWS Certificate Managerを作成するAWS CloudFormationテンプレート(SSL/TLS証明書用AWS Certificate Manager)
⇒WebHostWebHostCFnACMCertificate.yml
- カスタムリソースからデプロイし、us-east-1リージョンへ基本認証用Lambda@Edgeを作成するAWS CloudFormationテンプレート(基本認証用Lambda@Edge)
⇒WebHostWebHostCFnBasicAuthLambdaEdge.yml
- カスタムリソースからデプロイし、us-east-1リージョンへIP制限用AWS WAFを作成するAWS CloudFormationテンプレート(IP制限用AWS WAF)
⇒WebHostCFnWAFWebACL.yml
各ファイルやパラメータの例
各ファイルやパラメータの例を記載していきます。
特に入力パラメータに関しては入力形式を知っていただくための例なので、使用する場合は各パラメータを要件に合わせて設定する必要があります。
AWS CloudFormationテンプレート(カスタムリソース作成用)
参考記事:AWS LambdaカスタムリソースでAWS CloudFormationスタックを別リージョンにデプロイする
テンプレート本体
ファイル名:WebHostCFnCustomResourceToDeployCloudformationStack.yml
AWSTemplateFormatVersion: '2010-09-09' Description: 'CFn Template for a stack that creates AWS Lambda Custom Resource which deploys AWS CloudFormation stack.' Resources: LambdaCustomResourceToDeployCFnStack: Type: AWS::Lambda::Function DependsOn: - LambdaCustomResourceToDeployCFnStackRole Properties: FunctionName: LambdaCustomResourceToDeployCFnStack Description : 'AWS Lambda Custom Resource which deploys AWS CloudFormation stack.' Runtime: python3.9 MemorySize: 10240 Timeout: 900 Role: !GetAtt LambdaCustomResourceToDeployCFnStackRole.Arn Handler: index.lambda_handler Code: ZipFile: | # ## このカスタムリソースの特徴 # * カスタムリソース仕様のレスポンスを返すメソッドを自前で作成した # CloudFormationスタックから呼び出す場合はcfnresponse.sendが使用できますが、Lambda単体での実行やテストには使用できないためcfn_response_sendという自前のメソッドを作成しました。 # 参考:https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-lambda-function-code-cfnresponsemodule.html # * デプロイするテンプレートをS3またはハードコーディングから選択できるようにした # S3からテンプレートを取得する方法では様々なテンプレートが実行できます。一方で特定のテンプレート実行に特化させたい場合はハードコーディングを選択することで他のテンプレートを実行させないようにできます。 # * カスタムリソースからデプロイするCloudFormationテンプレートの引数の指定を簡素化した # デプロイするCloudformationテンプレートの引数として使用しないキーを指定して、それ以外を渡すことで引数の受け渡しを汎用的にしました。 # * 要求された処理と異なる状況の場合でもリソースが作成される方針に寄せた # 要求されたCreate、Update処理をそのままCloudFormationのデプロイに使用するのではなく、Update処理でスタックが存在しない場合にはCreate処理に切り替えるなど、なるべくリソースが作成される方針に寄せました。 # * 処理のスキップを選択できるようにした # 呼出元で依存関係を維持しながらCreate処理では使用せず、Update処理から使用するといったことができるようにスキップ処理が選択できるようにしました。 import urllib3 import json import boto3 import botocore import datetime # import cfnresponse SUCCESS = 'SUCCESS' FAILED = 'FAILED' http = urllib3.PoolManager() # 呼出元のCloudformationスタックから受けた引数のうち、デプロイするCloudformationスタックの引数として使用しないキー config = { "ExcludeKeys": ["ServiceToken", "StackName", "CfnTplS3Bucket", "CfnTplS3Key", "IsSkip"] } # デプロイするCloudformationテンプレートを直接コードに記述したい場合はここに記述 fixed_cfn_template = ''' ''' # us-east-1リージョンにCloudformationスタックをデプロイするクライアント cfn_client = boto3.client('cloudformation', region_name='us-east-1') # Cloudformationテンプレートを取得するS3リソース s3_resource = boto3.resource('s3') # カスタムリソースの仕様でCloudFormationにレスポンスを返す独自メソッド # CloudFormationスタックから呼び出す場合はcfnresponse.sendが使用できるが、Lambda単体での実行には使用できないため独自メソッドを使用する def cfn_response_send(event, context, response_status, response_data, physical_resource_id=None, no_echo=False, reason=None): response_url = event['ResponseURL'] print(response_url) response_body = { 'Status': response_status, 'Reason': reason or 'See the details in CloudWatch Log Stream: {}'.format(context.log_stream_name), 'PhysicalResourceId': physical_resource_id or context.log_stream_name, 'StackId': event['StackId'], 'RequestId': event['RequestId'], 'LogicalResourceId': event['LogicalResourceId'], 'NoEcho': no_echo, 'Data': response_data } json_response_body = json.dumps(response_body) print('Response body:') print(json_response_body) headers = { 'content-type': '', 'content-length': str(len(json_response_body)) } try: response = http.request('PUT', response_url, headers=headers, body=json_response_body) print('Status code:', response.status) except Exception as e: print('cfn_response_send(..) failed executing http.request(..):', e) def lambda_handler(event, context): response_data = {} try: print(('event:' + json.dumps(event, indent=2))) resource_properties = event['ResourceProperties'] is_skip = resource_properties.get('IsSkip') # IsSkipにtrueが入ってきた場合は何も処理せず空のレスポンスを返す # 呼出元でカスタムリソース処理をスキップする場合に使用する if is_skip is not None and is_skip.strip().lower() == 'true': print('Skip All Process.') cfn_response_send(event, context, SUCCESS, response_data) # cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data) return stack_name = resource_properties.get('StackName') exclude_keys = config['ExcludeKeys'] cfn_params = [] for key, val in resource_properties.items(): if key not in exclude_keys: cfn_params.append({ 'ParameterKey': key, 'ParameterValue': val }) print('----------[Start]cfn_params:----------') print(cfn_params) print('----------[End]cfn_params:------------') cfn_template = fixed_cfn_template try: # fixed_cfn_template変数に直接テンプレートを記述していない場合は指定されたS3バケットのオブジェクトからテンプレートを取得する if cfn_template.strip() == '': cfn_tpl_s3_bucket = resource_properties['CfnTplS3Bucket'] cfn_tpl_s3_key = resource_properties['CfnTplS3Key'] bucket = s3_resource.Bucket(cfn_tpl_s3_bucket) obj = bucket.Object(cfn_tpl_s3_key).get() cfn_template = obj['Body'].read().decode('utf-8') print('----------[Start]cfn_template:----------') print(cfn_template) print('----------[End]cfn_template:------------') except Exception as e: print('lambda_handler(..) failed executing read s3 obj(..):', e) raise if event['RequestType'] == 'Delete': try: print('Delete Stacks') # Delete処理の場合は単純に削除する print('Run cfn_client.delete_stack.') response = cfn_client.delete_stack( StackName=stack_name ) print('cfn_client.delete_stack response:') print(response) # スタックのDelete処理が完了するまで待つ waiter = cfn_client.get_waiter('stack_delete_complete') waiter.wait(StackName=stack_name) except (botocore.exceptions.ValidationError, botocore.exceptions.ClientError) as err: err_msg = str(err) print('err_msg:') print(err_msg) #相互に関連付けしたリソース同士で削除エラーのループが発生するため、例外は上げない。 if event['RequestType'] == 'Create' or event['RequestType'] == 'Update': # Create処理とUpdate処理の実行方針 # Create処理要求かつスタックが存在しない ⇒ Create処理 # Create処理要求かつスタックが存在する ⇒ 何もしない # Update処理要求かつスタックが存在しない ⇒ Create処理 # Update処理要求かつスタックが存在するかつ変更点が存在する ⇒ Update処理 # Update処理要求かつスタックが存在するかつ変更点が存在しない ⇒ 何もしない # 同名のスタックが存在するかを示すフラグ exist_stack = False try: print('Run cfn_client.describe_stacks.') existing_stacks = cfn_client.describe_stacks(StackName=stack_name) print('Existing Stacks:') print(existing_stacks) # スタックが存在し、describe_stacksが正常に処理されればexist_stackはTrue exist_stack = True except (botocore.exceptions.ValidationError, botocore.exceptions.ClientError) as err: err_msg = str(err) print('err_msg:') print(err_msg) # スタックが存在せず、describe_stacksがエラーになればexist_stackはFalse if 'DescribeStack' in err_msg and 'does not exist' in err_msg: print(('describe_stacks error: ' + err_msg)) exist_stack = False else: raise # Create処理が必要かを示すフラグ need_create = True if event['RequestType'] == 'Update': print('Update Stacks') if exist_stack == False: # Update処理かつスタックが存在しない場合はCreate処理でスタックを作成する need_create = True else: # Create処理が必要かどうかを示すフラグ need_create = False # すでにスタック名に一致するスタックが存在すればUpdateで処理する try: print('Run cfn_client.update_stack.') response = cfn_client.update_stack( StackName=stack_name, TemplateBody=cfn_template, Parameters=cfn_params, Capabilities=[ 'CAPABILITY_NAMED_IAM' ] ) print('cfn_client.update_stack response:') print(response) # スタックのUpdate処理が完了するまで待つ waiter = cfn_client.get_waiter('stack_update_complete') waiter.wait(StackName=stack_name) except (botocore.exceptions.ValidationError, botocore.exceptions.ClientError) as err: err_msg = str(err) print('err_msg:') print(err_msg) # Update処理を要求されたが、変更点がない場合は何もしない if 'UpdateStack' in err_msg and 'No updates are to be performed' in err_msg: print(('update_stacks error: ' + err_msg)) else: raise if event['RequestType'] == 'Create' or need_create == True: print('Create Stacks') # Create処理要求かつスタックが存在しない場合、またはUpdate処理要求かつスタックが存在しない場合にスタックをCreate処理する if exist_stack == False: print('Run cfn_client.create_stack.') response = cfn_client.create_stack( StackName=stack_name, TemplateBody=cfn_template, Parameters=cfn_params, Capabilities=[ 'CAPABILITY_NAMED_IAM' ] ) print('cfn_client.create_stack response:') print(response) # スタックのCreate処理が完了するまで待つ waiter = cfn_client.get_waiter('stack_create_complete') waiter.wait(StackName=stack_name) # CreateまたはUpdateされたスタックを取得する stacks = cfn_client.describe_stacks(StackName=stack_name) # Outputsの内容を取得し、返却値として整形する outputs = stacks['Stacks'][0]['Outputs'] print(('Outputs:' + json.dumps(outputs, indent=2))) for output in outputs: response_data[output['OutputKey']] = output['OutputValue'] print(('Outputs:' + json.dumps(response_data, indent=2))) # CloudFormationカスタムリソース仕様で呼出元のスタックにレスポンスを返す cfn_response_send(event, context, SUCCESS, response_data) # cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data) return except Exception as e: print('Exception:') print(e) # CloudFormationカスタムリソース仕様で呼出元のスタックにレスポンスを返す cfn_response_send(event, context, FAILED, response_data) # cfnresponse.send(event, context, cfnresponse.FAILED, response_data) return LambdaCustomResourceToDeployCFnStackRole: Type: AWS::IAM::Role Properties: RoleName: IAMRole-LambdaCustomResourceToDeployCFnStack Path: / AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - edgelambda.amazonaws.com - lambda.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyName: IAMPolicy-LambdaCustomResourceToDeployCFnStack PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - cloudformation:CancelUpdateStack - cloudformation:ContinueUpdateRollback - cloudformation:Describe* - cloudformation:Get* - cloudformation:List* - cloudformation:CreateStack - cloudformation:UpdateStack - cloudformation:DeleteStack - cloudformation:ValidateTemplate - s3:GetObject - s3:ListAllMyBuckets - s3:ListBucket - iam:AttachRolePolicy - iam:CreatePolicy - iam:CreatePolicyVersion - iam:CreateRole - iam:DeletePolicy - iam:DeletePolicyVersion - iam:DeleteRole - iam:DeleteRolePermissionsBoundary - iam:DeleteRolePolicy - iam:DetachRolePolicy - iam:GetPolicy - iam:GetPolicyVersion - iam:GetRole - iam:GetRolePolicy - iam:ListAttachedRolePolicies - iam:ListInstanceProfilesForRole - iam:ListPolicyTags - iam:ListPolicyVersions - iam:ListRolePolicies - iam:ListRoles - iam:ListRoleTags - iam:PassRole - iam:PutRolePermissionsBoundary - iam:PutRolePolicy - iam:SetDefaultPolicyVersion - iam:TagPolicy - iam:TagRole - iam:UntagPolicy - iam:UntagRole - iam:UpdateAssumeRolePolicy - iam:UpdateRole - iam:UpdateRoleDescription - acm:* - lambda:* - route53:* - secretsmanager:* - wafv2:* Resource: - '*' - Effect: Allow Action: - logs:CreateLogGroup Resource: - 'arn:aws:logs:*:*:*' - Effect: Allow Action: - logs:CreateLogStream - logs:PutLogEvents Resource: - !Sub 'arn:aws:logs:*:*:log-group:/aws/lambda/LambdaCustomResourceToDeployCFnStack:*'
AWS CloudFormationテンプレート(呼び出し元)
入力パラメータ例
ACMCFnStackName: WebHostCFnACMCertificate ACMCustomDomainName: cfn-acm-edge-waf-cfnt-s3.h-o2k.com ACMHostedZoneId: ZZZZZZZZZZZZZZZZZZZZZ ACMS3BucketNameOfStoringTemplate: h-o2k ACMS3KeyOfStoringTemplate: WebHostCFnACMCertificate.yml CloudFrontCachePolicyName: CachingDisabled CloudFrontDistributionID: CloudFrontOriginRequestPolicyName: NONE CloudFrontResponseHeaderPolicyName: CORS-with-preflight-and-SecurityHeadersPolicy CustomResourceLambdaARN: arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:LambdaCustomResourceToDeployCFnStack Env: dev LambdaEdgeBasicAuthFuncName: WebHostCFnBasicAuthLambdaEdge LambdaEdgeBasicAuthID: Iam LambdaEdgeBasicAuthPW: Nobody LambdaEdgeCFnStackName: WebHostCFnBasicAuthLambdaEdge LambdaEdgeS3BucketNameOfStoringTemplate: h-o2k LambdaEdgeS3KeyOfStoringTemplate: WebHostCFnBasicAuthLambdaEdge.yml S3BucketName: cfn-acm-edge-waf-cfnt-s3-20230311144618 S3BucketVersioningStatus: Suspended WAFWebACLAllowIPList: XXX.XXX.XXX.XXX/16,YYY.YYY.YYY.YYY/24,ZZZ.ZZZ.ZZZ.ZZZ/32 WAFWebACLCFnStackName: WebHostCFnWAFWebACL WAFWebACLResourcePrefix: WebHostIPRestrictions WAFWebACLS3BucketNameOfStoringTemplate: h-o2k WAFWebACLS3KeyOfStoringTemplate: WebHostCFnWAFWebACL.yml
テンプレート本体
ファイル名:WebHostCFnS3CloudFrontWithAcmLambdaEdgeWaf.yml
AWSTemplateFormatVersion: '2010-09-09' Description: 'CFn Template for a stack that creates ACM, Lambda@Edge, WAF, and S3+CloudFront Hosting.' #<作成する順番> #ACM証明書[US]→OriginAccessControl[JP]→S3バケットポリシー(作成)[JP]→S3バケット[JP]→CloudFront(作成)[JP]→Lambda@Edge(CloudFrontDistributionIDで作成)[US] #→S3バケットポリシー(更新:CloudFrontDistributionIDでOCA設定)[JP]→CloudFront(更新:Lambda@Edge設定)[JP] Parameters: Env: #【共通】作成するS3バケットのサフィックスに追加する環境名 Type: String Default: dev S3BucketName: #【共通】静的ウェブサイトホスティングに使用するS3バケット名 Type: String Default: cfn-acm-edge-waf-cfnt-s3-20230311144618 S3BucketVersioningStatus: #【共通】静的ウェブサイトホスティングに使用するS3バケットのバージョニング設定 Type: String Default: Suspended AllowedValues: - Enabled - Suspended CustomResourceLambdaARN: #【共通】ACM証明書、基本認証用Lambda@Edge、IP制限用AWS WAFを作成するスタックをus-east1にデプロイするLambdaカスタムリソースのARN Type: String Default: arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:LambdaCustomResourceToDeployCFnStack ACMCFnStackName: #【ACM用】ACM証明書を作成するためにus-east1にデプロイするCloudformationスタック名 Type: String Default: WebHostCFnACMCertificate ACMS3BucketNameOfStoringTemplate: #【ACM用】ACM証明書を作成するCloudformationテンプレートを保存しているバケット名 Type: String Default: h-o2k ACMS3KeyOfStoringTemplate: #【ACM用】ACM証明書を作成するCloudformationテンプレートを保存しているバケット以降のオブジェクトキー Type: String Default: WebHostCFnACMCertificate.yml ACMCustomDomainName: #【ACM用】ACM証明書を発行する対象ドメイン名 Type: String Default: cfn-acm-edge-waf-cfnt-s3.h-o2k.com ACMHostedZoneId: #【ACM用】ACM証明書を発行する対象ドメイン名を管理するRoute53のホストゾーンID Type: String Default: ZZZZZZZZZZZZZZZZZZZZZ WAFWebACLCFnStackName: #【WAFWebACL用】AWS WAF WebACLを作成するためにus-east1にデプロイするCloudformationスタック名 Type: String Default: WebHostCFnWAFWebACL WAFWebACLS3BucketNameOfStoringTemplate: #【WAFWebACL用】AWS WAF WebACLを作成するCloudformationテンプレートを保存しているバケット名 Type: String Default: h-o2k WAFWebACLS3KeyOfStoringTemplate: #【WAFWebACL用】AWS WAF WebACLを作成するCloudformationテンプレートを保存しているバケット以降のオブジェクトキー Type: String Default: WebHostCFnWAFWebACL.yml WAFWebACLResourcePrefix: #【WAFWebACL用】AWS WAF WebACLのリソースにつけるPrefix Type: String Default: WebHostIPRestrictions WAFWebACLAllowIPList: #【WAFWebACL用】AWS WAF WebACLのルールでIP制限をするCIDR #Type: CommaDelimitedList Type: String Default: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16" LambdaEdgeCFnStackName: #【Lambda@Edge用】Lambda@Edgeを作成するためにus-east1にデプロイするCloudformationスタック名 Type: String Default: WebHostCFnBasicAuthLambdaEdge LambdaEdgeS3BucketNameOfStoringTemplate: #【Lambda@Edge用】Lambda@Edgeを作成するCloudformationテンプレートを保存しているバケット名 Type: String Default: h-o2k LambdaEdgeS3KeyOfStoringTemplate: #【Lambda@Edge用】Lambda@Edgeを作成するCloudformationテンプレートを保存しているバケット以降のオブジェクトキー Type: String Default: WebHostCFnBasicAuthLambdaEdge.yml LambdaEdgeBasicAuthFuncName: #【Lambda@Edge用】基本認証をするLambda@Edgeの関数名 Type: String Default: WebHostCFnBasicAuthLambdaEdge LambdaEdgeBasicAuthID: #【Lambda@Edge用】基本認証をするLambda@Edgeで認証するID。AWS Secrets Managerシークレットとして保管する。 Type: String Default: Iam LambdaEdgeBasicAuthPW: #【Lambda@Edge用】基本認証をするLambda@Edgeで認証するパスワード。AWS Secrets Managerシークレットとして保管する。 Type: String Default: Nobody CloudFrontCachePolicyName: #【共通】Amazon CloudFrontのキャッシュポリシーの指定。マネージドポリシーからの選択式。 Type: String Default: CachingDisabled AllowedValues: - NONE - Amplify - CachingDisabled - CachingOptimized - CachingOptimizedForUncompressedObjects - Elemental-MediaPackage CloudFrontOriginRequestPolicyName: #【共通】Amazon CloudFrontのオリジンリクエストポリシー。マネージドポリシーからの選択式。 Type: String Default: NONE AllowedValues: - NONE - AllViewer - AllViewerAndCloudFrontHeaders-2022-06 - AllViewerExceptHostHeader - CORS-CustomOrigin - CORS-S3Origin - Elemental-MediaTailor-PersonalizedManifests - UserAgentRefererHeaders CloudFrontResponseHeaderPolicyName: #【共通】Amazon CloudFrontのレスポンスヘッダーポリシー。マネージドポリシーからの選択式。 Type: String Default: CORS-with-preflight-and-SecurityHeadersPolicy AllowedValues: - NONE - SimpleCORS - CORS-With-Preflight - SecurityHeadersPolicy - CORS-and-SecurityHeadersPolicy - CORS-with-preflight-and-SecurityHeadersPolicy CloudFrontDistributionID: #【共通】各リソースの関連付けに使用するAmazon CloudFrontのDistribution ID。 #Amazon S3バケットポリシーのOriginAccessControl関連付け、Lambda@Edgeで使用するAWS Secrets Managerシークレットの一意識別、AWS WAFの関連付けで使用する。 Type: String #※スタックの初回Create処理でAmazon CloudFrontを作成してから、2回目のUpdate処理で入力する。 Mappings: CloudFrontCachePolicyIds: NONE: Id: "" Amplify: Id: 2e54312d-136d-493c-8eb9-b001f22f67d2 CachingDisabled: Id: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad CachingOptimized: Id: 658327ea-f89d-4fab-a63d-7e88639e58f6 CachingOptimizedForUncompressedObjects: Id: b2884449-e4de-46a7-ac36-70bc7f1ddd6d Elemental-MediaPackage: Id: 08627262-05a9-4f76-9ded-b50ca2e3a84f CloudFrontOriginRequestPolicyIds: NONE: Id: "" AllViewer: Id: 216adef6-5c7f-47e4-b989-5492eafa07d3 AllViewerAndCloudFrontHeaders-2022-06: Id: 33f36d7e-f396-46d9-90e0-52428a34d9dc AllViewerExceptHostHeader: Id: b689b0a8-53d0-40ab-baf2-68738e2966ac CORS-CustomOrigin: Id: 59781a5b-3903-41f3-afcb-af62929ccde1 CORS-S3Origin: Id: 88a5eaf4-2fd4-4709-b370-b4c650ea3fcf Elemental-MediaTailor-PersonalizedManifests: Id: 775133bc-15f2-49f9-abea-afb2e0bf67d2 UserAgentRefererHeaders: Id: acba4595-bd28-49b8-b9fe-13317c0390fa CloudFrontResponseHeaderPolicyIds: NONE: Id: "" CORS-and-SecurityHeadersPolicy: Id: e61eb60c-9c35-4d20-a928-2b84e02af89c CORS-With-Preflight: Id: 5cc3b908-e619-4b99-88e5-2cf7f45965bd CORS-with-preflight-and-SecurityHeadersPolicy: Id: eaab4381-ed33-4a86-88ca-d9558dc6cd63 SecurityHeadersPolicy: Id: 67f7725c-6f97-4210-82d7-5512b31e9d03 SimpleCORS: Id: 60669652-455b-4ae9-85a4-c4c02393f86c Conditions: IsEnv: !Equals [!Ref Env, NONE] IsCloudFrontDistributionID: #入力パラメータにAmazon CloudFrontのDistribution IDがある場合はTrueでLambda@Edgeを作成、ない場合はFalseでLambda@Edge作成をスキップ。 !Not [!Equals [!Ref CloudFrontDistributionID, ""]] Resources: S3Bucket: Type: AWS::S3::Bucket #DependsOn: #DeletionPolicy: Retain Properties: BucketName: !If [IsEnv, !Ref S3BucketName, !Join ["", [!Ref S3BucketName, "-", !Ref Env]]] WebsiteConfiguration: IndexDocument: index.html ErrorDocument: index.html CorsConfiguration: CorsRules: - { AllowedHeaders: ["*"], AllowedMethods: ["GET","PUT","POST","DELETE","HEAD"], AllowedOrigins: ["*"], MaxAge: 3000, } VersioningConfiguration: Status: !Ref S3BucketVersioningStatus PrivateBucketPolicy: Type: AWS::S3::BucketPolicy DependsOn: - OriginAccessControl - S3Bucket Properties: Bucket: !Ref S3Bucket PolicyDocument: Statement: - Action: s3:GetObject Effect: Allow Resource: !Sub ${S3Bucket.Arn}/* Principal: Service: cloudfront.amazonaws.com Condition: StringEquals: AWS:SourceArn: !If - IsCloudFrontDistributionID - !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistributionID} - !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/* OriginAccessControl: Type: AWS::CloudFront::OriginAccessControl Properties: OriginAccessControlConfig: Description: !Join ["", ["OAC-", !If [IsEnv, !Ref S3BucketName, !Join ["", [!Ref S3BucketName, "-", !Ref Env]]]]] Name: !Join ["", ["OAC-", !If [IsEnv, !Ref S3BucketName, !Join ["", [!Ref S3BucketName, "-", !Ref Env]]]]] OriginAccessControlOriginType: s3 SigningBehavior: always SigningProtocol: sigv4 #CloudFrontをエイリアスレコードとしてRoute53ホストゾーンに登録するRoute53レコードセット Route53RecordSetGroup: Type: AWS::Route53::RecordSetGroup DependsOn: - CloudFrontDistribution Properties: HostedZoneId: !Ref ACMHostedZoneId RecordSets: - Name: !Ref ACMCustomDomainName Type: A #CloudFrontをエイリアスレコードとして登録する場合はエイリアスターゲットのHostedZoneIdが次の固定値となる #参考:https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/quickref-route53.html AliasTarget: HostedZoneId: Z2FDTNDATAQYW2 DNSName: !GetAtt CloudFrontDistribution.DomainName #ACM証明書を発行するために呼び出すカスタムリソース CustomResourceForACM: Type: Custom::CustomResourceForACM Properties: ServiceToken: !Ref CustomResourceLambdaARN StackName: !Ref ACMCFnStackName CfnTplS3Bucket: !Ref ACMS3BucketNameOfStoringTemplate CfnTplS3Key: !Ref ACMS3KeyOfStoringTemplate CustomDomainName: !Ref ACMCustomDomainName HostedZoneId: !Ref ACMHostedZoneId #AWS WAF WebACLを作成するために呼び出すカスタムリソース CustomResourceForWAFWebACL: Type: Custom::CustomResourceForWAFWebACL Properties: ServiceToken: !Ref CustomResourceLambdaARN StackName: !Ref WAFWebACLCFnStackName CfnTplS3Bucket: !Ref WAFWebACLS3BucketNameOfStoringTemplate CfnTplS3Key: !Ref WAFWebACLS3KeyOfStoringTemplate ResourcePrefix: !Ref WAFWebACLResourcePrefix AllowIPList: !Ref WAFWebACLAllowIPList IsSkip: ##入力パラメータにAmazon CloudFrontのDistribution IDがない場合はカスタムリソースにIsSkip:trueを送信し、AWS WAF WebACL作成処理をスキップする。 !If [IsCloudFrontDistributionID, "false", "true"] #Lambda@Edgeを作成するために呼び出すカスタムリソース CustomResourceForLambdaEdge: Type: Custom::CustomResourceForLambdaEdge Properties: ServiceToken: !Ref CustomResourceLambdaARN StackName: !Ref LambdaEdgeCFnStackName CfnTplS3Bucket: !Ref LambdaEdgeS3BucketNameOfStoringTemplate CfnTplS3Key: !Ref LambdaEdgeS3KeyOfStoringTemplate CloudFrontDistId: !Ref CloudFrontDistributionID BasicAuthFuncName: !Ref LambdaEdgeBasicAuthFuncName BasicAuthID: !Ref LambdaEdgeBasicAuthID BasicAuthPW: !Ref LambdaEdgeBasicAuthPW IsSkip: ##入力パラメータにAmazon CloudFrontのDistribution IDがない場合はカスタムリソースにIsSkip:trueを送信し、Lambda@Edge作成処理をスキップする。 !If [IsCloudFrontDistributionID, "false", "true"] CloudFrontDistribution: Type: AWS::CloudFront::Distribution DependsOn: #OriginAccessControlが作成されてからCloudFrontに関連付けるためDependsOnにカスタムリソースを追加 - OriginAccessControl #Amazon S3バケットが作成されてからCloudFrontに関連付けるためDependsOnにカスタムリソースを追加 - S3Bucket #カスタムリソースでACM証明書が発行されてからCloudFrontに関連付けるためDependsOnにカスタムリソースを追加 - CustomResourceForACM #カスタムリソースでAWS WAF WebACLが作成されてからCloudFrontに関連付けるためDependsOnにカスタムリソースを追加 - CustomResourceForWAFWebACL #カスタムリソースで基本認証用Lambda@Edgeバージョンが作成されてからCloudFrontに関連付けるためDependsOnにカスタムリソースを追加 - CustomResourceForLambdaEdge Properties: DistributionConfig: #CloudFrontのエイリアスにACM証明書を発行したドメイン名を設定する Aliases: - !Ref ACMCustomDomainName #CloudFrontにカスタムリソースで作成したACM証明書を設定する ViewerCertificate: SslSupportMethod: sni-only MinimumProtocolVersion: TLSv1.2_2021 AcmCertificateArn: !GetAtt CustomResourceForACM.AcmCertificateArn WebACLId: !If - IsCloudFrontDistributionID - !GetAtt CustomResourceForWAFWebACL.WAFv2WebACLArn - !Ref AWS::NoValue HttpVersion: http2 Origins: - DomainName: !GetAtt S3Bucket.RegionalDomainName Id: StaticWebsiteHostingS3Bucket OriginAccessControlId: !GetAtt OriginAccessControl.Id S3OriginConfig: OriginAccessIdentity: "" Enabled: true DefaultCacheBehavior: AllowedMethods: [GET, HEAD, OPTIONS] TargetOriginId: StaticWebsiteHostingS3Bucket #オリジンをターゲットに指定する ForwardedValues: QueryString: false ViewerProtocolPolicy: redirect-to-https CachePolicyId: !FindInMap [ CloudFrontCachePolicyIds, !Ref CloudFrontCachePolicyName , Id ] OriginRequestPolicyId: !FindInMap [ CloudFrontOriginRequestPolicyIds, !Ref CloudFrontOriginRequestPolicyName , Id ] ResponseHeadersPolicyId: !FindInMap [ CloudFrontResponseHeaderPolicyIds, !Ref CloudFrontResponseHeaderPolicyName , Id ] #DefaultTTL: 86400 #MaxTTL: 31536000 #MinTTL: 60 #Compress: true LambdaFunctionAssociations: !If - IsCloudFrontDistributionID - [ { EventType: viewer-request, IncludeBody: "true", LambdaFunctionARN: !GetAtt CustomResourceForLambdaEdge.LambdaFunctionVersionArn } ] - !Ref AWS::NoValue DefaultRootObject: index.html CustomErrorResponses: - { ErrorCachingMinTTL: 10, ErrorCode: 400, ResponseCode: 200, ResponsePagePath: /, } - { ErrorCachingMinTTL: 10, ErrorCode: 404, ResponseCode: 200, ResponsePagePath: /, } Outputs: Region: Value: !Ref AWS::Region HostingS3BucketName: Description: "Hosting bucket name" Value: !Ref S3Bucket ACMCustomDomainURL: Value: !Join ["", ["https://", !Ref ACMCustomDomainName]] Description: "Web hosting URL with Certificate" S3BucketWebsiteURL: Value: !GetAtt S3Bucket.WebsiteURL Description: "URL for website hosted on S3" S3BucketSecureURL: Value: !Join ["", ["https://", !GetAtt S3Bucket.DomainName]] Description: "Name of S3 bucket to hold website content" CloudFrontDistributionID: Value: !Ref CloudFrontDistribution CloudFrontDomainName: Value: !GetAtt CloudFrontDistribution.DomainName CloudFrontSecureURL: Value: !Join ["", ["https://", !GetAtt CloudFrontDistribution.DomainName]] CloudFrontOriginAccessControl: Value: !Ref OriginAccessControl CustomResourceSkiped: Value: !If [IsCloudFrontDistributionID, "false", "true"]
AWS CloudFormationテンプレート(SSL/TLS証明書用AWS Certificate Manager)
テンプレート本体
ファイル名:WebHostWebHostCFnACMCertificate.yml
AWSTemplateFormatVersion: "2010-09-09" Description: "CFn Template for a stack that creates AWS CertificateManager Certificate." Parameters: CustomDomainName: #ACM証明書を発行する対象のカスタムドメイン名 Type: String HostedZoneId: #ACM証明書を発行する対象のカスタムドメインを管理しているAmazon Route53ホストゾーンID Type: String Resources: CertificateManagerCertificate: Type: AWS::CertificateManager::Certificate Properties: DomainName: !Ref CustomDomainName DomainValidationOptions: - DomainName: !Ref CustomDomainName HostedZoneId: !Ref HostedZoneId ValidationMethod: DNS #カスタムドメインの検証はRoute53のDNSを使用する方法で実施する Outputs: Region: Value: !Ref AWS::Region AcmCertificateArn: Value: !Ref CertificateManagerCertificate #返却値はACM証明書のARN
AWS CloudFormationテンプレート(基本認証用Lambda@Edge)
テンプレート本体
ファイル名:WebHostWebHostCFnBasicAuthLambdaEdge.yml
AWSTemplateFormatVersion: '2010-09-09' Description: 'CFn Template for a stack that creates Lambda@Edge Version and AWS Secrets Manager Secret.' Parameters: CloudFrontDistId: #基本認証用Lambda@Edgeを呼び出すAmazon CloudFrontのDistribution ID。AWS Secrets ManagerのシークレットIDに使用する。 Type: String #※このCloudFormationテンプレートではAmazon CloudFrontとLambda@Edgeバージョンの関連付けはしないため注意。関連付けは呼出元で実施する想定。 BasicAuthFuncName: #基本認証用Lambda@Edgeの関数名 Type: String BasicAuthID: #AWS Secrets Managerシークレットに保管し、基本認証で使用するID。 Type: String BasicAuthPW: #AWS Secrets Managerシークレットに保管し、基本認証で使用するパスワード。 Type: String Resources: LambdaEdgeBasicAuth: Type: AWS::Lambda::Function DependsOn: - SecretsManagerSecret - LambdaEdgeBasicAuthRole Properties: FunctionName: !Ref BasicAuthFuncName Runtime: python3.8 MemorySize: 128 #Lambda@Edgeのクォータ最大値を設定 Timeout: 5 #Lambda@Edgeのクォータ最大値を設定 Role: !GetAtt LambdaEdgeBasicAuthRole.Arn Handler: index.lambda_handler Code: ZipFile: | # 基本認証を実施するLambda@Edgeの関数 import json import boto3 import base64 # ID、パスワードを保管し、認証に使用するAWS Secrets Managerを扱うクライアントを作成 asm_client = boto3.client('secretsmanager', region_name='us-east-1') # エラーの場合のレスポンスを定義 err_response = { 'status': '401', 'statusDescription': 'Unauthorized', 'body': 'Authentication Failed.', 'headers': { 'www-authenticate': [ { 'key': 'WWW-Authenticate', 'value': 'Basic realm="Basic Authentication"' } ] } } def lambda_handler(event, context): try: print('event:') print(event) # イベントからCloudFrontのリクエストを取得 request = event['Records'][0]['cf']['request'] # イベントからCloudFrontのDistribution IDを取得 cf_dist_id = event['Records'][0]['cf']['config']['distributionId'] # リクエストからヘッダーを取得 headers = request['headers'] if (headers.get('authorization') != None): # ヘッダーにauthorizationがあれば認証を試行する # headers['authorization'][0]['value']の中身は「Basic <base64でエンコードされた[ID:Password]の文字列>」 # authorizationの内容を分解してID、パスワードを取り出す target_credentials_str = headers['authorization'][0]['value'].split( " ") target_credentials = base64.b64decode( target_credentials_str[1]).decode().split(":") target_id = target_credentials[0] target_pw = target_credentials[1] # 取得したCloudFrontのDistribution IDおよび基本認証で入力されたIDでシークレットを取得する response = asm_client.get_secret_value( SecretId='CloudFrontBasicAuth/' + cf_dist_id + '/' + str(target_id) ) # シークレットが取得でき、格納されている文字列が基本認証で入力されたパスワードと一致すれば認証成功としてリクエストを返却する。 # それ以外の場合はエラーレスポンスを返す。 if (response.get('SecretString') != None): secret_string = json.loads(response['SecretString']) if (secret_string.get('Password') == target_pw): return request else: return err_response else: return err_response else: return err_response except Exception as e: print("Exception:") print(e) return err_response LambdaEdgeBasicAuthVersion: #呼出元スタックでAmazon CloudFrontと関連付けるためのLambdaEdgeバージョンを作成する Type: AWS::Lambda::Version DependsOn: - LambdaEdgeBasicAuth Properties: FunctionName: !Ref LambdaEdgeBasicAuth Description: 'Basic Auth Lambda Edge for Amazon CloudFront' LambdaEdgeBasicAuthRole: #基本認証用Lambda@Edgeに適用するIAMロールとIAMポリシーの設定 Type: AWS::IAM::Role Properties: RoleName: !Sub 'IAMRole-${BasicAuthFuncName}' Path: / AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - edgelambda.amazonaws.com - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: !Sub 'IAMPolicy-${BasicAuthFuncName}' PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - iam:CreateServiceLinkedRole Resource: - '*' - Effect: Allow Action: - lambda:GetFunction - lambda:EnableReplication Resource: - !Sub 'arn:aws:lambda:us-east-1:${AWS::AccountId}:function:${BasicAuthFuncName}' - Effect: Allow Action: - cloudfront:UpdateDistribution Resource: - !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistId}' - PolicyName: SecretsManagerGetSecretValue PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - secretsmanager:GetSecretValue Resource: - !Sub 'arn:aws:secretsmanager:us-east-1:${AWS::AccountId}:secret:CloudFrontBasicAuth/${CloudFrontDistId}/*' SecretsManagerSecret: #基本認証に使用するID、パスワードを格納するAWS Secrets Managerシークレットの作成 Type: 'AWS::SecretsManager::Secret' Properties: Name: !Sub CloudFrontBasicAuth/${CloudFrontDistId}/${BasicAuthID} SecretString: !Sub '{"Password":"${BasicAuthPW}"}' Description: "SecretsManagerSecret of LambdaEdgeBasicAuth" Outputs: Region: Value: !Ref 'AWS::Region' LambdaFunctionArn: Value: !Ref LambdaEdgeBasicAuth #基本認証用Lambda@EdgeのARN LambdaFunctionVersionArn: Value: !Ref LambdaEdgeBasicAuthVersion #基本認証用Lambda@EdgeバージョンのARN SecretsManagerSecretArn: Value: !Ref SecretsManagerSecret #基本認証情報を格納したAWS Secrets ManagerシークレットのARN
AWS CloudFormationテンプレート(IP制限用AWS WAF)
テンプレート本体
ファイル名:WebHostCFnWAFWebACL.yml
AWSTemplateFormatVersion: '2010-09-09' Description: 'CFn Template for a stack that creates AWS WAF WebACL.' Parameters: ResourcePrefix: #AWS WAFのリソースにつけるPrefix Type: String Default: IPRestrictions AllowIPList: #IP制限をするCIDR #Type: CommaDelimitedList Type: String Default: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16" Conditions: IsAllowIPList: !Not [!Equals [!Ref AllowIPList, ""]] Resources: WAFv2WebACL: Type: AWS::WAFv2::WebACL Properties: Name: !Sub "${ResourcePrefix}-WebACL" Scope: CLOUDFRONT DefaultAction: Block: {} VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: !Sub "${ResourcePrefix}-WebACL-Metric" Rules: - Name: !Sub "${ResourcePrefix}-WebACL-Rule" Action: Allow: {} Priority: 0 Statement: IPSetReferenceStatement: Arn: !GetAtt WAFv2IPSet.Arn VisibilityConfig: SampledRequestsEnabled: true CloudWatchMetricsEnabled: true MetricName: !Sub "${ResourcePrefix}-WebACL-Rule-Metric" WAFv2IPSet: Type: AWS::WAFv2::IPSet Properties: Name: !Sub "${ResourcePrefix}-IPSet" Scope: CLOUDFRONT IPAddressVersion: IPV4 Addresses: !If [IsAllowIPList, !Split [ ",", !Ref AllowIPList ], []] Outputs: Region: Value: !Ref AWS::Region WAFv2WebACLArn: Value: !GetAtt WAFv2WebACL.Arn #返却値はWAF WebACLのARN
構築手順
-
呼び出し元リージョンで「WebHostCFnCustomResourceToDeployAWS CloudFormationStack.yml」を使用してAWS CloudFormationスタックを作成し、AWS Lambdaカスタムリソースをデプロイする
us-east-1リージョンに「AWS Certificate Manager(ACM)証明書」、「基本認証をするAWS Lambda@Edgeとそのバージョン」、「IP制限をするAWS WAF Web ACL」を作成するためのAWS Lambdaカスタムリソースを呼び出し元リージョンへ予めデプロイしておく。 -
「AWS Certificate Manager(ACM)証明書」、「基本認証をするAWS Lambda@Edgeとそのバージョン」、「IP制限をするAWS WAF Web ACL」を作成するAWS CloudFormationテンプレートファイルを任意のAmazon S3バケットに置く
「WebHostCFnACMCertificate.yml」「WebHostCFnBasicAuthLambdaEdge.yml」「WebHostCFnWAFWebACL.yml」を作成するAWS CloudFormationテンプレートをAmazon S3バケット(前述のパラメータ例では「h-o2k」)に予め配置しておく。 -
呼出元AWS CloudFormationテンプレートファイル「WebHostCFnS3CloudFrontWithAcmLambdaEdgeWaf.yml 」をAWS CloudFormationで実行する
呼出元AWS CloudFormationテンプレートをAWSマネジメントコンソールなどからパラメータを入力の上でAWS CloudFormationスタックとして実行する(CloudFrontDistributionIDは入力しない)。 -
静的ウェブサイトホスティングに使用するS3バケットにコンテンツを追加する
静的ウェブサイトホスティング用S3バケット(前述のパラメータ例では「cfn-acm-edge-waf-cfnt-s3-20230311144618-dev」)にindex.htmlなどコンテンツを追加する。
今回の例では「I will always remember that day and all of you.」とだけ表示されるindex.htmlを追加しました。
-
初回実行(Create処理でAmazon CloudFrontが作成され、Distribution IDが発行される。カスタムリソースによる「基本認証をするAWS Lambda@Edgeとそのバージョン」、「IP制限をするAWS WAF Web ACL」の作成は実行しない。)
最初の呼出元AWS CloudFormationスタックの実行では「CloudFrontDistributionID」にAmazon CloudFrontのDistribution IDを入力しないことで、「基本認証をするAWS Lambda@Edgeとそのバージョン」、「IP制限をするAWS WAF Web ACL」を作成するAWS CloudFormationスタックの作成処理はスキップして、それ以外のリソースを作成します。
■初回実行時(Create)のパラメータ入力例
~省略~ CloudFrontDistributionID: #初回時は入力しない(Distribution IDが発行されていないので入力できない) ~省略~
-
2回目実行(Update処理でAmazon CloudFrontのDistribution IDをカスタムリソースに渡し、「基本認証をするAWS Lambda@Edgeとそのバージョン」、「IP制限をするAWS WAF Web ACL」を作成する。)
2回目の呼出元AWS CloudFormationスタックの実行では「CloudFrontDistributionID」に初回実行で発行されたAmazon CloudFrontのDistribution IDを入力することで、「基本認証をするAWS Lambda@Edgeとそのバージョン」、「IP制限をするAWS WAF Web ACL」を作成するAWS CloudFormationスタックの作成処理を実行して、これらのリソースと呼出元のAmazon CloudFrontを関連付けます。
■2回目実行時(Update)のパラメータ入力例
#2回目実行時(Update)はAmazon CloudFrontのDistribution IDを含め、すべてのパラメータを入力 ~省略~ CloudFrontDistributionID: XXXXXXXXXXXXX ~省略~
-
結果確認
正しく実行されていればパラメータで指定した許可IPアドレスからACM証明書を発行する対象ドメインにhttpsでアクセスすると基本認証のダイアログが表示され、パラメータで指定した基本認証のIDとパスワードで認証を通過します。
許可IPアドレス以外からアクセスすると「403 ERROR」でアクセス拒否されます。
正しく動作しない場合は呼出元AWS CloudFormationスタックのイベント内容、カスタムリソースでデプロイしたスタックのイベント内容、AWS LambdaカスタムリソースのAmazon CloudWatch Logsの内容、AWS CloudTrailのログから原因を特定して不具合を修正します。
削除手順
AWS CloudFormationではLambda@EdgeおよびAWS WAF Web ACLとAmazon CloudFrontの相互関係のある関連付け解除を順序制御することはしないので一度にすべてのスタックを削除することはできません。
そのため、削除する場合は次の手順のように関連付けを解除した後、呼出元AWS CloudFormationスタックと「基本認証をするAWS Lambda@Edgeとそのバージョン」、「IP制限をするAWS WAF Web ACL」を作成したスタックをそれぞれ削除する必要があります。
- 呼出元AWS CloudFormationスタックのパラメータでAmazon CloudFrontのDistribution IDを空にしてUpdate処理をする
呼出元AWS CloudFormationスタックのパラメータ「CloudFrontDistributionID」を空にしてスタックを更新する。
(us-east-1の基本認証用Lambda@EdgeのスタックはバージョンでAmazon CloudFrontと関連付けがあるので失敗しますが、呼び出し元のスタックではAmazon CloudFrontとLambda@Edgeの関連付けが削除されます。) - 呼出元AWS CloudFormationスタックを削除する
呼出元AWS CloudFormationスタックと「基本認証をするAWS Lambda@Edgeとそのバージョン」、「IP制限をするAWS WAF Web ACL」のスタックの関連付けが解除されているため、呼出元AWS CloudFormationスタックが削除できる。 - us-east-1で基本認証用Lambda@Edgeバージョンを作成したAWS CloudFormationスタックを削除する
呼出元AWS CloudFormationスタックと基本認証用Lambda@Edgeのスタックの関連付けが解除されているため、基本認証用Lambda@Edgeバージョンを作成したAWS CloudFormationスタックを単独で削除する。
参考:
Route 53 template snippets - AWS CloudFormation
cfn-response module - AWS CloudFormation
Tech Blog with related articles referenced
まとめ
今回は「AWS LambdaカスタムリソースでAWS CloudFormationスタックを別リージョンにデプロイする」の記事で説明したAWS CloudFormationスタックを別リージョンにデプロイするAWS Lambdaカスタムリソースを使用し、SSL/TLS証明書(AWS Certificate Manager)・基本認証(Lambda@Edge)・IP制限(AWS WAF)をそれぞれus-east-1で作成して、呼出元AWS CloudFormationスタックで作成したリソースと関連付ける例を紹介しました。
今後もAWS CloudFormation、AWS Amplify、AWS CDKなどのデプロイ関連サービスや静的ウェブホスティングに関するアップデートについて様々なパターンを試してみたいと考えています。