AWS Step Functions as CloudFormation Custom Resources - Automatic Certificate Creation Across AWS Accounts
Published: 2020-07-24 11:41:24

---
title: AWS Step Functions as CloudFormation Custom Resources - Automatic Certificate Creation Across AWS Accounts
published: true
date: 2020-07-24 11:00:00 UTC
tags: 
canonical_url: https://kbild.ch/blog/2020-6-10-custom_resource_certificate_creation/
---

Last year I wrote a CloudFormation example which deployed a [CodePipeline for the Hugo CMS](https://kbild.ch/blog/2019-02-25-pipeline_cloudformation/).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](https://aws.amazon.com/blogs/security/how-to-use-aws-certificate-manager-with-aws-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:

[Validate ACM certificates in Cloudformation](https://www.cloudar.be/awsblog/validate-acm-certificates-in-cloudformation)

[Custom Certificate Provider with DNS validation support](https://github.com/binxio/cfn-certificate-provider)

[Automatic Certificate Validation with Certificate Validator](https://www.dwolla.com/updates/certificate-validator/)

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](https://kbild.ch/202006/CF_Custom_Resource.png)](https://kbild.ch/202006/CF_Custom_Resource.png)

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](https://github.com/kbild/AWS_Cloudformation_Examples/tree/master/stepFunctionCertCreation) Repo.

The Custom Resource including **CertificateStateMachine** Step Functions with all Lambda Functions and Roles/Policies can be found in the [certificate\_xaccount\_customresource.yaml](https://github.com/kbild/AWS_Cloudformation_Examples/blob/master/stepFunctionCertCreation/certificate_xaccount_customresource.yaml)  
CloudFormation template.

* * *

### CertificateStateMachine

Most of the work is done by the Step Functions called **CertificateStateMachine** :

[![CertificateStateMachine](https://kbild.ch/202006/stepfunctions_graph.png)](https://kbild.ch/202006/stepfunctions_graph.png)

**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](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-lambda-function-code-cfnresponsemodule.html) 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](https://github.com/kbild/AWS_Cloudformation_Examples/blob/master/stepFunctionCertCreation/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](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-lambda-function-code-cfnresponsemodule.html) module.If the Step Functions can’t be triggered we will return an error (line 264) through the [cfn-response](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-lambda-function-code-cfnresponsemodule.html) 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](https://github.com/kbild/AWS_Cloudformation_Examples/blob/master/stepFunctionCertCreation/deploycert.yaml).

* * *

## 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](https://kbild.ch/202006/stepfunctions_graph2.png)](https://kbild.ch/202006/stepfunctions_graph2.png)

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](https://github.com/kbild/AWS_Cloudformation_Examples/blob/master/stepFunctionCertCreation/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.