AWS DevOps & Developer Productivity Blog
Continuous Delivery of Nested AWS CloudFormation Stacks Using AWS CodePipeline
In CodePipeline Update – Build Continuous Delivery Workflows for CloudFormation Stacks, Jeff Barr discusses infrastructure as code and how to use AWS CodePipeline for continuous delivery. In this blog post, I discuss the continuous delivery of nested CloudFormation stacks using AWS CodePipeline, with AWS CodeCommit as the source repository and AWS CodeBuild as a build and testing tool. I deploy the stacks using CloudFormation change sets following a manual approval process.
Here’s how to do it:
In AWS CodePipeline, create a pipeline with four stages:
- Source (AWS CodeCommit)
- Build and Test (AWS CodeBuild and AWS CloudFormation)
- Staging (AWS CloudFormation and manual approval)
- Production (AWS CloudFormation and manual approval)
Pipeline stages, the actions in each stage, and transitions between stages are shown in the following diagram.
CloudFormation templates, test scripts, and the build specification are stored in AWS CodeCommit repositories. These files are used in the Source stage of the pipeline in AWS CodePipeline.
The AWS::CloudFormation::Stack resource type is used to create child stacks from a master stack. The CloudFormation stack resource requires the templates of the child stacks to be stored in the S3 bucket. The location of the template file is provided as a URL in the properties section of the resource definition.
The following template creates three child stacks:
- Security (IAM, security groups).
- Database (an RDS instance).
- Web stacks (EC2 instances in an Auto Scaling group, elastic load balancer).
Description: Master stack which creates all required nested stacks
Parameters:
TemplatePath:
Type: String
Description: S3Bucket Path where the templates are stored
VPCID:
Type: "AWS::EC2::VPC::Id"
Description: Enter a valid VPC Id
PrivateSubnet1:
Type: "AWS::EC2::Subnet::Id"
Description: Enter a valid SubnetId of private subnet in AZ1
PrivateSubnet2:
Type: "AWS::EC2::Subnet::Id"
Description: Enter a valid SubnetId of private subnet in AZ2
PublicSubnet1:
Type: "AWS::EC2::Subnet::Id"
Description: Enter a valid SubnetId of public subnet in AZ1
PublicSubnet2:
Type: "AWS::EC2::Subnet::Id"
Description: Enter a valid SubnetId of public subnet in AZ2
S3BucketName:
Type: String
Description: Name of the S3 bucket to allow access to the Web Server IAM Role.
KeyPair:
Type: "AWS::EC2::KeyPair::KeyName"
Description: Enter a valid KeyPair Name
AMIId:
Type: "AWS::EC2::Image::Id"
Description: Enter a valid AMI ID to launch the instance
WebInstanceType:
Type: String
Description: Enter one of the possible instance type for web server
AllowedValues:
- t2.large
- m4.large
- m4.xlarge
- c4.large
WebMinSize:
Type: String
Description: Minimum number of instances in auto scaling group
WebMaxSize:
Type: String
Description: Maximum number of instances in auto scaling group
DBSubnetGroup:
Type: String
Description: Enter a valid DB Subnet Group
DBUsername:
Type: String
Description: Enter a valid Database master username
MinLength: 1
MaxLength: 16
AllowedPattern: "[a-zA-Z][a-zA-Z0-9]*"
DBPassword:
Type: String
Description: Enter a valid Database master password
NoEcho: true
MinLength: 1
MaxLength: 41
AllowedPattern: "[a-zA-Z0-9]*"
DBInstanceType:
Type: String
Description: Enter one of the possible instance type for database
AllowedValues:
- db.t2.micro
- db.t2.small
- db.t2.medium
- db.t2.large
Environment:
Type: String
Description: Select the appropriate environment
AllowedValues:
- dev
- test
- uat
- prod
Resources:
SecurityStack:
Type: "AWS::CloudFormation::Stack"
Properties:
TemplateURL:
Fn::Sub: "https://s3.amazonaws.com/${TemplatePath}/security-stack.yml"
Parameters:
S3BucketName:
Ref: S3BucketName
VPCID:
Ref: VPCID
Environment:
Ref: Environment
Tags:
- Key: Name
Value: SecurityStack
DatabaseStack:
Type: "AWS::CloudFormation::Stack"
Properties:
TemplateURL:
Fn::Sub: "https://s3.amazonaws.com/${TemplatePath}/database-stack.yml"
Parameters:
DBSubnetGroup:
Ref: DBSubnetGroup
DBUsername:
Ref: DBUsername
DBPassword:
Ref: DBPassword
DBServerSecurityGroup:
Fn::GetAtt: SecurityStack.Outputs.DBServerSG
DBInstanceType:
Ref: DBInstanceType
Environment:
Ref: Environment
Tags:
- Key: Name
Value: DatabaseStack
ServerStack:
Type: "AWS::CloudFormation::Stack"
Properties:
TemplateURL:
Fn::Sub: "https://s3.amazonaws.com/${TemplatePath}/server-stack.yml"
Parameters:
VPCID:
Ref: VPCID
PrivateSubnet1:
Ref: PrivateSubnet1
PrivateSubnet2:
Ref: PrivateSubnet2
PublicSubnet1:
Ref: PublicSubnet1
PublicSubnet2:
Ref: PublicSubnet2
KeyPair:
Ref: KeyPair
AMIId:
Ref: AMIId
WebSG:
Fn::GetAtt: SecurityStack.Outputs.WebSG
ELBSG:
Fn::GetAtt: SecurityStack.Outputs.ELBSG
DBClientSG:
Fn::GetAtt: SecurityStack.Outputs.DBClientSG
WebIAMProfile:
Fn::GetAtt: SecurityStack.Outputs.WebIAMProfile
WebInstanceType:
Ref: WebInstanceType
WebMinSize:
Ref: WebMinSize
WebMaxSize:
Ref: WebMaxSize
Environment:
Ref: Environment
Tags:
- Key: Name
Value: ServerStack
Outputs:
WebELBURL:
Description: "URL endpoint of web ELB"
Value:
Fn::GetAtt: ServerStack.Outputs.WebELBURL
During the Validate stage, AWS CodeBuild checks for changes to the AWS CodeCommit source repositories. It uses the ValidateTemplate API to validate the CloudFormation template and copies the child templates and configuration files to the appropriate location in the S3 bucket.
The following AWS CodeBuild build specification validates the CloudFormation templates listed under the TEMPLATE_FILES environment variable and copies them to the S3 bucket specified in the TEMPLATE_BUCKET environment variable in the AWS CodeBuild project. Optionally, you can use the TEMPLATE_PREFIX environment variable to specify a path inside the bucket. This updates the configuration files to use the location of the child template files. The location of the template files is provided as a parameter to the master stack.
version: 0.1
environment_variables:
plaintext:
CHILD_TEMPLATES: |
security-stack.yml
server-stack.yml
database-stack.yml
TEMPLATE_FILES: |
master-stack.yml
security-stack.yml
server-stack.yml
database-stack.yml
CONFIG_FILES: |
config-prod.json
config-test.json
config-uat.json
phases:
install:
commands:
npm install jsonlint -g
pre_build:
commands:
- echo "Validating CFN templates"
- |
for cfn_template in $TEMPLATE_FILES; do
echo "Validating CloudFormation template file $cfn_template"
aws cloudformation validate-template --template-body file://$cfn_template
done
- |
for conf in $CONFIG_FILES; do
echo "Validating CFN parameters config file $conf"
jsonlint -q $conf
done
build:
commands:
- echo "Copying child stack templates to S3"
- |
for child_template in $CHILD_TEMPLATES; do
if [ "X$TEMPLATE_PREFIX" = "X" ]; then
aws s3 cp "$child_template" "s3://$TEMPLATE_BUCKET/$child_template"
else
aws s3 cp "$child_template" "s3://$TEMPLATE_BUCKET/$TEMPLATE_PREFIX/$child_template"
fi
done
- echo "Updating template configurtion files to use the appropriate values"
- |
for conf in $CONFIG_FILES; do
if [ "X$TEMPLATE_PREFIX" = "X" ]; then
echo "Replacing \"TEMPLATE_PATH_PLACEHOLDER\" for \"$TEMPLATE_BUCKET\" in $conf"
sed -i -e "s/TEMPLATE_PATH_PLACEHOLDER/$TEMPLATE_BUCKET/" $conf
else
echo "Replacing \"TEMPLATE_PATH_PLACEHOLDER\" for \"$TEMPLATE_BUCKET/$TEMPLATE_PREFIX\" in $conf"
sed -i -e "s/TEMPLATE_PATH_PLACEHOLDER/$TEMPLATE_BUCKET\/$TEMPLATE_PREFIX/" $conf
fi
done
artifacts:
files:
- master-stack.yml
- config-*.json
After the template files are copied to S3, CloudFormation creates a test stack and triggers AWS CodeBuild as a test action.
Then the AWS CodeBuild build specification executes validate-env.py
, the Python script used to determine whether resources created using the nested CloudFormation stacks conform to the specifications provided in the CONFIG_FILE.
version: 0.1
environment_variables:
plaintext:
CONFIG_FILE: env-details.yml
phases:
install:
commands:
- pip install --upgrade pip
- pip install boto3 --upgrade
- pip install pyyaml --upgrade
- pip install yamllint --upgrade
pre_build:
commands:
- echo "Validating config file $CONFIG_FILE"
- yamllint $CONFIG_FILE
build:
commands:
- echo "Validating resources..."
- python validate-env.py
- exit $?
Upon successful completion of the test action, CloudFormation deletes the test stack and proceeds to the UAT stage in the pipeline.
During this stage, CloudFormation creates a change set against the UAT stack and then executes the change set. This updates the UAT environment and makes it available for acceptance testing. The process continues to a manual approval action. After the QA team validates the UAT environment and provides an approval, the process moves to the Production stage in the pipeline.
During this stage, CloudFormation creates a change set for the nested production stack and the process continues to a manual approval step. Upon approval (usually by a designated executive), the change set is executed and the production deployment is completed.
Setting up a continuous delivery pipeline
I used a CloudFormation template to set up my continuous delivery pipeline. The codepipeline-cfn-codebuild.yml template, available from GitHub, sets up a full-featured pipeline.
When I use the template to create my pipeline, I specify the following:
- AWS CodeCommit repositories.
- SNS topics to send approval notifications.
- S3 bucket name where the artifacts will be stored.
The CFNTemplateRepoName points to the AWS CodeCommit repository where CloudFormation templates, configuration files, and build specification files are stored.
My repo contains following files:
The continuous delivery pipeline is ready just seconds after clicking Create Stack. After it’s created, the pipeline executes each stage. Upon manual approvals for the UAT and Production stages, the pipeline successfully enables continuous delivery.
Implementing a change in nested stack
To make changes to a child stack in a nested stack (for example, to update a parameter value or add or change resources), update the master stack. The changes must be made in the appropriate template or configuration files and then checked in to the AWS CodeCommit repository. This triggers the following deployment process:
Conclusion
In this post, I showed how you can use AWS CodePipeline, AWS CloudFormation, AWS CodeBuild, and a manual approval process to create a continuous delivery pipeline for both infrastructure as code and application deployment.
For more information about AWS CodePipeline, see the AWS CodePipeline documentation. You can get started in just a few clicks. All CloudFormation templates, AWS CodeBuild build specification files, and the Python script that performs the validation are available in codepipeline-nested-cfn GitHub repository.
About the author
Prakash Palanisamy is a Solutions Architect for Amazon Web Services. When he is not working on Serverless, DevOps or Alexa, he will be solving problems in Project Euler. He also enjoys watching educational documentaries.