Spotlight
Guidance for Technical Leadership
A brief exploration of evidence-based approaches to Technical Leadership and Performance Evaluations.
Lately, I've seen some discussion around CloudFormation and some of its limitations as well as organizational practices.
CloudFormation nested stacks offer elegant solutions to a lot of these challenges. However, don't just take my word for it.
pssshhh nested stacks are the best. its internal guidance as well as external
— chrismunns (@chrismunns) July 8, 2019
Understanding all the tricks and features available to you, however, is an adventure in spelunking the proverbial Mines of Moria that is AWS documentation.
The first thing to realize is that nested stacks are treated just like any other resource in a CloudFormation template. There is nothing unique or uncomfortable about this situation. A stack is simply a resource to be created and managed like any other resource you would be managing.
# example template resources for a template stack
Resources:
MyNestedStack:
Type: AWS::CloudFormation::Stack # required
Properties:
NotificationARNs: # not required
- String
Parameters: # conditionally required if the nested stack requires the parameters
Key : Value
Tags: # not required
- Tag
TemplateURL: String # required
TimeoutInMinutes: Integer # not required
Typically when I start a project, I create a "root" stack. This root stack exists solely to define the other stacks that exist within my infrastructure.
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
Stage:
Type: String
Resources:
DataStores:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: shared/datastores/template.yaml
Parameters:
Stage: !Ref Stage
InternalJobs:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: internal/jobs/template.yaml
Parameters:
Stage: !Ref Stage
PublicAPI:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: public/api/template.yaml
Parameters:
Stage: !Ref Stage
This top level template.yaml
is supported by a directory tree of the following.
.
├── internal
│ └── jobs
│ └── template.yaml
├── public
│ └── api
│ └── template.yaml
├── shared
│ └── datastores
│ └── template.yaml
└── template.yaml
Finally, for things to go smoothly, we need to embrace the AWS CLI to do all the heavy lifting for us when it comes to managing a template and its lifecycle. aws cloudformation package
and aws cloudformation deploy
are going to be our core commands for this post.
I have found that embracing the native AWS way of doing things may take some upfront effort, but my ROI is realized within hours of working on the project. You don't want to roll your own cloudformation packaging. If that's the only thing you take away from this post, let it be that.
You'll note that TemplateURL
is a file path above. aws cloudformation package
manages the process walking a tree of nested stacks and uploading all necessary assets to S3
and rewriting the designated locations in an output template.
After a quick aws cloudformation package --template-file template.yaml --output-template packaged.yaml --s3-bucket {your-deployment-s3-bucket}
on the root template, you'll get output to packaged.yaml
that reflects a new "packaged" template with all necessary assets uploaded to your deployment s3 bucket.
If you find yourself working with CloudFormation a lot, especially if you are already using VS Code, check out this post by Matthew Hodgkins. It introduces some great tools, like linting on your templates. Getting little red squiggles under your mistyped resource names is the difference between finishing a project and debating a career change.
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
Stage:
Type: String
Resources:
DataStores:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: https://s3.amazonaws.com/serverless-deployment-454679818906-us-east-2/71243994a4224bb9eb149de44022813a.template
Parameters:
Stage:
Ref: Stage
InternalJobs:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: https://s3.amazonaws.com/serverless-deployment-454679818906-us-east-2/71243994a4224bb9eb149de44022813a.template
Parameters:
Stage:
Ref: Stage
PublicAPI:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: https://s3.amazonaws.com/serverless-deployment-454679818906-us-east-2/71243994a4224bb9eb149de44022813a.template
Parameters:
Stage:
Ref: Stage
These few tricks would let us pretty easily manage multiple stacks and split up resources logically. We don't have to worry about the 200 resource limit, we can make it easier to work with our templates, but what about dependencies between resources?
How do we let AWS Lambda functions in one stack know about, and be delegated permissions to a DynamoDB table or S3 bucket?
Let's talk about CloudFormation Outputs
and References
. Let's say we have a DynamoDB table that we want to use in our Lambda function. Both in our API and internal compute jobs.
We create the table and tell CloudFormation to make the dynamic table name and ARN available as outputs.
It is almost always best practice to allow CloudFormation to name your resources. in the long run, it makes it easier to manage and maintain your stack, as well as have many stacks in a single account/region without collisions.
# shared/datastores/template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
Stage:
Type: String
Resources:
HelloTable:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions:
- AttributeName: id
AttributeType: S
BillingMode: PAY_PER_REQUEST
KeySchema:
- AttributeName: id
KeyType: HASH
Outputs:
HelloTable:
Value: !Ref HelloTable
HelloTableArn:
Value: !GetAtt HelloTable.Arn
Next, we create an AWS Lambda function using the serverless transform that has access to the DynamoDB table and also passes an environment variable with the table name.
You may note we don't have code in here; rather it points to a file relative to the location of the template. This code gets packaged up and put on S3 for us as part of the single cloudformation package
command on the root stack.
If you are curious about what other magic lies in the cloudformation package
command, check out the docs!
# internal/jobs/template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Parameters:
Stage:
Type: String
HelloTable:
Type: String
MinLength: 1
HelloTableArn:
Type: String
MinLength: 1
Resources:
HelloLambda:
Type: AWS::Serverless::Function
Properties:
Handler: handler.py
Runtime: python3.7
CodeUri: src/
Policies:
- AWSLambdaExecute
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:BatchGetItem
- dynamodb:GetItem
- dynamodb:Query
- dynamodb:Scan
- dynamodb:BatchWriteItem
- dynamodb:PutItem
- dynamodb:UpdateItem
Resource: !Ref HelloTableArn
Environment:
Variables:
TABLE_NAME: !Ref HelloTable
Events:
PIIScan:
Type: Schedule
Properties:
Schedule: rate(1 day)
The tree, including AWS Lambda functions and their source code, may look something like the following.
.
├── internal
│ └── jobs
│ ├── src
│ │ └── handler.py
│ └── template.yaml
├── packaged.yaml
├── public
│ └── api
│ ├── src
│ │ └── handler.py
│ └── template.yaml
├── shared
│ └── datastores
│ └── template.yaml
└── template.yaml
Wiring up these parameters uses something we are quite familiar with, the GetAtt
function. On our root level stack, we add in a couple of new Parameters on our InternalJobs
stack.
# Top level stack metadata...
Resources:
DataStores:
# stack info / resource config
InternalJobs:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: internal/jobs/template.yaml
Parameters:
Stage: !Ref Stage
# ------------- ADDED LINES -------------
HelloTable: !GetAtt DataStores.Outputs.HelloTable
HelloTableArn: !GetAtt DataStores.Outputs.HelloTableArn
# ------------- ADDED LINES -------------
# stack continues...
Now, if our DynamoDB table changes in name or ARN for any reason, our jobs stack references dynamically updates based on those incoming parameters. Development iterations are faster, and correctness is more naturally achieved.
The CloudFormation import / export functionality should only be relied on in the case you are exposing values for other services entirely to depend on. Realize that once an export has been "imported" by another stack, it cannot ever change its value.
A common reason for exports is service discoverability. Instead, take a look at using either SSM Parameters or AWS Cloud Map.
Let's say you have a canonical way of using s3 buckets within your organization. Its configuration sets up all sorts of lifecycle policies, policies, encryption, etc. Your organization creates and provides this template org.s3.template.yaml
.
Your team wants 3 buckets in your project. You grab the organizations template, and in your root template, you can use this same template multiple times using parameters for different behavior with CloudFormation conditions in the s3 template allowing a simplified control interface of the template.
# Top level stack metadata...
Resources:
ProcessStore:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: org.s3.template.yaml
UploadsStore:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: org.s3.template.yaml
Parameters:
AllowPublicUploads: true
AssetStore:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: org.s3.template.yaml
Parameters:
IncludePublicCDN: true
Once your stack is packaged
we need to move to deployment. The deployment should reference your output packaged.yaml
template.
aws cloudformation deploy --region us-east-2 --template-file packaged.yaml --stack-name blog-post --parameter-overrides Stage=demo
When deploying things that using template transforms or things that create IAM roles/policies you likely need to including --capabilities CAPABILITY_AUTO_EXPAND CAPABILITY_IAM
to your command acknowledging you are aware of the activities taking place.
Security best practices dictate that you use --role-arn
to delegate an IAM Role to the CloudFormation service to run your stack as, rather than running as your credentials issuing the deployment. Learn more about using the service role in the AWS docs.
The CloudFormation changesets feature becomes pretty useless. You can't use it to understand what is going to change in the child stacks. While it is sub-optimal, if you rely on changesets for knowing what is going to happen, I would recommend instead transitioning to cleaner git merges and reviewing source control as an oracle of truth (everything is code defined? right?) rather than the changesets. You still want to make sure you have a solid promotional process (dev -> prod) in place, and make judicious use of stack policies to prevent your data from disappearing.
Drift Detection is still possible, but you need to enumerate over every child stack requesting a drift detection and gathering them to understand what has changed.
Getting into and recovering from bad states can be a little harder to understand. AWS docs, once again, have details worth examining.
If you are working with nested stacks, or anything remotely related to serverless, check out our little community project over at https://serverless.help. We'd love to help guide you in the right direction!
A brief exploration of evidence-based approaches to Technical Leadership and Performance Evaluations.