NRIネットコム Blog

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

Amazon S3とAmazon CloudFrontによる静的ウェブサイトにSSL/TLS証明書(AWS Certificate Manager)・基本認証(Lambda@Edge)・IP制限(AWS WAF)をクロスリージョンで追加するAWS CloudFormationテンプレートとAWS Lambdaカスタムリソース

小西秀和です。
この記事は過去に投稿した次の記事の続編で、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 LambdaカスタムリソースによるSSL証明書・基本認証・IP制限のスタックデプロイと関連付けの例
AWS LambdaカスタムリソースによるSSL証明書・基本認証・IP制限のスタックデプロイと関連付けの例

テンプレートの名称設定

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

  1. AWSTemplateFormatVersion: '2010-09-09'
  2. Description: 'CFn Template for a stack that creates AWS Lambda Custom Resource which deploys AWS CloudFormation stack.'
  3. Resources:
  4. LambdaCustomResourceToDeployCFnStack:
  5. Type: AWS::Lambda::Function
  6. DependsOn:
  7. - LambdaCustomResourceToDeployCFnStackRole
  8. Properties:
  9. FunctionName: LambdaCustomResourceToDeployCFnStack
  10. Description : 'AWS Lambda Custom Resource which deploys AWS CloudFormation stack.'
  11. Runtime: python3.9
  12. MemorySize: 10240
  13. Timeout: 900
  14. Role: !GetAtt LambdaCustomResourceToDeployCFnStackRole.Arn
  15. Handler: index.lambda_handler
  16. Code:
  17. ZipFile: |
  18. # ## このカスタムリソースの特徴
  19. # * カスタムリソース仕様のレスポンスを返すメソッドを自前で作成した
  20. # CloudFormationスタックから呼び出す場合はcfnresponse.sendが使用できますが、Lambda単体での実行やテストには使用できないためcfn_response_sendという自前のメソッドを作成しました。
  21. # 参考:https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-lambda-function-code-cfnresponsemodule.html
  22. # * デプロイするテンプレートをS3またはハードコーディングから選択できるようにした
  23. # S3からテンプレートを取得する方法では様々なテンプレートが実行できます。一方で特定のテンプレート実行に特化させたい場合はハードコーディングを選択することで他のテンプレートを実行させないようにできます。
  24. # * カスタムリソースからデプロイするCloudFormationテンプレートの引数の指定を簡素化した
  25. # デプロイするCloudformationテンプレートの引数として使用しないキーを指定して、それ以外を渡すことで引数の受け渡しを汎用的にしました。
  26. # * 要求された処理と異なる状況の場合でもリソースが作成される方針に寄せた
  27. # 要求されたCreate、Update処理をそのままCloudFormationのデプロイに使用するのではなく、Update処理でスタックが存在しない場合にはCreate処理に切り替えるなど、なるべくリソースが作成される方針に寄せました。
  28. # * 処理のスキップを選択できるようにした
  29. # 呼出元で依存関係を維持しながらCreate処理では使用せず、Update処理から使用するといったことができるようにスキップ処理が選択できるようにしました。
  30. import urllib3
  31. import json
  32. import boto3
  33. import botocore
  34. import datetime
  35. # import cfnresponse
  36. SUCCESS = 'SUCCESS'
  37. FAILED = 'FAILED'
  38. http = urllib3.PoolManager()
  39. # 呼出元のCloudformationスタックから受けた引数のうち、デプロイするCloudformationスタックの引数として使用しないキー
  40. config = {
  41. "ExcludeKeys": ["ServiceToken", "StackName", "CfnTplS3Bucket", "CfnTplS3Key", "IsSkip"]
  42. }
  43. # デプロイするCloudformationテンプレートを直接コードに記述したい場合はここに記述
  44. fixed_cfn_template = '''
  45. '''
  46. # us-east-1リージョンにCloudformationスタックをデプロイするクライアント
  47. cfn_client = boto3.client('cloudformation', region_name='us-east-1')
  48. # Cloudformationテンプレートを取得するS3リソース
  49. s3_resource = boto3.resource('s3')
  50. # カスタムリソースの仕様でCloudFormationにレスポンスを返す独自メソッド
  51. # CloudFormationスタックから呼び出す場合はcfnresponse.sendが使用できるが、Lambda単体での実行には使用できないため独自メソッドを使用する
  52. def cfn_response_send(event, context, response_status, response_data, physical_resource_id=None, no_echo=False, reason=None):
  53. response_url = event['ResponseURL']
  54. print(response_url)
  55. response_body = {
  56. 'Status': response_status,
  57. 'Reason': reason or 'See the details in CloudWatch Log Stream: {}'.format(context.log_stream_name),
  58. 'PhysicalResourceId': physical_resource_id or context.log_stream_name,
  59. 'StackId': event['StackId'],
  60. 'RequestId': event['RequestId'],
  61. 'LogicalResourceId': event['LogicalResourceId'],
  62. 'NoEcho': no_echo,
  63. 'Data': response_data
  64. }
  65. json_response_body = json.dumps(response_body)
  66. print('Response body:')
  67. print(json_response_body)
  68. headers = {
  69. 'content-type': '',
  70. 'content-length': str(len(json_response_body))
  71. }
  72. try:
  73. response = http.request('PUT', response_url, headers=headers, body=json_response_body)
  74. print('Status code:', response.status)
  75. except Exception as e:
  76. print('cfn_response_send(..) failed executing http.request(..):', e)
  77. def lambda_handler(event, context):
  78. response_data = {}
  79. try:
  80. print(('event:' + json.dumps(event, indent=2)))
  81. resource_properties = event['ResourceProperties']
  82. is_skip = resource_properties.get('IsSkip')
  83. # IsSkipにtrueが入ってきた場合は何も処理せず空のレスポンスを返す
  84. # 呼出元でカスタムリソース処理をスキップする場合に使用する
  85. if is_skip is not None and is_skip.strip().lower() == 'true':
  86. print('Skip All Process.')
  87. cfn_response_send(event, context, SUCCESS, response_data)
  88. # cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)
  89. return
  90. stack_name = resource_properties.get('StackName')
  91. exclude_keys = config['ExcludeKeys']
  92. cfn_params = []
  93. for key, val in resource_properties.items():
  94. if key not in exclude_keys:
  95. cfn_params.append({
  96. 'ParameterKey': key,
  97. 'ParameterValue': val
  98. })
  99. print('----------[Start]cfn_params:----------')
  100. print(cfn_params)
  101. print('----------[End]cfn_params:------------')
  102. cfn_template = fixed_cfn_template
  103. try:
  104. # fixed_cfn_template変数に直接テンプレートを記述していない場合は指定されたS3バケットのオブジェクトからテンプレートを取得する
  105. if cfn_template.strip() == '':
  106. cfn_tpl_s3_bucket = resource_properties['CfnTplS3Bucket']
  107. cfn_tpl_s3_key = resource_properties['CfnTplS3Key']
  108. bucket = s3_resource.Bucket(cfn_tpl_s3_bucket)
  109. obj = bucket.Object(cfn_tpl_s3_key).get()
  110. cfn_template = obj['Body'].read().decode('utf-8')
  111. print('----------[Start]cfn_template:----------')
  112. print(cfn_template)
  113. print('----------[End]cfn_template:------------')
  114. except Exception as e:
  115. print('lambda_handler(..) failed executing read s3 obj(..):', e)
  116. raise
  117. if event['RequestType'] == 'Delete':
  118. try:
  119. print('Delete Stacks')
  120. # Delete処理の場合は単純に削除する
  121. print('Run cfn_client.delete_stack.')
  122. response = cfn_client.delete_stack(
  123. StackName=stack_name
  124. )
  125. print('cfn_client.delete_stack response:')
  126. print(response)
  127. # スタックのDelete処理が完了するまで待つ
  128. waiter = cfn_client.get_waiter('stack_delete_complete')
  129. waiter.wait(StackName=stack_name)
  130. except (botocore.exceptions.ValidationError, botocore.exceptions.ClientError) as err:
  131. err_msg = str(err)
  132. print('err_msg:')
  133. print(err_msg)
  134. #相互に関連付けしたリソース同士で削除エラーのループが発生するため、例外は上げない。
  135. if event['RequestType'] == 'Create' or event['RequestType'] == 'Update':
  136. # Create処理とUpdate処理の実行方針
  137. # Create処理要求かつスタックが存在しない ⇒ Create処理
  138. # Create処理要求かつスタックが存在する ⇒ 何もしない
  139. # Update処理要求かつスタックが存在しない ⇒ Create処理
  140. # Update処理要求かつスタックが存在するかつ変更点が存在する ⇒ Update処理
  141. # Update処理要求かつスタックが存在するかつ変更点が存在しない ⇒ 何もしない
  142. # 同名のスタックが存在するかを示すフラグ
  143. exist_stack = False
  144. try:
  145. print('Run cfn_client.describe_stacks.')
  146. existing_stacks = cfn_client.describe_stacks(StackName=stack_name)
  147. print('Existing Stacks:')
  148. print(existing_stacks)
  149. # スタックが存在し、describe_stacksが正常に処理されればexist_stackはTrue
  150. exist_stack = True
  151. except (botocore.exceptions.ValidationError, botocore.exceptions.ClientError) as err:
  152. err_msg = str(err)
  153. print('err_msg:')
  154. print(err_msg)
  155. # スタックが存在せず、describe_stacksがエラーになればexist_stackはFalse
  156. if 'DescribeStack' in err_msg and 'does not exist' in err_msg:
  157. print(('describe_stacks error: ' + err_msg))
  158. exist_stack = False
  159. else:
  160. raise
  161. # Create処理が必要かを示すフラグ
  162. need_create = True
  163. if event['RequestType'] == 'Update':
  164. print('Update Stacks')
  165. if exist_stack == False:
  166. # Update処理かつスタックが存在しない場合はCreate処理でスタックを作成する
  167. need_create = True
  168. else:
  169. # Create処理が必要かどうかを示すフラグ
  170. need_create = False
  171. # すでにスタック名に一致するスタックが存在すればUpdateで処理する
  172. try:
  173. print('Run cfn_client.update_stack.')
  174. response = cfn_client.update_stack(
  175. StackName=stack_name,
  176. TemplateBody=cfn_template,
  177. Parameters=cfn_params,
  178. Capabilities=[
  179. 'CAPABILITY_NAMED_IAM'
  180. ]
  181. )
  182. print('cfn_client.update_stack response:')
  183. print(response)
  184. # スタックのUpdate処理が完了するまで待つ
  185. waiter = cfn_client.get_waiter('stack_update_complete')
  186. waiter.wait(StackName=stack_name)
  187. except (botocore.exceptions.ValidationError, botocore.exceptions.ClientError) as err:
  188. err_msg = str(err)
  189. print('err_msg:')
  190. print(err_msg)
  191. # Update処理を要求されたが、変更点がない場合は何もしない
  192. if 'UpdateStack' in err_msg and 'No updates are to be performed' in err_msg:
  193. print(('update_stacks error: ' + err_msg))
  194. else:
  195. raise
  196. if event['RequestType'] == 'Create' or need_create == True:
  197. print('Create Stacks')
  198. # Create処理要求かつスタックが存在しない場合、またはUpdate処理要求かつスタックが存在しない場合にスタックをCreate処理する
  199. if exist_stack == False:
  200. print('Run cfn_client.create_stack.')
  201. response = cfn_client.create_stack(
  202. StackName=stack_name,
  203. TemplateBody=cfn_template,
  204. Parameters=cfn_params,
  205. Capabilities=[
  206. 'CAPABILITY_NAMED_IAM'
  207. ]
  208. )
  209. print('cfn_client.create_stack response:')
  210. print(response)
  211. # スタックのCreate処理が完了するまで待つ
  212. waiter = cfn_client.get_waiter('stack_create_complete')
  213. waiter.wait(StackName=stack_name)
  214. # CreateまたはUpdateされたスタックを取得する
  215. stacks = cfn_client.describe_stacks(StackName=stack_name)
  216. # Outputsの内容を取得し、返却値として整形する
  217. outputs = stacks['Stacks'][0]['Outputs']
  218. print(('Outputs:' + json.dumps(outputs, indent=2)))
  219. for output in outputs:
  220. response_data[output['OutputKey']] = output['OutputValue']
  221. print(('Outputs:' + json.dumps(response_data, indent=2)))
  222. # CloudFormationカスタムリソース仕様で呼出元のスタックにレスポンスを返す
  223. cfn_response_send(event, context, SUCCESS, response_data)
  224. # cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)
  225. return
  226. except Exception as e:
  227. print('Exception:')
  228. print(e)
  229. # CloudFormationカスタムリソース仕様で呼出元のスタックにレスポンスを返す
  230. cfn_response_send(event, context, FAILED, response_data)
  231. # cfnresponse.send(event, context, cfnresponse.FAILED, response_data)
  232. return
  233. LambdaCustomResourceToDeployCFnStackRole:
  234. Type: AWS::IAM::Role
  235. Properties:
  236. RoleName: IAMRole-LambdaCustomResourceToDeployCFnStack
  237. Path: /
  238. AssumeRolePolicyDocument:
  239. Version: 2012-10-17
  240. Statement:
  241. - Effect: Allow
  242. Principal:
  243. Service:
  244. - edgelambda.amazonaws.com
  245. - lambda.amazonaws.com
  246. Action:
  247. - sts:AssumeRole
  248. Policies:
  249. - PolicyName: IAMPolicy-LambdaCustomResourceToDeployCFnStack
  250. PolicyDocument:
  251. Version: '2012-10-17'
  252. Statement:
  253. - Effect: Allow
  254. Action:
  255. - cloudformation:CancelUpdateStack
  256. - cloudformation:ContinueUpdateRollback
  257. - cloudformation:Describe*
  258. - cloudformation:Get*
  259. - cloudformation:List*
  260. - cloudformation:CreateStack
  261. - cloudformation:UpdateStack
  262. - cloudformation:DeleteStack
  263. - cloudformation:ValidateTemplate
  264. - s3:GetObject
  265. - s3:ListAllMyBuckets
  266. - s3:ListBucket
  267. - iam:AttachRolePolicy
  268. - iam:CreatePolicy
  269. - iam:CreatePolicyVersion
  270. - iam:CreateRole
  271. - iam:DeletePolicy
  272. - iam:DeletePolicyVersion
  273. - iam:DeleteRole
  274. - iam:DeleteRolePermissionsBoundary
  275. - iam:DeleteRolePolicy
  276. - iam:DetachRolePolicy
  277. - iam:GetPolicy
  278. - iam:GetPolicyVersion
  279. - iam:GetRole
  280. - iam:GetRolePolicy
  281. - iam:ListAttachedRolePolicies
  282. - iam:ListInstanceProfilesForRole
  283. - iam:ListPolicyTags
  284. - iam:ListPolicyVersions
  285. - iam:ListRolePolicies
  286. - iam:ListRoles
  287. - iam:ListRoleTags
  288. - iam:PassRole
  289. - iam:PutRolePermissionsBoundary
  290. - iam:PutRolePolicy
  291. - iam:SetDefaultPolicyVersion
  292. - iam:TagPolicy
  293. - iam:TagRole
  294. - iam:UntagPolicy
  295. - iam:UntagRole
  296. - iam:UpdateAssumeRolePolicy
  297. - iam:UpdateRole
  298. - iam:UpdateRoleDescription
  299. - acm:*
  300. - lambda:*
  301. - route53:*
  302. - secretsmanager:*
  303. - wafv2:*
  304. Resource:
  305. - '*'
  306. - Effect: Allow
  307. Action:
  308. - logs:CreateLogGroup
  309. Resource:
  310. - 'arn:aws:logs:*:*:*'
  311. - Effect: Allow
  312. Action:
  313. - logs:CreateLogStream
  314. - logs:PutLogEvents
  315. Resource:
  316. - !Sub 'arn:aws:logs:*:*:log-group:/aws/lambda/LambdaCustomResourceToDeployCFnStack:*'

