AWS Cloud Operations & Migrations Blog

Building an AWS CloudFormation custom resource to manage StackSets

In this blog post I’d like to share an AWS CloudFormation custom resource I’ve written that allows you to deploy StackSets from within a CloudFormation template. You can use StackSets to deploy and manage CloudFormation stacks in multiple accounts and multiple AWS Regions from a central location using a single template and set of operations.

Custom resources are a powerful tool for extending the infrastructure deployment capabilities of CloudFormation. They consist of a resource definition to include in your template and an AWS Lambda function to respond to create, update, and delete actions associated with that resource. For the StackSets custom resource implementation, I chose to write my function in Python to take advantage of the Custom Resource Helper framework available on the AWSLabs GitHub repository. This framework provides basic function definitions for the various actions, allowing me to focus on writing the SDK calls and other code specific to my resource.

The code for my custom resource is open source and available on GitHub if you’d like to download it and follow along.

The resource definition

The resource definition is how template developers interact with your resource within CloudFormation templates. You construct the definition using JSON or YAML and it is passed to the Lambda function defined as an ARN within the ServiceToken property of the resource.

I’ve modeled the StackSet as a single resource that includes both the StackSet itself and the stack instances it is managing, so I need to include properties for both. Looking at the CreateStackSet and CreateStackInstances API actions, I can see the inputs I’ll need for my API calls. For the StackSet, I’ll need a unique name and description, any capability flags required to deploy into the target accounts, the location of the target template, and the list of parameter values and tags to pass into it.

Here’s an example of how you use the StackSet resource in a template:

    Type: Custom::StackSet
        Fn::ImportValue: StackSetCustomResource
      StackSetName: EventDynamoDB
      StackSetDescription: Deploy DynamoDB for events in dev and production
        - ReadCapacity: 20
        - WriteCapacity: 20
      Capabilities: !Ref "AWSS::NoValue"
      OperationPreferences: {
        "RegionOrder": , [ "us-east-2", "us-west-2" ]
        "FailureToleranceCount": 0,
        "MaxConcurrentCount": 3
        - Creator: Chuck

This resource definition supports many of the StackSet features including specifying IAM capabilities for stack instances, setting operational preferences, and tagging. After defining the properties for the StackSet itself, you define one or more stack instances as a combination of accounts, Regions, and optional parameter overrides in an array:

        # Dev
        - Regions:
            - us-east-2
            - XXXXXXXXXXXX
        # Production
        - Regions:
            - us-east-2
            - us-west-2
            - YYYYYYYYYYYY
            - ReadCapacity: 50
            - WriteCapacity: 50

The code

As I mentioned previously, the Lambda function is written in Python and uses the Custom Resource Helper framework from AWSLabs as well as the AWS SDK for Python (Boto3) to interact with the CloudFormation service. Every Lambda function needs a handler function defined as an entry point. Fortunately, the Custom Resource Helper provides a pre-baked handler:

def handler(event, context):
    # update the logger with event info
    global logger
    logger = crhelper.log_config(event)
    return crhelper.cfn_handler(event, context, create, update, delete,
                                 logger, init_failed)

This handler examines event information received from CloudFormation and then routes to the create(), update(), or delete() functions defined in the code. So, the next step is to fill out these function definitions with the appropriate API calls.

  • The create() function validates that we have all the properties we need from the resource definition. Then it makes API calls to create the StackSet and stack instances.
  • The update() function includes logic to evaluate which properties have changed and whether those changes affect the StackSet, the stack instances, or both. Helper functions flatten the matrix of accounts and Regions to determine which stack instances should be added or removed.
  • The delete() function first deletes all stack instances associated with the StackSet, then the set itself.

Because the code is executed within a Lambda function, the custom resource only has five minutes to make all the necessary API calls — a limitation I’ve come up against on occasion. In the future I’d like to move the code to a framework that supports orchestrating across multiple Lambda function calls, perhaps by using a different custom resource framework.

Next steps

The StackSet custom resource is available now in the AWSLabs repository on GitHub. The code includes a CloudFormation template to deploy the custom resource and instructions in the README. If you want to help improve this resource, feel free to submit issues, feedback, and pull requests via the repository.

Now that the code has been released, there are several opportunities for improvement:

To orchestrate changes to complex sets of stack instances, we’ll need to allow stack actions to span multiple Lambda function calls, passing along event information and monitoring status of the backend API calls. There are still gaps in functionality, including the ability to specify that certain stacks are retained if the StackSet is deleted.

About the Author

Chuck Meyer is a Senior Developer Advocate for AWS CloudFormation based in Ohio.  He spends his time working with both external and internal development teams to constantly improve the developer experience for CloudFormation users.  He’s a live music true believer and spends as much time as possible playing bass and watching bands.