AWS Cloud Operations 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:
Resources:
StackSet:
Type: Custom::StackSet
Properties:
ServiceToken:
Fn::ImportValue: StackSetCustomResource
StackSetName: EventDynamoDB
StackSetDescription: Deploy DynamoDB for events in dev and production
TemplateURL: https://s3.us-east-2.amazonaws.com/somebucket/events.yaml
Parameters:
- ReadCapacity: 20
- WriteCapacity: 20
Capabilities: !Ref "AWSS::NoValue"
OperationPreferences: {
"RegionOrder": , [ "us-east-2", "us-west-2" ]
"FailureToleranceCount": 0,
"MaxConcurrentCount": 3
}
Tags:
- 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:
StackInstances:
# Dev
- Regions:
- us-east-2
Accounts:
- XXXXXXXXXXXX
# Production
- Regions:
- us-east-2
- us-west-2
Accounts:
- YYYYYYYYYYYY
ParameterOverrides:
- 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.