AWS CloudFormation Templates and AWS Lambda Custom Resources for Associating AWS Certificate Manager, Lambda@Edge, and AWS WAF with a Website on Amazon S3 and Amazon CloudFront Cross-Region
First Published:
Last Updated:
Deploy AWS Cloudformation Stack Cross-Region with AWS Lambda Custom Resources
In this article, as a continuation, I use the AWS Lambda custom resources created in the previous article to associate SSL/TLS certificates (AWS Certificate Manager), basic authentication (Lambda@Edge), and IP restrictions (AWS WAF) with static website hosting using Amazon S3 and Amazon CloudFront cross-region.
* The source code published in this article and other articles by this author was developed as part of independent research and is provided 'as is' without any warranty of operability or fitness for a particular purpose. Please use it at your own risk. The code may be modified without prior notice.
Overview Diagram of the Configuration Tried in this Article
In this article, I am trying to create the configuration shown in the overview diagram in the following steps using AWS CloudFormation and custom resources. The reason for separating the processing into the initial creation and the second update is that the Amazon CloudFront Distribution ID can only be obtained after creating Amazon CloudFront, and Amazon CloudFront needs to be updated again for association after creating AWS resources that require the Distribution ID.[Initial Creation]
- Create the calling AWS CloudFormation stack with parameters without inputting the Amazon CloudFront Distribution ID
- The calling AWS CloudFormation stack creates OriginAccessControl (OAC), Amazon S3 bucket, and Amazon CloudFront distribution in the deployed region
- The calling AWS CloudFormation stack uses a custom resource to deploy the following to us-east-1 and returns the created results to the calling AWS CloudFormation stack
- AWS Certificate Manager (ACM) certificate
- The calling AWS CloudFormation stack creates resources in the calling region and associates the "AWS Certificate Manager (ACM) certificate" in the us-east-1 region with Amazon CloudFront using the return values from the custom resource
- Update the calling AWS CloudFormation stack with parameters by inputting the Amazon CloudFront Distribution ID
- The calling AWS CloudFormation stack calls a custom resource to deploy the following to us-east-1 and returns the created results to the calling AWS CloudFormation stack
- AWS WAF Web ACL for IP restrictions
- AWS Lambda@Edge and its version for basic authentication
- The calling AWS CloudFormation stack updates resources in the calling region to associate OriginAccessControl (OAC), and associates the "AWS Lambda@Edge and its version for basic authentication" and "AWS WAF Web ACL for IP restrictions" in the us-east-1 region with Amazon CloudFront using the return values from the custom resource

