CloudFormation Custom Resources - Why and When

CloudFormation Custom Resources - Why and When

All good solutions start with a problem :). Imagine you'd like to deploy an application as a CloudFormation stack and part of its infrastructure includes s3 for storage, some contents of which you'd like to be replicated to another region. The storage part of the application might look something like this:

S3 bucket with replication to a backup

The question arises of how to deploy this via infrastructure-as-code, as close to a 1-click/push button deploy as possible. One might try the below steps:

  1. Provision the bucket and the necessary KMS keys in the secondary region
  2. Extract their ARNs from the CF output.
  3. Run another CF deploy into the primary region providing the above ARNs as parameters.

Whilst this approach would get you the end result, it doesn’t easily let you build robustness into the system and although logically your resources are one unit, you haven’t deployed them as such.

How do we get round these issues? Ultimately we want to deploy all the resources as part of one logical Cloudformation template but resources in multiple regions can only be deployed as StackSets using Cloudformation. StackSets don’t help us in this circumstance because they deploy resources in a cookie-cutter fashion across the regions, i.e. everything is the same and there cannot be dependencies between regions (which is what we need here).

AWS Stacksets. Source: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacksets-concepts.html

Enter in Lambda-backed custom resources! These basically allow you execute any code as part of the CloudFormation lifecycle of a custom type of resource that you define. Extremely powerful. I’ll refer to other great articles like this for further detail. What does our application from before look like with this in place?

S3 backup bucket deployed via a Custom Resource Provider lambda function

How about we stare at some code then? In the interests of keeping this post short, I'll focus on just the S3 part of the custom resource provision.

In the long-winded attempt at doing this, deploying then copying again, this is what out backup bucket could have looked like.

  MyBackupBucket:
    Type: "AWS::S3::Bucket"
    Properties:
      BucketName: "my-super-unique-backup-thingy"
      AccessControl: "Private"
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: "AES256"

Now, using a custom resource provider, to deploy this it would look like:

  MyBackupBucket:
    Type: Custom::CrossRegionReplicationBucket
    Properties:
      ServiceToken: !GetAtt CRRDestinationBucketProvider.Arn
      CRRDestinationBucketName: "my-super-unique-backup-thingy"
      CRRDestinationBucketRegion: "YOUR_DESIRED_BACKUP_REGION"

Ok, so the Type attribute is self-explanatory, but what's the ServiceToken? Well, this is the Arn of the Lambda that will actually deploy the desired custom resource, in this case a bucket in another region, for us. Unless you're planning to use a lamba/other provider that is shared by lots of stacks outside of yours, I'd recommend deploying this as part of the same template so we'd have some thing like this:

  CRRDestinationBucketProvider:
    Type: AWS::Lambda::Function
    Properties:
      Runtime: nodejs12.x
      Role: #some lambda role with permissions for s3 operations
      Handler: index.handler
      Code:
        ZipFile: |

          const {       
               S3Client,
               CreateBucketCommand,
               DeleteBucketCommand,
          } = require('@aws-sdk/client-s3')

          var response = require('cfn-response')

          exports.handler = async function(event, context) {
              console.log("REQUEST RECEIVED:\n" + JSON.stringify(event))

              const s3client = new S3Client({region: event.ResourceProperties.CRRDestinationBucketRegion});

              if (event.RequestType == "Delete") {
                  const request = new DeleteBucketCommand({
Bucket:event.ResourceProperties.CRRDestinationBucketName
                  });

                  const deleteResponseData = await s3client.send(request);
                  response.send(event, context, "SUCCESS")
                  return
              }

              if (event.RequestType == "Create") {
                  const request = new CreateBucketCommand({
    Bucket:event.ResourceProperties.CRRDestinationBucketName,
    CreateBucketConfiguration: {
          LocationConstraint: 
          event.ResourceProperties.CRRDestinationBucketRegion,
    },
                  });

                  const createResponseData = await s3client.send(request);
                  response.send(event, context, "SUCCESS",  {BucketArn: `arn:aws:s3:::${event.ResourceProperties.CRRDestinationBucketName}`}, event.ResourceProperties.CRRDestinationBucketName)
                  return
              } 

              // include code to handle `Update` events here

          }
      Description: Custom resource provider for s3 buckets in other regions.
      TracingConfig:
        Mode: Active

The above code is neither perfect not prod-ready but it does give a good idea of what's necessary to start using custom resources to solve a common type of problem faced when building resilient architectures compliant with certain industry standards (common in areas like Healthcare or Finance).

From the above example, you can see the bucket name is sent back as the physical resource id and the Arn is sent as part of the CloudFormation event response data. Setting the Arn as part of the response data allows this, and any other attributes you'd like, to be retrieved for an instance of your custom resource by using the !GetAtt command. In this case, we'd have something like !GetAtt MyBackupBucket.BucketArn.

Finally it's worth noting that there are other solutions to this problem outside CloudFormation, for example Terraform. However, if you need a simple solution that doesn't involve adding more tooling to your tech stack, CloudFormation Custom Resources might just be the thing for you.

Have fun.

P.S. If you're thinking developing and maintaining Custom Resources is challenging/you would like to reuse great resources that others have developed, AWS have now developed and released a Cloudformation Registry. This will allow you to reuse Custom Resources developed by first and third-parties in the community. Very cool!