DEV Community

Klaus Bild
Klaus Bild

Posted on • Originally published at kbild.ch on

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:

Validate ACM certificates in Cloudformation

Custom Certificate Provider with DNS validation support

Automatic Certificate Validation with 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

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)
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)