AWS Step Functions as CloudFormation Custom Resources - Automatic Certificate Creation Across AWS Accounts

- aws customresource cloudformation acm stepfunctions

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.

SolutionOverview


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


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.

DNSStateMachine


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.