AWS CloudFormationテンプレート(呼び出し元)

入力パラメータ例

  1. ACMCFnStackName: WebHostCFnACMCertificate
  2. ACMCustomDomainName: cfn-acm-edge-waf-cfnt-s3.h-o2k.com
  3. ACMHostedZoneId: ZZZZZZZZZZZZZZZZZZZZZ
  4. ACMS3BucketNameOfStoringTemplate: h-o2k
  5. ACMS3KeyOfStoringTemplate: WebHostCFnACMCertificate.yml
  6. CloudFrontCachePolicyName: CachingDisabled
  7. CloudFrontDistributionID:
  8. CloudFrontOriginRequestPolicyName: NONE
  9. CloudFrontResponseHeaderPolicyName: CORS-with-preflight-and-SecurityHeadersPolicy
  10. CustomResourceLambdaARN: arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:LambdaCustomResourceToDeployCFnStack
  11. Env: dev
  12. LambdaEdgeBasicAuthFuncName: WebHostCFnBasicAuthLambdaEdge
  13. LambdaEdgeBasicAuthID: Iam
  14. LambdaEdgeBasicAuthPW: Nobody
  15. LambdaEdgeCFnStackName: WebHostCFnBasicAuthLambdaEdge
  16. LambdaEdgeS3BucketNameOfStoringTemplate: h-o2k
  17. LambdaEdgeS3KeyOfStoringTemplate: WebHostCFnBasicAuthLambdaEdge.yml
  18. S3BucketName: cfn-acm-edge-waf-cfnt-s3-20230311144618
  19. S3BucketVersioningStatus: Suspended
  20. WAFWebACLAllowIPList: XXX.XXX.XXX.XXX/16,YYY.YYY.YYY.YYY/24,ZZZ.ZZZ.ZZZ.ZZZ/32
  21. WAFWebACLCFnStackName: WebHostCFnWAFWebACL
  22. WAFWebACLResourcePrefix: WebHostIPRestrictions
  23. WAFWebACLS3BucketNameOfStoringTemplate: h-o2k
  24. WAFWebACLS3KeyOfStoringTemplate: WebHostCFnWAFWebACL.yml

