AWS CloudFormation provides the functionality to write infrastructure as a code. This comes with a lot of advantages like disaster recovery, the ability to do code review and many more. There are cases where the CloudFormation natively does not support some of the functionality. In these cases, we need Custom CloudFormation resources:

When we need extra output option which is not provided by default CloudFormation.

Need extra parameters that are supported by CLI but not present in CloudFormation.

Newly launched AWS service mostly do not have CloudFormation support

In these kinds of cases, we will need the ability to create AWS CloudFormation resources according to our needs.

In this guide, we will be developing a Custom CloudFormation resource using AWS GO SDK.

Custom Cloudformation template

Defining a custom CloudFormation resource in a template is very simple:

// customS3Resource.yaml AWSTemplateFormatVersion: '2010-09-09' Description: S3 bucket resource using custom resource Resources: CustomS3Bucket: Type: Custom::CustomResource Version : 1.0 Properties: ServiceToken: Fn::ImportValue: Customs3Resource BucketName: "MyBucket" Outputs: BucketLocation: Value: !GetAtt CustomS3Bucket.Location Export: Name: S3BucketLocation

In the above template, the resource type needs to be defined Custom::CustomResource. The version is to specify the version of the template. Inside Properties, ServiceToken is a minimal property needed for a Custom Cloudformation template. This defines which lambda function to trigger when the resource is created/updated or deleted.

In the above template, we are creating an S3 bucket using a custom CloudFormation resource. We can define any number of properties. In this example, we will be only using a BucketName as the property. In the ServiceToken we are using the import function to link it to a lambda function. This lambda function does not exist yet, so let us create one and export it in another stack.

Writing a Lambda function

A lambda function gets triggered when the stack above is created or modified. We need to create one with the logic to create the s3 bucket.

We will be using AWS SDK for Golang(Go) to create the lambda function. You can use any other programming language supported by AWS for writing lambda.

package main import ( "context" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-lambda-go/cfn" ) func main() { lambda.Start(cfn.LambdaWrap(handler)) } func handler(ctx context.Context, event cfn.Event) (physicalResourceID string, data map[string]interface{}, err error) { bucketName := event.ResourceProperties["BucketName"].(string) input := CreateBucketInput{Bucket: bucketName} output, err := CreateBucket(input)r if err != nil { return bucketName, map[string]interface{}{}, err } data := map[string]interface{}{Location: output.Location} return bucketName, data, nil }

The lambda above when triggered after the CloudFormation stack is created will create a bucket with the name, provided in the properties of the template.

The lambda needs to notify the CloudFormation about the status of the resource creation. It could succeed/fail during creation. Here we are using a package, github.com/aws/aws-lambda-go/cfn provided by AWS SDK for Go(Golang) for notifying about the resource creation status. A custom Cloudformation lambda trigger needs to return 3 arguments when we are using the cfn package:

Physical Resource ID – The physical unique id of the resource that we are creating. In the above example, we are creating an s3 bucket. The unique identifier of a bucket is the bucket name. So we are returning the BucketName above. Data – The data defined the output that can be used by the template to export. We are able to add !GetAtt CustomS3Bucket.Location in the template above because we have added Location in the output. Error – If any error this will cause the CloudFormation to fail and rollback if needed.

Once this is done deploy the lambda function. You can use sam to deploy the lambda. Make sure you output the lambda function with name Customs3Resource so that it can be used by the above template.

Once the lambda is deployed, create the CloudFormation stack defined in the above template:

aws cloudformation create-stack\ --stack-name CustomCloudformationExample\ --template-body file://customS3Resource.yaml

Update/Delete the custom resource

The above example creates a resource. We also need a mechanism to update and delete the resource when modifying the stack.

The event argument in the handler contains detail of the request type. A request type can be Create, Update or Delete. Based on this resource type we can update/delete the resource. Let us add some logic to handle them in the lambda:

package main import ( "context" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-lambda-go/cfn" ) func main() { lambda.Start(cfn.LambdaWrap(handler)) } func handler(ctx context.Context, event cfn.Event) (physicalResourceID string, data map[string]interface{}, err error) { if event.RequestType == "Create" { bucketName := event.ResourceProperties["BucketName"].(string) input := CreateBucketInput{Bucket: bucketName} output, err := CreateBucket(input)r if err != nil { return bucketName, map[string]interface{}{}, err } data := map[string]interface{}{Location: output.Location} return bucketName, data, nil } if event.RequestType == "Update" { bucketName := event.PhysicalResourceID // TODO: Add logic to update the s3 bucket } if event.RequestType == "Delete" { bucketName := event.PhysicalResourceID // TODO: Add logic to delete the s3 bucket } }

Update the lambda function. Make changes to your stack and update the CloudFormation stack.

With this, we will be able to create/update or delete a resource with our custom logic. AWS Cloudformation is a powerful tool to write IaC. Custom Cloudformation helps to extend the functionality of the resources based on our needs.

Learn more about AWS CloudFormation here: