AWS Step Functions as CloudFormation Custom Resources - Automatic Certificate Creation Across AWS Accounts
Last year I wrote a CloudFormation example which deployed a CodePipeline for the Hugo CMS. This was an almost fully automated solution for a Hugo deployment, the only manual step was to create the needed certificate with the Amazon Certificate Manager.
Some weeks ago AWS added the possibility of fully automated certificate creation via CloudFormation if you add the HostedZoneId to your CloudFormation certificate resource.
This solution is neat but will not work on our company accounts because we have all Route 53 DNS Zones in a different AWS account.
Therefore I needed a solution which works fully automated across different AWS accounts.
Searching for examples gave me a good starting point to create my own solution, a custom CloudFormation resource.
Here are some examples which will help you to understand custom CloudFormation resources:
All these examples work perfectly and can be easily modified to work across different AWS accounts but the fact that they use long running Lambda Functions didn’t satisfy me.
Occasionally executing long running Lambda Functions doesn’t cost much but nevertheless I always prefer short running ones and to use AWS Step Functions for the Workflow logic combined with Lambda Functions with a single purpose.
Challenge accepted, let’s create a CloudFormation Custom Resource which will work with Step Functions.
Unfortunately only Lambda Functions or SNS topics may be used as Custom Resource in CloudFormation, so we first have to create a Lambda Function which can be used as Custom Resource in CloudFormation and which interconnects CloudFormation with our Step Functions.
We have 3 components:
-
CustomResourceCertificate → The custom resource in the CloudFormation template
-
LambdaCallStateMachine → The Lambda Function which will be triggered by the CloudFormation custom resource and which will call the Step Functions
-
CertificateStateMachine → The actual Step Functions which consists of some logic and Lambda Functions
You will find all the examples explained below in this AWS_Cloudformation_Examples Github Repo.
The Custom Resource including CertificateStateMachine Step Functions with all Lambda Functions and Roles/Policies can be found in the certificate_xaccount_customresource.yaml
CloudFormation template.
CertificateStateMachine
Most of the work is done by the Step Functions called CertificateStateMachine:
CertificateStateMachine starts with a
Choice Action
and if you look at the CloudFormation template which creates this Step Functions you see that the variable $.RequestType
is used as switch. This variable is sent by AWS CloudFormation and will give us the information if this is a Create
, Update
or Delete
request.
Create or Update Path
Following the Create
or Update
path will first call the
Create
step which triggers a Lamba Function called LambdaCreateCertificateRequest. As the name suggests this simple Function calls ACM and requests to create a certificate. We use the parameters HostedZoneId
, WebSiteURL
and Region
which we will get from CustomResourceCertificate whenever this Custom Resource is used in a CloudFormation template
→ find more details later in this post
As response we will get the CertificateArn
which we will need in the next steps.
After
Wait_10_seconds
another Lambda Function LambdaDescribeCertificateRequest is called in step
DescribeCert
. This function takes the CertificateArn
as input and calls ACM again to get the needed DNS CNAME entries
for the validation and the ValidationStatus
.
CreateDNS
triggers LambdaCreateDNSEntry Lambda Function and takes the CNAME entries as input. Here the magic for the cross account creation happens. The Lambda Function will use the ARN of the Role which is created in our Route 53 Domain AWS Account and will call Route 53 to create the DNS Record Set.
CheckCert
will again use LambdaDescribeCertificateRequest to get the ValidationStatus of the Cert creation.
Cert Ready?
will loop using
Wait_100_seconds_for_certificate
until the ValidationStatus
equals SUCCESS
Last Step
SendResultCreation
calls the Lambda Function LambdaSendResult. This Function returns a success response to AWS CloudFormation via the cfn-response module. This module knows the AWS CloudFormation Endpoint for the response through the variable responseUrl
. This variable is provided when AWS CloudFormation calls the ServiceToken of the Custom Resource.
The CertificateArn
is used as physicalResourceId
for the Custom Resource, so this will be the Return value of the Custom Resource.
Delete Path
The Delete
Path starts with the Lambda Function LambdaDescribeCertificateRequest in step
DescribeCertDeletion
. In contrast to the use of the same Function in the Create Path the CertificateArn is provided via the variable PhysicalResourceId
. The response includes the DNS CNAME entries
which were used for the validation.
Delete
step will call the Lambda Function LambdaDeleteResource. This Function will first delete the DNS Entries in our Route 53 Domain AWS Account, again done via assuming a Role in this Account. Second the Function deletes the according Certificate in ACM.
Last Step
SendResultDeletion
calls the Lambda Function LambdaSendResult and returns a success response to AWS CloudFormation equal to the use in the Create Path.
LambdaCallStateMachine
Next let’s look at the LambdaCallStateMachine Python Function. The Function can as well be found in the CloudFormation template certificate_xaccount_customresource.yaml.
246 from botocore.exceptions import ClientError
247 import boto3
248 import cfnresponse
249 import os
250 import json
251
252 statemachineARN = os.getenv('statemachineARN')
253
254 def lambda_handler(event, context):
255 sfn_client = boto3.client('stepfunctions')
256 try:
257 response = sfn_client.start_execution(stateMachineArn=statemachineARN,input=(json.dumps(event)))
258 sfn_arn = response.get('executionArn')
259 print(sfn_arn)
260 except Exception:
261 print('Could not run the Step Functions')
262 responseData = {}
263 responseData['Error'] = "CouldNotCallStateMachine"
264 response=cfnresponse.send(event, context, FAILED, responseData)
265 return(response)
266 return(sfn_arn)
As you can see on line 252 we will get the ARN of the CertificateStateMachine Step Functions (aka statemachineARN) as environment variable.
This ARN will be automatically filled with the correct ARN of the CertificateStateMachine Step Functions during CloudFormation deployment (Line 242 → statemachineARN : !Ref CertificateStateMachine).
In line 257 we call the Step Functions and provide the Lambda Function input event unchanged as json string to the Step Functions. This event input will be provided by AWS CloudFormation during Custom Resource Creation/Update/Deletion.
This is an example what you can expect in such an input event:
{
"StackId": "arn:aws:cloudformation:eu-central-1:700000000000:stack/cloudeecms/6g300000-cc00-00ea-aaba-0a0f000aced0",
"ResponseURL": "https://cloudformation-custom-resource-response-eucentral1.s3.eu-central-1.amazonaws.com/arn%3Aaws%3Acloudformation%3Aeu-central-1%3A711632663682%3Astack/cloudeecms/6f371890-cc16-11ea-bbab-0a3f741aced4%7CCustomResourceCertificate%7C4dfd25c6-43c4-4a38-97f5-c14845f454ee?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20200722T122539Z&X-Amz-SignedHeaders=host&X-Amz-Expires=7200&X-Amz-Credential=BLGBZZHSTLS2MMALHGQI%3G30400000%2Feu-central-1%2Fs3%2Faws4_request&X-Amz-Signature=3c2424f204c3e935024046g3fd28ld42hs04hd2b1ad2jegw25ls1f924hsf2lsr",
"ResourceProperties": {
"HostedZoneId": "Z00000000AZD0FWVZH0RA",
"WebSiteURL": "www.cloudee-cms.biz",
"Region": "us-east-1",
"ServiceToken": "arn:aws:lambda:eu-central-1:700000000000:function:CallStateMachine-700000000000"
},
"RequestType": "Create",
"ServiceToken": "arn:aws:lambda:eu-central-1:700000000000:function:CallStateMachine-700000000000",
"ResourceType": "Custom::CreateCertificate",
"RequestId": "5ehq93f9-28d2-9d20-53g5-d63926g294dw",
"LogicalResourceId": "CustomResourceCertificate"
}
If everything works as expected the Lambda will be terminated and the Step Functions will take care of returning a response to the cfn-response module.
If the Step Functions can’t be triggered we will return an error (line 264) through the cfn-response module.
CustomResourceCertificate
A custom resource in CloudFormation is defined by a Type
starting with 'Custom::'
and the custom resource name, here 'CreateCertificate'
.
The resource must have a ServiceToken
. This token represents the ARN of the Lambda Function or SNS Topic which should be called. In this case we import the ARN of the Lambda Function LambdaCallStateMachine
(which was already created by the certificate_xaccount_customresource.yaml CloudFormation template).
This custom resource needs 3 additional properties, the WebSiteURL
for which the certificate should be created, the HostedZoneId
of the Route 53 domain in which the needed DNS entry for validation will be created and the Region
where the certificate should be created.
We already saw these 3 properties inside the Step Functions where LambdaCreateCertificateRequest is called.
37 CustomResourceCertificate:
38 Type: 'Custom::CreateCertificate'
39 Properties:
40 ServiceToken: !ImportValue LambdaCallStateMachineCertArn
41 WebSiteURL: !Ref WebSiteURL
42 HostedZoneId: !Ref HostedZoneId
43 Region: !Ref Region
You can find the full version of the CloudFormation template which creates the certificate here.
Outlook
I tried to write the Lambda Functions generic so that they can be reused in other Step Functions. This gives us the freedom to use them in a second Step Functions example called DNSStateMachine. These Step Functions will be used for a Custom CloudFormation Resource which creates DNS entries in our Route 53 Domain AWS Account.
As you can see we reuse the same Lambda Functions LambdaCreateDNSEntry, LambdaDeleteResource and LambdaSendResult. You find the DNSStateMachine example in the CloudFormation template certificate_xaccount_customresource.yaml as well.
Summary
This example showed how you can combine a Custom CloudFormation Resource with Step Functions and automatically create an ACM certificate even if the Route 53 Domain for validation is in another AWS account. This gives you an idea how you can start using Step Functions for your own CloudFormation resources.