テンプレート本体

ファイル名:WebHostCFnS3CloudFrontWithAcmLambdaEdgeWaf.yml

  1. AWSTemplateFormatVersion: '2010-09-09'
  2. Description: 'CFn Template for a stack that creates ACM, Lambda@Edge, WAF, and S3+CloudFront Hosting.'
  3. #<作成する順番>
  4. #ACM証明書[US]→OriginAccessControl[JP]→S3バケットポリシー(作成)[JP]→S3バケット[JP]→CloudFront(作成)[JP]→Lambda@Edge(CloudFrontDistributionIDで作成)[US]
  5. #→S3バケットポリシー(更新:CloudFrontDistributionIDでOCA設定)[JP]→CloudFront(更新:Lambda@Edge設定)[JP]
  6. Parameters:
  7. Env: #【共通】作成するS3バケットのサフィックスに追加する環境名
  8. Type: String
  9. Default: dev
  10. S3BucketName: #【共通】静的ウェブサイトホスティングに使用するS3バケット名
  11. Type: String
  12. Default: cfn-acm-edge-waf-cfnt-s3-20230311144618
  13. S3BucketVersioningStatus: #【共通】静的ウェブサイトホスティングに使用するS3バケットのバージョニング設定
  14. Type: String
  15. Default: Suspended
  16. AllowedValues:
  17. - Enabled
  18. - Suspended
  19. CustomResourceLambdaARN: #【共通】ACM証明書、基本認証用Lambda@Edge、IP制限用AWS WAFを作成するスタックをus-east1にデプロイするLambdaカスタムリソースのARN
  20. Type: String
  21. Default: arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:LambdaCustomResourceToDeployCFnStack
  22. ACMCFnStackName: #【ACM用】ACM証明書を作成するためにus-east1にデプロイするCloudformationスタック名
  23. Type: String
  24. Default: WebHostCFnACMCertificate
  25. ACMS3BucketNameOfStoringTemplate: #【ACM用】ACM証明書を作成するCloudformationテンプレートを保存しているバケット名
  26. Type: String
  27. Default: h-o2k
  28. ACMS3KeyOfStoringTemplate: #【ACM用】ACM証明書を作成するCloudformationテンプレートを保存しているバケット以降のオブジェクトキー
  29. Type: String
  30. Default: WebHostCFnACMCertificate.yml
  31. ACMCustomDomainName: #【ACM用】ACM証明書を発行する対象ドメイン名
  32. Type: String
  33. Default: cfn-acm-edge-waf-cfnt-s3.h-o2k.com
  34. ACMHostedZoneId: #【ACM用】ACM証明書を発行する対象ドメイン名を管理するRoute53のホストゾーンID
  35. Type: String
  36. Default: ZZZZZZZZZZZZZZZZZZZZZ
  37. WAFWebACLCFnStackName: #【WAFWebACL用】AWS WAF WebACLを作成するためにus-east1にデプロイするCloudformationスタック名
  38. Type: String
  39. Default: WebHostCFnWAFWebACL
  40. WAFWebACLS3BucketNameOfStoringTemplate: #【WAFWebACL用】AWS WAF WebACLを作成するCloudformationテンプレートを保存しているバケット名
  41. Type: String
  42. Default: h-o2k
  43. WAFWebACLS3KeyOfStoringTemplate: #【WAFWebACL用】AWS WAF WebACLを作成するCloudformationテンプレートを保存しているバケット以降のオブジェクトキー
  44. Type: String
  45. Default: WebHostCFnWAFWebACL.yml
  46. WAFWebACLResourcePrefix: #【WAFWebACL用】AWS WAF WebACLのリソースにつけるPrefix
  47. Type: String
  48. Default: WebHostIPRestrictions
  49. WAFWebACLAllowIPList: #【WAFWebACL用】AWS WAF WebACLのルールでIP制限をするCIDR
  50. #Type: CommaDelimitedList
  51. Type: String
  52. Default: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
  53. LambdaEdgeCFnStackName: #【Lambda@Edge用】Lambda@Edgeを作成するためにus-east1にデプロイするCloudformationスタック名
  54. Type: String
  55. Default: WebHostCFnBasicAuthLambdaEdge
  56. LambdaEdgeS3BucketNameOfStoringTemplate: #【Lambda@Edge用】Lambda@Edgeを作成するCloudformationテンプレートを保存しているバケット名
  57. Type: String
  58. Default: h-o2k
  59. LambdaEdgeS3KeyOfStoringTemplate: #【Lambda@Edge用】Lambda@Edgeを作成するCloudformationテンプレートを保存しているバケット以降のオブジェクトキー
  60. Type: String
  61. Default: WebHostCFnBasicAuthLambdaEdge.yml
  62. LambdaEdgeBasicAuthFuncName: #【Lambda@Edge用】基本認証をするLambda@Edgeの関数名
  63. Type: String
  64. Default: WebHostCFnBasicAuthLambdaEdge
  65. LambdaEdgeBasicAuthID: #【Lambda@Edge用】基本認証をするLambda@Edgeで認証するID。AWS Secrets Managerシークレットとして保管する。
  66. Type: String
  67. Default: Iam
  68. LambdaEdgeBasicAuthPW: #【Lambda@Edge用】基本認証をするLambda@Edgeで認証するパスワード。AWS Secrets Managerシークレットとして保管する。
  69. Type: String
  70. Default: Nobody
  71. CloudFrontCachePolicyName: #【共通】Amazon CloudFrontのキャッシュポリシーの指定。マネージドポリシーからの選択式。
  72. Type: String
  73. Default: CachingDisabled
  74. AllowedValues:
  75. - NONE
  76. - Amplify
  77. - CachingDisabled
  78. - CachingOptimized
  79. - CachingOptimizedForUncompressedObjects
  80. - Elemental-MediaPackage
  81. CloudFrontOriginRequestPolicyName: #【共通】Amazon CloudFrontのオリジンリクエストポリシー。マネージドポリシーからの選択式。
  82. Type: String
  83. Default: NONE
  84. AllowedValues:
  85. - NONE
  86. - AllViewer
  87. - AllViewerAndCloudFrontHeaders-2022-06
  88. - AllViewerExceptHostHeader
  89. - CORS-CustomOrigin
  90. - CORS-S3Origin
  91. - Elemental-MediaTailor-PersonalizedManifests
  92. - UserAgentRefererHeaders
  93. CloudFrontResponseHeaderPolicyName: #【共通】Amazon CloudFrontのレスポンスヘッダーポリシー。マネージドポリシーからの選択式。
  94. Type: String
  95. Default: CORS-with-preflight-and-SecurityHeadersPolicy
  96. AllowedValues:
  97. - NONE
  98. - SimpleCORS
  99. - CORS-With-Preflight
  100. - SecurityHeadersPolicy
  101. - CORS-and-SecurityHeadersPolicy
  102. - CORS-with-preflight-and-SecurityHeadersPolicy
  103. CloudFrontDistributionID: #【共通】各リソースの関連付けに使用するAmazon CloudFrontのDistribution ID。
  104. #Amazon S3バケットポリシーのOriginAccessControl関連付け、Lambda@Edgeで使用するAWS Secrets Managerシークレットの一意識別、AWS WAFの関連付けで使用する。
  105. Type: String #※スタックの初回Create処理でAmazon CloudFrontを作成してから、2回目のUpdate処理で入力する。
  106. Mappings:
  107. CloudFrontCachePolicyIds:
  108. NONE:
  109. Id: ""
  110. Amplify:
  111. Id: 2e54312d-136d-493c-8eb9-b001f22f67d2
  112. CachingDisabled:
  113. Id: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad
  114. CachingOptimized:
  115. Id: 658327ea-f89d-4fab-a63d-7e88639e58f6
  116. CachingOptimizedForUncompressedObjects:
  117. Id: b2884449-e4de-46a7-ac36-70bc7f1ddd6d
  118. Elemental-MediaPackage:
  119. Id: 08627262-05a9-4f76-9ded-b50ca2e3a84f
  120. CloudFrontOriginRequestPolicyIds:
  121. NONE:
  122. Id: ""
  123. AllViewer:
  124. Id: 216adef6-5c7f-47e4-b989-5492eafa07d3
  125. AllViewerAndCloudFrontHeaders-2022-06:
  126. Id: 33f36d7e-f396-46d9-90e0-52428a34d9dc
  127. AllViewerExceptHostHeader:
  128. Id: b689b0a8-53d0-40ab-baf2-68738e2966ac
  129. CORS-CustomOrigin:
  130. Id: 59781a5b-3903-41f3-afcb-af62929ccde1
  131. CORS-S3Origin:
  132. Id: 88a5eaf4-2fd4-4709-b370-b4c650ea3fcf
  133. Elemental-MediaTailor-PersonalizedManifests:
  134. Id: 775133bc-15f2-49f9-abea-afb2e0bf67d2
  135. UserAgentRefererHeaders:
  136. Id: acba4595-bd28-49b8-b9fe-13317c0390fa
  137. CloudFrontResponseHeaderPolicyIds:
  138. NONE:
  139. Id: ""
  140. CORS-and-SecurityHeadersPolicy:
  141. Id: e61eb60c-9c35-4d20-a928-2b84e02af89c
  142. CORS-With-Preflight:
  143. Id: 5cc3b908-e619-4b99-88e5-2cf7f45965bd
  144. CORS-with-preflight-and-SecurityHeadersPolicy:
  145. Id: eaab4381-ed33-4a86-88ca-d9558dc6cd63
  146. SecurityHeadersPolicy:
  147. Id: 67f7725c-6f97-4210-82d7-5512b31e9d03
  148. SimpleCORS:
  149. Id: 60669652-455b-4ae9-85a4-c4c02393f86c
  150. Conditions:
  151. IsEnv:
  152. !Equals [!Ref Env, NONE]
  153. IsCloudFrontDistributionID: #入力パラメータにAmazon CloudFrontのDistribution IDがある場合はTrueでLambda@Edgeを作成、ない場合はFalseでLambda@Edge作成をスキップ。
  154. !Not [!Equals [!Ref CloudFrontDistributionID, ""]]
  155. Resources:
  156. S3Bucket:
  157. Type: AWS::S3::Bucket
  158. #DependsOn:
  159. #DeletionPolicy: Retain
  160. Properties:
  161. BucketName:
  162. !If [IsEnv, !Ref S3BucketName, !Join ["", [!Ref S3BucketName, "-", !Ref Env]]]
  163. WebsiteConfiguration:
  164. IndexDocument: index.html
  165. ErrorDocument: index.html
  166. CorsConfiguration:
  167. CorsRules:
  168. - {
  169. AllowedHeaders: ["*"],
  170. AllowedMethods: ["GET","PUT","POST","DELETE","HEAD"],
  171. AllowedOrigins: ["*"],
  172. MaxAge: 3000,
  173. }
  174. VersioningConfiguration:
  175. Status: !Ref S3BucketVersioningStatus
  176. PrivateBucketPolicy:
  177. Type: AWS::S3::BucketPolicy
  178. DependsOn:
  179. - OriginAccessControl
  180. - S3Bucket
  181. Properties:
  182. Bucket: !Ref S3Bucket
  183. PolicyDocument:
  184. Statement:
  185. - Action: s3:GetObject
  186. Effect: Allow
  187. Resource: !Sub ${S3Bucket.Arn}/*
  188. Principal:
  189. Service: cloudfront.amazonaws.com
  190. Condition:
  191. StringEquals:
  192. AWS:SourceArn:
  193. !If
  194. - IsCloudFrontDistributionID
  195. - !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistributionID}
  196. - !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/*
  197. OriginAccessControl:
  198. Type: AWS::CloudFront::OriginAccessControl
  199. Properties:
  200. OriginAccessControlConfig:
  201. Description: !Join ["", ["OAC-", !If [IsEnv, !Ref S3BucketName, !Join ["", [!Ref S3BucketName, "-", !Ref Env]]]]]
  202. Name: !Join ["", ["OAC-", !If [IsEnv, !Ref S3BucketName, !Join ["", [!Ref S3BucketName, "-", !Ref Env]]]]]
  203. OriginAccessControlOriginType: s3
  204. SigningBehavior: always
  205. SigningProtocol: sigv4
  206. #CloudFrontをエイリアスレコードとしてRoute53ホストゾーンに登録するRoute53レコードセット
  207. Route53RecordSetGroup:
  208. Type: AWS::Route53::RecordSetGroup
  209. DependsOn:
  210. - CloudFrontDistribution
  211. Properties:
  212. HostedZoneId:
  213. !Ref ACMHostedZoneId
  214. RecordSets:
  215. - Name: !Ref ACMCustomDomainName
  216. Type: A
  217. #CloudFrontをエイリアスレコードとして登録する場合はエイリアスターゲットのHostedZoneIdが次の固定値となる
  218. #参考:https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/quickref-route53.html
  219. AliasTarget:
  220. HostedZoneId: Z2FDTNDATAQYW2
  221. DNSName: !GetAtt CloudFrontDistribution.DomainName
  222. #ACM証明書を発行するために呼び出すカスタムリソース
  223. CustomResourceForACM:
  224. Type: Custom::CustomResourceForACM
  225. Properties:
  226. ServiceToken: !Ref CustomResourceLambdaARN
  227. StackName: !Ref ACMCFnStackName
  228. CfnTplS3Bucket: !Ref ACMS3BucketNameOfStoringTemplate
  229. CfnTplS3Key: !Ref ACMS3KeyOfStoringTemplate
  230. CustomDomainName: !Ref ACMCustomDomainName
  231. HostedZoneId: !Ref ACMHostedZoneId
  232. #AWS WAF WebACLを作成するために呼び出すカスタムリソース
  233. CustomResourceForWAFWebACL:
  234. Type: Custom::CustomResourceForWAFWebACL
  235. Properties:
  236. ServiceToken: !Ref CustomResourceLambdaARN
  237. StackName: !Ref WAFWebACLCFnStackName
  238. CfnTplS3Bucket: !Ref WAFWebACLS3BucketNameOfStoringTemplate
  239. CfnTplS3Key: !Ref WAFWebACLS3KeyOfStoringTemplate
  240. ResourcePrefix: !Ref WAFWebACLResourcePrefix
  241. AllowIPList: !Ref WAFWebACLAllowIPList
  242. IsSkip: ##入力パラメータにAmazon CloudFrontのDistribution IDがない場合はカスタムリソースにIsSkip:trueを送信し、AWS WAF WebACL作成処理をスキップする。
  243. !If [IsCloudFrontDistributionID, "false", "true"]
  244. #Lambda@Edgeを作成するために呼び出すカスタムリソース
  245. CustomResourceForLambdaEdge:
  246. Type: Custom::CustomResourceForLambdaEdge
  247. Properties:
  248. ServiceToken: !Ref CustomResourceLambdaARN
  249. StackName: !Ref LambdaEdgeCFnStackName
  250. CfnTplS3Bucket: !Ref LambdaEdgeS3BucketNameOfStoringTemplate
  251. CfnTplS3Key: !Ref LambdaEdgeS3KeyOfStoringTemplate
  252. CloudFrontDistId: !Ref CloudFrontDistributionID
  253. BasicAuthFuncName: !Ref LambdaEdgeBasicAuthFuncName
  254. BasicAuthID: !Ref LambdaEdgeBasicAuthID
  255. BasicAuthPW: !Ref LambdaEdgeBasicAuthPW
  256. IsSkip: ##入力パラメータにAmazon CloudFrontのDistribution IDがない場合はカスタムリソースにIsSkip:trueを送信し、Lambda@Edge作成処理をスキップする。
  257. !If [IsCloudFrontDistributionID, "false", "true"]
  258. CloudFrontDistribution:
  259. Type: AWS::CloudFront::Distribution
  260. DependsOn:
  261. #OriginAccessControlが作成されてからCloudFrontに関連付けるためDependsOnにカスタムリソースを追加
  262. - OriginAccessControl
  263. #Amazon S3バケットが作成されてからCloudFrontに関連付けるためDependsOnにカスタムリソースを追加
  264. - S3Bucket
  265. #カスタムリソースでACM証明書が発行されてからCloudFrontに関連付けるためDependsOnにカスタムリソースを追加
  266. - CustomResourceForACM
  267. #カスタムリソースでAWS WAF WebACLが作成されてからCloudFrontに関連付けるためDependsOnにカスタムリソースを追加
  268. - CustomResourceForWAFWebACL
  269. #カスタムリソースで基本認証用Lambda@Edgeバージョンが作成されてからCloudFrontに関連付けるためDependsOnにカスタムリソースを追加
  270. - CustomResourceForLambdaEdge
  271. Properties:
  272. DistributionConfig:
  273. #CloudFrontのエイリアスにACM証明書を発行したドメイン名を設定する
  274. Aliases:
  275. - !Ref ACMCustomDomainName
  276. #CloudFrontにカスタムリソースで作成したACM証明書を設定する
  277. ViewerCertificate:
  278. SslSupportMethod: sni-only
  279. MinimumProtocolVersion: TLSv1.2_2021
  280. AcmCertificateArn: !GetAtt CustomResourceForACM.AcmCertificateArn
  281. WebACLId:
  282. !If
  283. - IsCloudFrontDistributionID
  284. - !GetAtt CustomResourceForWAFWebACL.WAFv2WebACLArn
  285. - !Ref AWS::NoValue
  286. HttpVersion: http2
  287. Origins:
  288. - DomainName: !GetAtt S3Bucket.RegionalDomainName
  289. Id: StaticWebsiteHostingS3Bucket
  290. OriginAccessControlId: !GetAtt OriginAccessControl.Id
  291. S3OriginConfig:
  292. OriginAccessIdentity: ""
  293. Enabled: true
  294. DefaultCacheBehavior:
  295. AllowedMethods: [GET, HEAD, OPTIONS]
  296. TargetOriginId: StaticWebsiteHostingS3Bucket #オリジンをターゲットに指定する
  297. ForwardedValues:
  298. QueryString: false
  299. ViewerProtocolPolicy: redirect-to-https
  300. CachePolicyId: !FindInMap [ CloudFrontCachePolicyIds, !Ref CloudFrontCachePolicyName , Id ]
  301. OriginRequestPolicyId: !FindInMap [ CloudFrontOriginRequestPolicyIds, !Ref CloudFrontOriginRequestPolicyName , Id ]
  302. ResponseHeadersPolicyId: !FindInMap [ CloudFrontResponseHeaderPolicyIds, !Ref CloudFrontResponseHeaderPolicyName , Id ]
  303. #DefaultTTL: 86400
  304. #MaxTTL: 31536000
  305. #MinTTL: 60
  306. #Compress: true
  307. LambdaFunctionAssociations:
  308. !If
  309. - IsCloudFrontDistributionID
  310. - [
  311. {
  312. EventType: viewer-request,
  313. IncludeBody: "true",
  314. LambdaFunctionARN: !GetAtt CustomResourceForLambdaEdge.LambdaFunctionVersionArn
  315. }
  316. ]
  317. - !Ref AWS::NoValue
  318. DefaultRootObject: index.html
  319. CustomErrorResponses:
  320. - {
  321. ErrorCachingMinTTL: 10,
  322. ErrorCode: 400,
  323. ResponseCode: 200,
  324. ResponsePagePath: /,
  325. }
  326. - {
  327. ErrorCachingMinTTL: 10,
  328. ErrorCode: 404,
  329. ResponseCode: 200,
  330. ResponsePagePath: /,
  331. }
  332. Outputs:
  333. Region:
  334. Value:
  335. !Ref AWS::Region
  336. HostingS3BucketName:
  337. Description: "Hosting bucket name"
  338. Value:
  339. !Ref S3Bucket
  340. ACMCustomDomainURL:
  341. Value:
  342. !Join ["", ["https://", !Ref ACMCustomDomainName]]
  343. Description: "Web hosting URL with Certificate"
  344. S3BucketWebsiteURL:
  345. Value:
  346. !GetAtt S3Bucket.WebsiteURL
  347. Description: "URL for website hosted on S3"
  348. S3BucketSecureURL:
  349. Value:
  350. !Join ["", ["https://", !GetAtt S3Bucket.DomainName]]
  351. Description: "Name of S3 bucket to hold website content"
  352. CloudFrontDistributionID:
  353. Value:
  354. !Ref CloudFrontDistribution
  355. CloudFrontDomainName:
  356. Value:
  357. !GetAtt CloudFrontDistribution.DomainName
  358. CloudFrontSecureURL:
  359. Value:
  360. !Join ["", ["https://", !GetAtt CloudFrontDistribution.DomainName]]
  361. CloudFrontOriginAccessControl:
  362. Value:
  363. !Ref OriginAccessControl
  364. CustomResourceSkiped:
  365. Value:
  366. !If [IsCloudFrontDistributionID, "false", "true"]

AWS CloudFormationテンプレート(SSL/TLS証明書用AWS Certificate Manager)

テンプレート本体

ファイル名:WebHostWebHostCFnACMCertificate.yml

  1. AWSTemplateFormatVersion: "2010-09-09"
  2. Description: "CFn Template for a stack that creates AWS CertificateManager Certificate."
  3. Parameters:
  4. CustomDomainName: #ACM証明書を発行する対象のカスタムドメイン名
  5. Type: String
  6. HostedZoneId: #ACM証明書を発行する対象のカスタムドメインを管理しているAmazon Route53ホストゾーンID
  7. Type: String
  8. Resources:
  9. CertificateManagerCertificate:
  10. Type: AWS::CertificateManager::Certificate
  11. Properties:
  12. DomainName: !Ref CustomDomainName
  13. DomainValidationOptions:
  14. - DomainName: !Ref CustomDomainName
  15. HostedZoneId: !Ref HostedZoneId
  16. ValidationMethod: DNS #カスタムドメインの検証はRoute53のDNSを使用する方法で実施する
  17. Outputs:
  18. Region:
  19. Value: !Ref AWS::Region
  20. AcmCertificateArn:
  21. Value: !Ref CertificateManagerCertificate #返却値はACM証明書のARN

AWS CloudFormationテンプレート(基本認証用Lambda@Edge)

テンプレート本体

ファイル名:WebHostWebHostCFnBasicAuthLambdaEdge.yml

  1. AWSTemplateFormatVersion: '2010-09-09'
  2. Description: 'CFn Template for a stack that creates Lambda@Edge Version and AWS Secrets Manager Secret.'
  3. Parameters:
  4. CloudFrontDistId: #基本認証用Lambda@Edgeを呼び出すAmazon CloudFrontのDistribution ID。AWS Secrets ManagerのシークレットIDに使用する。
  5. Type: String #※このCloudFormationテンプレートではAmazon CloudFrontとLambda@Edgeバージョンの関連付けはしないため注意。関連付けは呼出元で実施する想定。
  6. BasicAuthFuncName: #基本認証用Lambda@Edgeの関数名
  7. Type: String
  8. BasicAuthID: #AWS Secrets Managerシークレットに保管し、基本認証で使用するID。
  9. Type: String
  10. BasicAuthPW: #AWS Secrets Managerシークレットに保管し、基本認証で使用するパスワード。
  11. Type: String
  12. Resources:
  13. LambdaEdgeBasicAuth:
  14. Type: AWS::Lambda::Function
  15. DependsOn:
  16. - SecretsManagerSecret
  17. - LambdaEdgeBasicAuthRole
  18. Properties:
  19. FunctionName: !Ref BasicAuthFuncName
  20. Runtime: python3.8
  21. MemorySize: 128 #Lambda@Edgeのクォータ最大値を設定
  22. Timeout: 5 #Lambda@Edgeのクォータ最大値を設定
  23. Role: !GetAtt LambdaEdgeBasicAuthRole.Arn
  24. Handler: index.lambda_handler
  25. Code:
  26. ZipFile: |
  27. # 基本認証を実施するLambda@Edgeの関数
  28. import json
  29. import boto3
  30. import base64
  31. # ID、パスワードを保管し、認証に使用するAWS Secrets Managerを扱うクライアントを作成
  32. asm_client = boto3.client('secretsmanager', region_name='us-east-1')
  33. # エラーの場合のレスポンスを定義
  34. err_response = {
  35. 'status': '401',
  36. 'statusDescription': 'Unauthorized',
  37. 'body': 'Authentication Failed.',
  38. 'headers': {
  39. 'www-authenticate': [
  40. {
  41. 'key': 'WWW-Authenticate',
  42. 'value': 'Basic realm="Basic Authentication"'
  43. }
  44. ]
  45. }
  46. }
  47. def lambda_handler(event, context):
  48. try:
  49. print('event:')
  50. print(event)
  51. # イベントからCloudFrontのリクエストを取得
  52. request = event['Records'][0]['cf']['request']
  53. # イベントからCloudFrontのDistribution IDを取得
  54. cf_dist_id = event['Records'][0]['cf']['config']['distributionId']
  55. # リクエストからヘッダーを取得
  56. headers = request['headers']
  57. if (headers.get('authorization') != None):
  58. # ヘッダーにauthorizationがあれば認証を試行する
  59. # headers['authorization'][0]['value']の中身は「Basic <base64でエンコードされた[ID:Password]の文字列>」
  60. # authorizationの内容を分解してID、パスワードを取り出す
  61. target_credentials_str = headers['authorization'][0]['value'].split(
  62. " ")
  63. target_credentials = base64.b64decode(
  64. target_credentials_str[1]).decode().split(":")
  65. target_id = target_credentials[0]
  66. target_pw = target_credentials[1]
  67. # 取得したCloudFrontのDistribution IDおよび基本認証で入力されたIDでシークレットを取得する
  68. response = asm_client.get_secret_value(
  69. SecretId='CloudFrontBasicAuth/' +
  70. cf_dist_id + '/' + str(target_id)
  71. )
  72. # シークレットが取得でき、格納されている文字列が基本認証で入力されたパスワードと一致すれば認証成功としてリクエストを返却する。
  73. # それ以外の場合はエラーレスポンスを返す。
  74. if (response.get('SecretString') != None):
  75. secret_string = json.loads(response['SecretString'])
  76. if (secret_string.get('Password') == target_pw):
  77. return request
  78. else:
  79. return err_response
  80. else:
  81. return err_response
  82. else:
  83. return err_response
  84. except Exception as e:
  85. print("Exception:")
  86. print(e)
  87. return err_response
  88. LambdaEdgeBasicAuthVersion: #呼出元スタックでAmazon CloudFrontと関連付けるためのLambdaEdgeバージョンを作成する
  89. Type: AWS::Lambda::Version
  90. DependsOn:
  91. - LambdaEdgeBasicAuth
  92. Properties:
  93. FunctionName: !Ref LambdaEdgeBasicAuth
  94. Description: 'Basic Auth Lambda Edge for Amazon CloudFront'
  95. LambdaEdgeBasicAuthRole: #基本認証用Lambda@Edgeに適用するIAMロールとIAMポリシーの設定
  96. Type: AWS::IAM::Role
  97. Properties:
  98. RoleName: !Sub 'IAMRole-${BasicAuthFuncName}'
  99. Path: /
  100. AssumeRolePolicyDocument:
  101. Version: 2012-10-17
  102. Statement:
  103. - Effect: Allow
  104. Principal:
  105. Service:
  106. - edgelambda.amazonaws.com
  107. - lambda.amazonaws.com
  108. Action:
  109. - sts:AssumeRole
  110. ManagedPolicyArns:
  111. - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
  112. Policies:
  113. - PolicyName: !Sub 'IAMPolicy-${BasicAuthFuncName}'
  114. PolicyDocument:
  115. Version: '2012-10-17'
  116. Statement:
  117. - Effect: Allow
  118. Action:
  119. - iam:CreateServiceLinkedRole
  120. Resource:
  121. - '*'
  122. - Effect: Allow
  123. Action:
  124. - lambda:GetFunction
  125. - lambda:EnableReplication
  126. Resource:
  127. - !Sub 'arn:aws:lambda:us-east-1:${AWS::AccountId}:function:${BasicAuthFuncName}'
  128. - Effect: Allow
  129. Action:
  130. - cloudfront:UpdateDistribution
  131. Resource:
  132. - !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistId}'
  133. - PolicyName: SecretsManagerGetSecretValue
  134. PolicyDocument:
  135. Version: '2012-10-17'
  136. Statement:
  137. - Effect: Allow
  138. Action:
  139. - secretsmanager:GetSecretValue
  140. Resource:
  141. - !Sub 'arn:aws:secretsmanager:us-east-1:${AWS::AccountId}:secret:CloudFrontBasicAuth/${CloudFrontDistId}/*'
  142. SecretsManagerSecret: #基本認証に使用するID、パスワードを格納するAWS Secrets Managerシークレットの作成
  143. Type: 'AWS::SecretsManager::Secret'
  144. Properties:
  145. Name: !Sub CloudFrontBasicAuth/${CloudFrontDistId}/${BasicAuthID}
  146. SecretString: !Sub '{"Password":"${BasicAuthPW}"}'
  147. Description: "SecretsManagerSecret of LambdaEdgeBasicAuth"
  148. Outputs:
  149. Region:
  150. Value:
  151. !Ref 'AWS::Region'
  152. LambdaFunctionArn:
  153. Value:
  154. !Ref LambdaEdgeBasicAuth #基本認証用Lambda@EdgeのARN
  155. LambdaFunctionVersionArn:
  156. Value:
  157. !Ref LambdaEdgeBasicAuthVersion #基本認証用Lambda@EdgeバージョンのARN
  158. SecretsManagerSecretArn:
  159. Value:
  160. !Ref SecretsManagerSecret #基本認証情報を格納したAWS Secrets ManagerシークレットのARN

AWS CloudFormationテンプレート(IP制限用AWS WAF)

テンプレート本体

ファイル名:WebHostCFnWAFWebACL.yml

  1. AWSTemplateFormatVersion: '2010-09-09'
  2. Description: 'CFn Template for a stack that creates AWS WAF WebACL.'
  3. Parameters:
  4. ResourcePrefix: #AWS WAFのリソースにつけるPrefix
  5. Type: String
  6. Default: IPRestrictions
  7. AllowIPList: #IP制限をするCIDR
  8. #Type: CommaDelimitedList
  9. Type: String
  10. Default: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
  11. Conditions:
  12. IsAllowIPList:
  13. !Not [!Equals [!Ref AllowIPList, ""]]
  14. Resources:
  15. WAFv2WebACL:
  16. Type: AWS::WAFv2::WebACL
  17. Properties:
  18. Name: !Sub "${ResourcePrefix}-WebACL"
  19. Scope: CLOUDFRONT
  20. DefaultAction:
  21. Block: {}
  22. VisibilityConfig:
  23. SampledRequestsEnabled: true
  24. CloudWatchMetricsEnabled: true
  25. MetricName: !Sub "${ResourcePrefix}-WebACL-Metric"
  26. Rules:
  27. - Name: !Sub "${ResourcePrefix}-WebACL-Rule"
  28. Action:
  29. Allow: {}
  30. Priority: 0
  31. Statement:
  32. IPSetReferenceStatement:
  33. Arn: !GetAtt WAFv2IPSet.Arn
  34. VisibilityConfig:
  35. SampledRequestsEnabled: true
  36. CloudWatchMetricsEnabled: true
  37. MetricName: !Sub "${ResourcePrefix}-WebACL-Rule-Metric"
  38. WAFv2IPSet:
  39. Type: AWS::WAFv2::IPSet
  40. Properties:
  41. Name: !Sub "${ResourcePrefix}-IPSet"
  42. Scope: CLOUDFRONT
  43. IPAddressVersion: IPV4
  44. Addresses: !If [IsAllowIPList, !Split [ ",", !Ref AllowIPList ], []]
  45. Outputs:
  46. Region:
  47. Value: !Ref AWS::Region
  48. WAFv2WebACLArn:
  49. Value: !GetAtt WAFv2WebACL.Arn #返却値はWAF WebACLのARN

構築手順

  1. 呼び出し元リージョンで「WebHostCFnCustomResourceToDeployAWS CloudFormationStack.yml」を使用してAWS CloudFormationスタックを作成し、AWS Lambdaカスタムリソースをデプロイする
    us-east-1リージョンに「AWS Certificate Manager(ACM)証明書」、「基本認証をするAWS Lambda@Edgeとそのバージョン」、「IP制限をするAWS WAF Web ACL」を作成するためのAWS Lambdaカスタムリソースを呼び出し元リージョンへ予めデプロイしておく。
  2. 「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」)に予め配置しておく。
  3. 呼出元AWS CloudFormationテンプレートファイル「WebHostCFnS3CloudFrontWithAcmLambdaEdgeWaf.yml 」をAWS CloudFormationで実行する
    呼出元AWS CloudFormationテンプレートをAWSマネジメントコンソールなどからパラメータを入力の上でAWS CloudFormationスタックとして実行する(CloudFrontDistributionIDは入力しない)。
  4. 静的ウェブサイトホスティングに使用するS3バケットにコンテンツを追加する
    静的ウェブサイトホスティング用S3バケット(前述のパラメータ例では「cfn-acm-edge-waf-cfnt-s3-20230311144618-dev」)にindex.htmlなどコンテンツを追加する。
    今回の例では「I will always remember that day and all of you.」とだけ表示されるindex.htmlを追加しました。
  5. 初回実行(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)のパラメータ入力例
    1. ~省略~
    2. CloudFrontDistributionID: #初回時は入力しない(Distribution IDが発行されていないので入力できない)
    3. ~省略~
  6. 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)のパラメータ入力例
    1. #2回目実行時(Update)はAmazon CloudFrontのDistribution IDを含め、すべてのパラメータを入力
    2. ~省略~
    3. CloudFrontDistributionID: XXXXXXXXXXXXX
    4. ~省略~
  7. 結果確認
    正しく実行されていればパラメータで指定した許可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」を作成したスタックをそれぞれ削除する必要があります。

  1. 呼出元AWS CloudFormationスタックのパラメータでAmazon CloudFrontのDistribution IDを空にしてUpdate処理をする
    呼出元AWS CloudFormationスタックのパラメータ「CloudFrontDistributionID」を空にしてスタックを更新する。
    (us-east-1の基本認証用Lambda@EdgeのスタックはバージョンでAmazon CloudFrontと関連付けがあるので失敗しますが、呼び出し元のスタックではAmazon CloudFrontとLambda@Edgeの関連付けが削除されます。)
  2. 呼出元AWS CloudFormationスタックを削除する
    呼出元AWS CloudFormationスタックと「基本認証をするAWS Lambda@Edgeとそのバージョン」、「IP制限をするAWS WAF Web ACL」のスタックの関連付けが解除されているため、呼出元AWS CloudFormationスタックが削除できる。
  3. 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などのデプロイ関連サービスや静的ウェブホスティングに関するアップデートについて様々なパターンを試してみたいと考えています。

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

執筆者小西秀和

Japan AWS Top Engineer, Japan AWS All Certifications Engineer(AWS認定全冠)として、知識と実践的な経験を活かし、AWSの活用に取り組んでいます。
NRIネットコムBlog: 小西 秀和: 記事一覧
Amazon.co.jp: 小西 秀和: books, biography, latest update
Personal Tech Blog | [B! Bookmark]