小西秀和です。
この記事は過去に投稿した次の記事の続編で、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などのデプロイ関連サービスや静的ウェブホスティングに関するアップデートについて様々なパターンを試してみたいと考えています。