Setting Template Names
The AWS resources defined in the AWS CloudFormation template can have arbitrary file names as long as they can be uniquely identified and linked to the necessary ones, but for explanation purposes, the file names are set as follows.- AWS CloudFormation template that creates a static website using Amazon S3 and Amazon CloudFront in any region, and deploys SSL/TLS certificate (AWS Certificate Manager), basic authentication (Lambda@Edge), and IP restriction (AWS WAF) to us-east-1 using custom resources and associates them (calling template)
File Name:WebHostCFnS3CloudFrontWithAcmLambdaEdgeWaf.yml
- AWS CloudFormation template that creates a custom resource called from the calling template to deploy resources to us-east-1 using the AWS CloudFormation template stored in the specified Amazon S3 (custom resource creation)
File Name:WebHostCFnCustomResourceToDeployAWS CloudFormationStack.yml
- AWS CloudFormation template that is deployed from the custom resource and creates AWS Certificate Manager for SSL/TLS certificate in us-east-1 (AWS Certificate Manager for SSL/TLS certificate)
File Name:WebHostWebHostCFnACMCertificate.yml
- AWS CloudFormation template that is deployed from the custom resource and creates Lambda@Edge for basic authentication in us-east-1 (Lambda@Edge for basic authentication)
File Name:WebHostWebHostCFnBasicAuthLambdaEdge.yml
- AWS CloudFormation template that is deployed from the custom resource and creates AWS WAF for IP restriction in us-east-1 (AWS WAF for IP restriction)
File Name:WebHostCFnWAFWebACL.yml
Examples of Each File and Parameters
I will provide examples of each file and parameter.The input parameters, in particular, are examples to let you know the input format, so if you use them, you need to set each parameter according to your requirements.
AWS CloudFormation Template (for Custom Resource Creation)
For the AWS CloudFormation template for custom resource creation, please refer to the following article.Deploy AWS Cloudformation Stack Cross-Region with AWS Lambda Custom Resources
Template Body
File Name: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: |
# ## Features of this custom resource
# * Created a method that returns responses based on the custom resource specification on its own
# When called from a CloudFormation stack, cfnresponse.send can be used, but it cannot be used for standalone execution or testing of Lambda, so I created my own method called cfn_response_send.
# Reference: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-lambda-function-code-cfnresponsemodule.html
# * Enabled selecting the template to deploy from S3 or hardcoding
# The method of obtaining the template from S3 allows various templates to be executed. On the other hand, if you want to specialize in executing a specific template, you can select hardcoding to prevent other templates from being executed.
# * Simplified the specification of arguments for the CloudFormation template deployed from the custom resource
# By specifying keys not used as arguments for the CloudFormation template to deploy and passing the rest, the argument passing is made generic.
# * Adjusted to a policy where resources are created even in situations different from the requested processing
# Instead of directly using the requested Create and Update processing for CloudFormation deployment, the policy is adjusted to create resources as much as possible, such as switching to Create processing when the stack does not exist in Update processing.
# * Enabled selecting to skip processing
# It is possible to skip processing so that, for example, it can be used from Update processing while maintaining the dependency relationship in the calling side without using it in Create processing.
import urllib3
import json
import boto3
import botocore
import datetime
# import cfnresponse
SUCCESS = 'SUCCESS'
FAILED = 'FAILED'
http = urllib3.PoolManager()
# Keys that are not used as arguments for the CloudFormation stack to deploy, among the arguments received from the calling CloudFormation stack
config = {
"ExcludeKeys": ["ServiceToken", "StackName", "CfnTplS3Bucket", "CfnTplS3Key", "IsSkip"]
}
# If you want to directly describe the CloudFormation template to deploy in the code, describe it here
fixed_cfn_template = '''
'''
# Client that deploys CloudFormation stack to us-east-1 region
cfn_client = boto3.client('cloudformation', region_name='us-east-1')
# S3 resource that obtains CloudFormation template
s3_resource = boto3.resource('s3')
# Custom method that returns responses to CloudFormation according to the custom resource specification
# When called from a CloudFormation stack, cfnresponse.send can be used, but a custom method is used for standalone execution of 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')
# If 'true' is passed as IsSkip, return an empty response without any processing
# Used when skipping custom resource processing on the calling side
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:
# If the template is not directly described in the fixed_cfn_template variable, obtain the template from the object of the specified S3 bucket
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')
# In the case of Delete processing, simply delete
print('Run cfn_client.delete_stack.')
response = cfn_client.delete_stack(
StackName=stack_name
)
print('cfn_client.delete_stack response:')
print(response)
# Wait until the stack Delete processing is complete
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)
#Do not raise an exception to avoid deletion error loop between mutually associated resources.
if event['RequestType'] == 'Create' or event['RequestType'] == 'Update':
# Execution policy for Create and Update processing
# Create processing request and stack does not exist ⇒ Create processing
# Create processing request and stack exists ⇒ Do nothing
# Update processing request and stack does not exist ⇒ Create processing
# Update processing request and stack exists and there are changes ⇒ Update processing
# Update processing request and stack exists and there are no changes ⇒ Do nothing
# Flag indicating whether a stack with the same name exists
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)
# If the stack exists and describe_stacks is processed successfully, exist_stack is True
exist_stack = True
except (botocore.exceptions.ValidationError, botocore.exceptions.ClientError) as err:
err_msg = str(err)
print('err_msg:')
print(err_msg)
# If the stack does not exist and describe_stacks results in an error, exist_stack is False
if 'DescribeStack' in err_msg and 'does not exist' in err_msg:
print(('describe_stacks error: ' + err_msg))
exist_stack = False
else:
raise
# Flag indicating whether Create processing is required
need_create = True
if event['RequestType'] == 'Update':
print('Update Stacks')
if exist_stack == False:
# In the case of Update processing and the stack does not exist, create the stack with Create processing
need_create = True
else:
# Flag indicating whether Create processing is required
need_create = False
# If a stack with a matching name already exists, process with 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)
# Wait until the stack Update processing is complete
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)
# If Update processing is requested but there are no changes, do nothing
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')
# Perform stack Create processing if Create processing is requested and the stack does not exist, or if Update processing is requested and the stack does not exist
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)
# Wait until the stack Create processing is complete
waiter = cfn_client.get_waiter('stack_create_complete')
waiter.wait(StackName=stack_name)
# Obtain the created or updated stack
stacks = cfn_client.describe_stacks(StackName=stack_name)
# Obtain the contents of Outputs and format them as return values
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)))
# Return a response to the calling stack according to the CloudFormation custom resource specification
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)
# Return a response to the calling stack according to the CloudFormation custom resource specification
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 Template (Calling)
Input Parameter Examples
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-20240311144618 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
Template Body
File Name:WebHostCFnS3CloudFrontWithAcmLambdaEdgeWaf.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'CFn Template for a stack that creates ACM, Lambda@Edge, WAF, and S3+CloudFront Hosting.'
#[Order of Creation]
#ACM Certificate[US] -> OriginAccessControl[JP] -> S3 Bucket Policy (create)[JP] -> S3 Bucket[JP] -> CloudFront (create)[JP] -> Lambda@Edge (create with CloudFrontDistributionID)[US]
# -> S3 Bucket Policy (update: set OCA with CloudFrontDistributionID)[JP] -> CloudFront (update: set Lambda@Edge)[JP]
Parameters:
Env: #[Common] Environment name to add to the suffix of the S3 bucket to be created
Type: String
Default: dev
S3BucketName: #[Common] S3 bucket name to use for static website hosting
Type: String
Default: cfn-acm-edge-waf-cfnt-s3-20240311144618
S3BucketVersioningStatus: #[Common] Versioning setting for the S3 bucket used for static website hosting
Type: String
Default: Suspended
AllowedValues:
- Enabled
- Suspended
CustomResourceLambdaARN: #[Common] ARN of the AWS Lambda custom resource that deploys the AWS CloudFormation stack to us-east1 to create the SSL/TLS certificate (AWS Certificate Manager), basic authentication Lambda@Edge, and IP restriction AWS WAF
Type: String
Default: arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:LambdaCustomResourceToDeployCFnStack
ACMCFnStackName: #[For ACM] CloudFormation stack name to deploy to us-east1 to create the ACM certificate
Type: String
Default: WebHostCFnACMCertificate
ACMS3BucketNameOfStoringTemplate: #[For ACM] Bucket name where the CloudFormation template to create the ACM certificate is stored
Type: String
Default: h-o2k
ACMS3KeyOfStoringTemplate: #[For ACM] Object key after the bucket where the CloudFormation template to create the ACM certificate is stored
Type: String
Default: WebHostCFnACMCertificate.yml
ACMCustomDomainName: #[For ACM] Custom domain name to issue the ACM certificate for
Type: String
Default: cfn-acm-edge-waf-cfnt-s3.h-o2k.com
ACMHostedZoneId: #[For ACM] Route53 hosted zone ID that manages the custom domain name to issue the ACM certificate for
Type: String
Default: ZZZZZZZZZZZZZZZZZZZZZ
WAFWebACLCFnStackName: #[For WAFWebACL] CloudFormation stack name to deploy to us-east1 to create the AWS WAF WebACL
Type: String
Default: WebHostCFnWAFWebACL
WAFWebACLS3BucketNameOfStoringTemplate: #[For WAFWebACL] Bucket name where the CloudFormation template to create the AWS WAF WebACL is stored
Type: String
Default: h-o2k
WAFWebACLS3KeyOfStoringTemplate: #[For WAFWebACL] Object key after the bucket where the CloudFormation template to create the AWS WAF WebACL is stored
Type: String
Default: WebHostCFnWAFWebACL.yml
WAFWebACLResourcePrefix: #[For WAFWebACL] Prefix to add to the AWS WAF WebACL resources
Type: String
Default: WebHostIPRestrictions
WAFWebACLAllowIPList: #[For WAFWebACL] CIDR for IP restriction in AWS WAF WebACL rules
#Type: CommaDelimitedList
Type: String
Default: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
LambdaEdgeCFnStackName: #[For Lambda@Edge] CloudFormation stack name to deploy to us-east1 to create Lambda@Edge
Type: String
Default: WebHostCFnBasicAuthLambdaEdge
LambdaEdgeS3BucketNameOfStoringTemplate: #[For Lambda@Edge] Bucket name where the CloudFormation template to create Lambda@Edge is stored
Type: String
Default: h-o2k
LambdaEdgeS3KeyOfStoringTemplate: #[For Lambda@Edge] Object key after the bucket where the CloudFormation template to create Lambda@Edge is stored
Type: String
Default: WebHostCFnBasicAuthLambdaEdge.yml
LambdaEdgeBasicAuthFuncName: #[For Lambda@Edge] Function name of Lambda@Edge for basic authentication
Type: String
Default: WebHostCFnBasicAuthLambdaEdge
LambdaEdgeBasicAuthID: #[For Lambda@Edge] ID to authenticate with Lambda@Edge for basic authentication. Stored as an AWS Secrets Manager secret.
Type: String
Default: Iam
LambdaEdgeBasicAuthPW: #[For Lambda@Edge] Password to authenticate with Lambda@Edge for basic authentication. Stored as an AWS Secrets Manager secret.
Type: String
Default: Nobody
CloudFrontCachePolicyName: #[Common] Amazon CloudFront cache policy specification. Selected from managed policies.
Type: String
Default: CachingDisabled
AllowedValues:
- NONE
- Amplify
- CachingDisabled
- CachingOptimized
- CachingOptimizedForUncompressedObjects
- Elemental-MediaPackage
CloudFrontOriginRequestPolicyName: #[Common] Amazon CloudFront origin request policy. Selected from managed policies.
Type: String
Default: NONE
AllowedValues:
- NONE
- AllViewer
- AllViewerAndCloudFrontHeaders-2022-06
- AllViewerExceptHostHeader
- CORS-CustomOrigin
- CORS-S3Origin
- Elemental-MediaTailor-PersonalizedManifests
- UserAgentRefererHeaders
CloudFrontResponseHeaderPolicyName: #[Common] Amazon CloudFront response header policy. Selected from managed policies.
Type: String
Default: CORS-with-preflight-and-SecurityHeadersPolicy
AllowedValues:
- NONE
- SimpleCORS
- CORS-With-Preflight
- SecurityHeadersPolicy
- CORS-and-SecurityHeadersPolicy
- CORS-with-preflight-and-SecurityHeadersPolicy
CloudFrontDistributionID: #[Common] Amazon CloudFront Distribution ID used for associating each resource.
#Used for Amazon S3 bucket policy OriginAccessControl association, unique identification of AWS Secrets Manager secret used by Lambda@Edge, and AWS WAF association.
Type: String #* Input during the second Update process after creating Amazon CloudFront with the initial Create process of the stack.
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: #If the input parameter contains the Amazon CloudFront Distribution ID, create Lambda@Edge as True, otherwise skip Lambda@Edge creation as False.
!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
#Route 53 record set that registers CloudFront as an alias record in the Route 53 hosted zone
Route53RecordSetGroup:
Type: AWS::Route53::RecordSetGroup
DependsOn:
- CloudFrontDistribution
Properties:
HostedZoneId:
!Ref ACMHostedZoneId
RecordSets:
- Name: !Ref ACMCustomDomainName
Type: A
#When registering CloudFront as an alias record, the HostedZoneId of the alias target becomes the following fixed value
#Reference: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/quickref-route53.html
AliasTarget:
HostedZoneId: Z2FDTNDATAQYW2
DNSName: !GetAtt CloudFrontDistribution.DomainName
#Custom resource called to issue the ACM certificate
CustomResourceForACM:
Type: Custom::CustomResourceForACM
Properties:
ServiceToken: !Ref CustomResourceLambdaARN
StackName: !Ref ACMCFnStackName
CfnTplS3Bucket: !Ref ACMS3BucketNameOfStoringTemplate
CfnTplS3Key: !Ref ACMS3KeyOfStoringTemplate
CustomDomainName: !Ref ACMCustomDomainName
HostedZoneId: !Ref ACMHostedZoneId
#Custom resource called to create 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: #* If the input parameter does not contain the Amazon CloudFront Distribution ID, send IsSkip:true to the custom resource and skip the AWS WAF WebACL creation process.
!If [IsCloudFrontDistributionID, "false", "true"]
#Custom resource called to create 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: #* If the input parameter does not contain the Amazon CloudFront Distribution ID, send IsSkip:true to the custom resource and skip the Lambda@Edge creation process.
!If [IsCloudFrontDistributionID, "false", "true"]
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
DependsOn:
#Add the custom resource to DependsOn so that CloudFront is associated after OriginAccessControl is created
- OriginAccessControl
#Add the custom resource to DependsOn so that CloudFront is associated after the Amazon S3 bucket is created
- S3Bucket
#Add the custom resource to DependsOn so that CloudFront is associated after the ACM certificate is issued by the custom resource
- CustomResourceForACM
#Add the custom resource to DependsOn so that CloudFront is associated after the AWS WAF WebACL is created by the custom resource
- CustomResourceForWAFWebACL
#Add the custom resource to DependsOn so that CloudFront is associated after the basic authentication Lambda@Edge version is created by the custom resource
- CustomResourceForLambdaEdge
Properties:
DistributionConfig:
#Set the domain name for which the ACM certificate was issued as the CloudFront alias
Aliases:
- !Ref ACMCustomDomainName
#Set the ACM certificate created by the custom resource to CloudFront
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 #Specify the origin as the target
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 Template (AWS Certificate Manager for SSL/TLS Certificate)
Template Body
File Name:WebHostWebHostCFnACMCertificate.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: "CFn Template for a stack that creates AWS CertificateManager Certificate."
Parameters:
CustomDomainName: #Custom domain name to issue the ACM certificate for
Type: String
HostedZoneId: #Amazon Route53 hosted zone ID that manages the custom domain to issue the ACM certificate for
Type: String
Resources:
CertificateManagerCertificate:
Type: AWS::CertificateManager::Certificate
Properties:
DomainName: !Ref CustomDomainName
DomainValidationOptions:
- DomainName: !Ref CustomDomainName
HostedZoneId: !Ref HostedZoneId
ValidationMethod: DNS #Perform custom domain validation using the Route53 DNS method
Outputs:
Region:
Value: !Ref AWS::Region
AcmCertificateArn:
Value: !Ref CertificateManagerCertificate #Returned value is the ARN of the ACM certificate
AWS CloudFormation Template (Lambda@Edge for Basic Authentication)
Template Body
File Name:WebHostWebHostCFnBasicAuthLambdaEdge.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'CFn Template for a stack that creates Lambda@Edge Version and AWS Secrets Manager Secret.'
Parameters:
CloudFrontDistId: #Amazon CloudFront Distribution ID that calls the basic authentication Lambda@Edge. Used for AWS Secrets Manager secret ID.
Type: String #* Note that this CloudFormation template does not associate Amazon CloudFront with the Lambda@Edge version. The association is assumed to be done in the calling template.
BasicAuthFuncName: #Function name of Lambda@Edge for basic authentication
Type: String
BasicAuthID: #ID stored in AWS Secrets Manager secret and used for basic authentication.
Type: String
BasicAuthPW: #Password stored in AWS Secrets Manager secret and used for basic authentication.
Type: String
Resources:
LambdaEdgeBasicAuth:
Type: AWS::Lambda::Function
DependsOn:
- SecretsManagerSecret
- LambdaEdgeBasicAuthRole
Properties:
FunctionName: !Ref BasicAuthFuncName
Runtime: python3.8
MemorySize: 128 #Set the Lambda@Edge quota maximum value
Timeout: 5 #Set the Lambda@Edge quota maximum value
Role: !GetAtt LambdaEdgeBasicAuthRole.Arn
Handler: index.lambda_handler
Code:
ZipFile: |
# Lambda@Edge function that performs basic authentication
import json
import boto3
import base64
# Create a client to handle AWS Secrets Manager, which stores the ID and password used for authentication
asm_client = boto3.client('secretsmanager', region_name='us-east-1')
# Define the response for errors
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)
# Obtain the CloudFront request from the event
request = event['Records'][0]['cf']['request']
# Obtain the CloudFront Distribution ID from the event
cf_dist_id = event['Records'][0]['cf']['config']['distributionId']
# Obtain the headers from the request
headers = request['headers']
if (headers.get('authorization') != None):
# If the headers contain authorization, attempt authentication
# The content of headers['authorization'][0]['value'] is "Basic "
# Decode the authorization content to extract the ID and password
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]
# Obtain the secret using the obtained CloudFront Distribution ID and ID entered for basic authentication
response = asm_client.get_secret_value(
SecretId='CloudFrontBasicAuth/' +
cf_dist_id + '/' + str(target_id)
)
# If the secret can be obtained and the stored string matches the password entered for basic authentication, consider authentication successful and return the request.
# Otherwise, return an error response.
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: #Create a LambdaEdge version to associate with Amazon CloudFront in the calling stack
Type: AWS::Lambda::Version
DependsOn:
- LambdaEdgeBasicAuth
Properties:
FunctionName: !Ref LambdaEdgeBasicAuth
Description: 'Basic Auth Lambda Edge for Amazon CloudFront'
LambdaEdgeBasicAuthRole: #IAM role and IAM policy settings applied to the basic authentication Lambda@Edge
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: #Create an AWS Secrets Manager secret that stores the ID and password used for basic authentication
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 #ARN of the basic authentication Lambda@Edge
LambdaFunctionVersionArn:
Value:
!Ref LambdaEdgeBasicAuthVersion #ARN of the basic authentication Lambda@Edge version
SecretsManagerSecretArn:
Value:
!Ref SecretsManagerSecret #ARN of the AWS Secrets Manager secret that stores the basic authentication information
AWS CloudFormation Template (AWS WAF for IP Restriction)
Template Body
File Name:WebHostCFnWAFWebACL.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'CFn Template for a stack that creates AWS WAF WebACL.'
Parameters:
ResourcePrefix: #Prefix added to AWS WAF resources
Type: String
Default: IPRestrictions
AllowIPList: #CIDR for IP restriction
#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 #Returned value is the ARN of the WAF WebACL
Build Procedure
-
Create an AWS CloudFormation stack in the calling region using "WebHostCFnCustomResourceToDeployAWS CloudFormationStack.yml" and deploy the AWS Lambda custom resource
Pre-deploy the AWS Lambda custom resource that creates the AWS CloudFormation stack to deploy the "AWS Certificate Manager (ACM) certificate", "AWS Lambda@Edge and its version for basic authentication", and "AWS WAF Web ACL for IP restriction" to us-east-1 in the calling region. -
Place the AWS CloudFormation template files that create the "AWS Certificate Manager (ACM) certificate", "AWS Lambda@Edge and its version for basic authentication", and "AWS WAF Web ACL for IP restriction" in any Amazon S3 bucket
Pre-place the "WebHostCFnACMCertificate.yml", "WebHostCFnBasicAuthLambdaEdge.yml", and "WebHostCFnWAFWebACL.yml" AWS CloudFormation templates to create in the Amazon S3 bucket (the "h-o2k" bucket in the above parameter example). -
Execute the calling AWS CloudFormation template file "WebHostCFnS3CloudFrontWithAcmLambdaEdgeWaf.yml" with AWS CloudFormation
Execute the calling AWS CloudFormation template as an AWS CloudFormation stack by inputting parameters from the AWS Management Console, etc. (do not input CloudFrontDistributionID). -
Add content to the S3 bucket used for static website hosting
Add content such as index.html to the S3 bucket for static website hosting (the "cfn-acm-edge-waf-cfnt-s3-20240311144618-dev" bucket in the above parameter example).
In this example, I added an index.html that simply displays "I will always remember that day and all of you."
-
Initial execution (Amazon CloudFront is created with Create process and Distribution ID is issued. Creation of "AWS Lambda@Edge and its version for basic authentication" and "AWS WAF Web ACL for IP restriction" by custom resources is not executed.)
In the initial execution of the calling AWS CloudFormation stack, by not inputting the Amazon CloudFront Distribution ID in "CloudFrontDistributionID", the creation process of the AWS CloudFormation stacks that create "AWS Lambda@Edge and its version for basic authentication" and "AWS WAF Web ACL for IP restriction" is skipped, and other resources are created.
[Example parameter input for initial execution (Create)]
--omitted-- CloudFrontDistributionID: #Do not input for the initial execution (cannot input because the Distribution ID has not been issued) --omitted--
-
Second execution (Update process to pass the Amazon CloudFront Distribution ID to the custom resource and create "AWS Lambda@Edge and its version for basic authentication" and "AWS WAF Web ACL for IP restriction".)
In the second execution of the calling AWS CloudFormation stack, by inputting the Amazon CloudFront Distribution ID issued in the initial execution in "CloudFrontDistributionID", the creation process of the AWS CloudFormation stacks that create "AWS Lambda@Edge and its version for basic authentication" and "AWS WAF Web ACL for IP restriction" is executed, and these resources are associated with the calling Amazon CloudFront.
[Example parameter input for second execution (Update)]
#For the second execution (Update), input all parameters including the Amazon CloudFront Distribution ID --omitted-- CloudFrontDistributionID: XXXXXXXXXXXXX --omitted--
-
Result confirmation
If executed correctly, accessing the domain for which the ACM certificate was issued via https from the allowed IP address specified in the parameter displays the basic authentication dialog, and authentication passes with the ID and password for basic authentication specified in the parameter.
Access from IP addresses other than the allowed ones will be denied with "403 ERROR".
If it does not work correctly, identify the cause and fix the issue from the event contents of the calling AWS CloudFormation stack, the event contents of the stack deployed by the custom resource, the contents of AWS Lambda custom resource Amazon CloudWatch Logs, and AWS CloudTrail logs.
Deletion Procedure
AWS CloudFormation does not control the order of removing the association between Lambda@Edge, AWS WAF Web ACL, and Amazon CloudFront, so all stacks cannot be deleted at once.Therefore, when deleting, it is necessary to remove the association as shown in the following procedure, and then delete the calling AWS CloudFormation stack and the stacks that created "AWS Lambda@Edge and its version for basic authentication" and "AWS WAF Web ACL for IP restriction" separately.
-
Update the calling AWS CloudFormation stack by setting the Amazon CloudFront Distribution ID parameter to empty
Update the stack by setting the parameter "CloudFrontDistributionID" of the calling AWS CloudFormation stack to empty.
(The stack for the basic authentication Lambda@Edge in us-east-1 will fail because the version is associated with Amazon CloudFront, but the association between Amazon CloudFront and Lambda@Edge will be removed in the calling stack.)
-
Delete the calling AWS CloudFormation stack
The calling AWS CloudFormation stack can be deleted because the association with the stacks for "AWS Lambda@Edge and its version for basic authentication" and "AWS WAF Web ACL for IP restriction" has been removed.
-
Delete the AWS CloudFormation stack that created the basic authentication Lambda@Edge version in us-east-1
Since the association between the calling AWS CloudFormation stack and the basic authentication Lambda@Edge stack has been removed, delete the AWS CloudFormation stack that created the basic authentication Lambda@Edge version individually.
References:
Tech Blog with curated related content
Route 53 template snippets - AWS CloudFormation
cfn-response module - AWS CloudFormation
Summary
In this article, I introduced an example of using the AWS Lambda custom resource to deploy AWS CloudFormation stacks cross-region, which was explained in the article "Deploy AWS Cloudformation Stack Cross-Region with AWS Lambda Custom Resources", to create SSL/TLS certificate (AWS Certificate Manager), basic authentication (Lambda@Edge), and IP restriction (AWS WAF) each in us-east-1 and associate them with resources created in the calling AWS CloudFormation stack.In the future, I would like to continue trying various patterns regarding deployment-related services such as AWS CloudFormation, AWS Amplify, AWS CDK, and updates related to static web hosting.
Written by Hidekazu Konishi