Build a CI/CD Pipeline to Improve Your IaC with AWS CloudFormation
A walk-through of how to create a CI/CD pipeline from scratch using Amazon CodeCatalyst, to deploy your Infrastructure as Code (IaC) with AWS CloudFormation.
About | |
---|---|
✅ AWS experience | 200 - Intermediate |
⏱ Time to complete | 30 minutes |
💰 Cost to complete | Free tier eligible |
🧩 Prerequisites | - AWS Account - CodeCatalyst Account - AWS CloudFormation basic understanding |
💻 Code Sample | Code sample used in tutorial on GitHub |
📢 Feedback | Any feedback, issues, or just a 👍 / 👎 ? |
⏰ Last Updated | 2023-03-22 |
- Stack name
CodeCatalyst-IAM-roles
- Region: any (preferred
us-west-2
) - Download the template provided here and upload this in CloudFormation console -> Create new stack -> Template -> Upload a template.
main_branch_IAM_role
and pr_branch_IAM_role
in your AWS account.Please note, themain_branch_IAM_role
provides full access to AWS resources in EC2 and CloudFormation services, as those will be used by sample CloudFormation template mentioned in this blog. Please use this role carefully and delete it when not required.
Create Space
on the CodeCatalyst Dashboard, add a name (we will use CloudFormation CodeCatalyst
), and add the AWS Account ID to link for billing. Follow the prompts to link your AWS Account with CodeCatalyst.012345678901
is a placeholder; replace it with your own account ID. You can find your account ID in the top right of your AWS Console. Before you can create the space, follow the Verify in the AWS console
link and complete the verification. You should get green tick to proceed!AWS Accounts
tab, click on your account ID, and then click on Manage roles from the AWS Management Console
.Add IAM role to Amazon CodeCatalyst space
. In the dialog, select the option Add an existing role you have created in IAM
and select the main_branch_IAM_role
from the dropdown. Click Add role
. Follow the same steps for the pr_branch_IAM_role
.Create Project
button, select Start from scratch
, and give your project a name - we will use ThreeTierApp
.Code
in the left-side navigation menu, then select Source repositories
, Add repository
, and choose Create repository
. Set a repository name ( we will use 3-tier-app
in this tutorial), add a description and none
for the .gitignore file:CI/CD
, Environments
, and click on Create Environment
. In the Environment details enter following:- Environment name:
PreProdEnv
- Environment type:
Non-production
- Description:
Pre-production environment to learn, test and experiment with CloudFormation
- AWS account connection
Connection
: Select the AWS account ID that will be used in the workflows to deploy this environment
- AWS Cloud9
- Visual Studio Code
- JetBrains IDEs
- IntelliJ IDEA Ultimate
- GoLand
- PyCharm Professional
AWS Cloud9
for our development environment.Code
, Dev Environments
, and click on Create Dev Environment
. In the Create dev environment and open with AWS Cloud9
dialog box select following:- Repository:
Clone a repository
- Repository: Select the repo that you want to clone. We created a repo
3-tier-app1
above, so select same. - Branch:
Work in existing branch
- Alias:
bootstrap
Create
and it will open a new Cloud9 dev environment. It will take a minute or two, so grab a coffee before we start the CloudFormation magic.3-tier-app
, with a readme.md
, devfile.yaml
, and some hidden folders. The devfile.yaml
, contains the definition to build your application libraries and toolchain. You can ignore it for the purpose of this tutorial.In the real world, you would deploy the networking infrastructure and application deployment in separate CloudFormation nested stacks. Nested stacks allow more regularly needed changes to the application stack to be applied without running the networking stack every time, which saves time. Likewise, the networking team can make changes independently without running the application stack. This approach also keeps the template body size within CloudFormation Quotas. Since this deployment with CodeCatalyst is smaller and simpler -- and we don't have to worry about long-term operations -- let's deploy everything in a single template.
1
2
3
4
5
6
7
8
9
10
11
12
# go to the root folder of the repo and run following
$ cd 3-tier-app/
$ wget https://raw.githubusercontent.com/build-on-aws/ci-cd-iac-aws-cloudformation/main/cloudformation-templates/VPC_AutoScaling_With_Public_IPs.json
# check git status
$ git status
# add changed files, commit and push to the git repo
$ git add . -A
$ git commit -m "Uploading first CloudFormation template"
$ git push
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ git status
On branch main
Your branch is up to date with 'origin/main'.
Untracked files:
(use "git add <file>..." to include in what will be committed)
VPC_AutoScaling_With_Public_IPs.json
nothing added to commit but untracked files present (use "git add" to track)
$ git add .
$ git commit -m "Uploading first CloudFormation template"
[main eca3764] Uploading first CloudFormation template
1 file changed, 560 insertions(+)
create mode 100644 VPC_AutoScaling_With_Public_IPs.json
$ git push
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 2 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 4.59 KiB | 4.59 MiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
remote: Validating objects: 100%
To https://git.us-west-2.codecatalyst.aws/v1/CloudFormation-CodeCatalyst/ThreeTierApp/3-tier-app
cbbfab7..eca3764 main -> main
Code
in the left-side navigation menu, then select Source repositories
. A new file VPC_AutoScaling_With_Public_IPs.json
should be added to the repo.- Create Workflow using Code Catalyst Console in Visual drag-drop method [navigate to CI/CD -> Workflows]
- Create Workflow using the dev environment using yaml [the method we are following in the blog]
main_branch.yaml
file:1
2
3
4
# go to the root folder of the repo and run following
mkdir -p .codecatalyst/workflows
touch .codecatalyst/workflows/main_branch.yaml
.codecatalyst/workflows/main_branch.yaml
in your IDE, and add the following. Remember to:- replace the placeholder AWS account ID
123456789012
with the value of your account, and the IAM role namemain_branch_IAM_role
, if you changed it - if you are using your own CloudFormation template, replace the CloudFormation filename in
template
- main_branch.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Name: Main_Branch_Workflow
SchemaVersion: "1.0"
# Optional - Set automatic triggers.
Triggers:
- Type: Push
Branches:
- main
# Required - Define action configurations.
Actions:
DeployAWSCloudFormationstack_7c:
Identifier: aws/cfn-deploy@v1
Configuration:
parameter-overrides: SSHLocation=54.10.10.2/32,WebServerInstanceType=t2.micro
capabilities: CAPABILITY_IAM,CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND
template: VPC_AutoScaling_With_Public_IPs.json
region: us-west-2
name: PreProdEnvStack
Timeout: 10
Environment:
Connections:
- Role: main_branch_IAM_role
Name: "123456789012"
Name: PreProdEnv
Inputs:
Sources:
- WorkflowSource
1
2
3
$ git add . -A
$ git commit -m "Adding main branch workflow"
$ git push
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ git add . -A
$ git commit -m "Adding main branch workflow"
[main 5677f67] Adding main branch workflow
1 file changed, 28 insertions(+)
create mode 100644 .codecatalyst/workflows/main_branch.yaml
$ git push
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 2 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (5/5), 911 bytes | 455.00 KiB/s, done.
Total 5 (delta 0), reused 0 (delta 0), pack-reused 0
remote: Validating objects: 100%
To https://git.us-west-2.codecatalyst.aws/v1/CloudFormation-CodeCatalyst/ThreeTierApp/3-tier-app
eca3764..5677f67 main -> main
CI/CD
-> Workflows
page. You should see the workflow running:Variables
tab.It might happen that the run fails due to errors. Read through the logs to understand any issue and remediate it. If you get internal error, at this point it is good to check the stack deployment in the CloudFormation dashboard of AWS Management Console. If the CloudFormation stack has rolled back, then delete the stack manually, fix the issues, and run the workflow again. You can find help in Documentation for Troubleshooting, Premium Support center or AWS re:Post.
main_workflow
that we created earlier will automatically merge the code and deploy changes to PreProdEnv
, my non-prod environment (step7). You might have different branching strategies and more tests for your production environment deployment.pr_branch.yaml
in the hidden .codecatalyst/workflows/
directory and paste the following.- replace the placeholder AWS account ID
123456789012
with the value of your account, and the IAM role namemain_branch_IAM_role
, if you changed it - if you are using your own CloudFormation template, replace the CloudFormation filename in
template
- pr_branch.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
Name: PR_Branch_Workflow
SchemaVersion: "1.0"
# Optional - Set automatic triggers.
Triggers:
- Type: PULLREQUEST
Branches:
- main
Events:
- OPEN
- REVISION
# Required - Define action configurations.
Actions:
Super-Linter_0d:
# Identifies the action. Do not modify this value.
Identifier: aws/github-actions-runner@v1
# Specifies the source and/or artifacts to pass to the action as input.
Inputs:
# Optional
Sources:
- WorkflowSource # This specifies that the action requires this Workflow as a source
# Defines the action's properties.
Configuration:
# Required - Steps are sequential instructions that run shell commands
# Action URL: https://github.com/marketplace/actions/super-linter
# Please visit the action URL to look for examples on the action usage.
# Be aware that a new version of the action could be available on GitHub.
Steps:
- name: Lint Code Base
uses: github/super-linter@v4
env:
VALIDATE_CLOUDFORMATION: "true"
CreateChangeSet:
Identifier: aws/cfn-deploy@v1
DependsOn:
- Super-Linter_0d
Configuration:
parameter-overrides: SSHLocation=54.10.10.2/32,WebServerInstanceType=t2.micro
capabilities: CAPABILITY_IAM,CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND
no-execute-changeset: "1"
template: VPC_AutoScaling_With_Public_IPs.json
region: us-west-2
name: PreProdEnvStack
Timeout: 10
Environment:
Connections:
- Role: pr_branch_IAM_role
Name: "123456789012"
Name: PreProdEnv
Inputs:
Sources:
- WorkflowSource
- In the
Super-Linter_0d
action, we are definingVALIDATE_CLOUDFORMATION: "true"
environment variable. This ensures that our CloudFormation template is validated using thecfn-lint
github action. cfn-lint is an open source tool that helps validate AWS CloudFormation yaml/json templates against the AWS CloudFormation Resource Specification and additional checks. This includes checking valid values for resource properties and best practices. - In the
CreateChangeSet
action, theno-execute-changeset: "1"
option in the workflow below, it indicates whether to run the change set or have it reviewed. Default is'0'
, which means it will run the change set. We don't want it to execute the changes; we just want to see the changes that will happen if PR is merged, hence we set it to'1'
which means we do not execute the ChangeSet. - In the
CreateChangeSet
action, we have added a new propertyDependsOn: - Super-Linter_0d
. This tells the workflow to first runSuper-Linter_0d
action, and if it is successful, only then run theCreateChangeSet
action.
1
2
3
$ git add . -A
$ git commit -m "Adding PR branch workflow"
$ git push
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ git add . -A
$ git commit -m "Adding PR branch workflow"
[main e844b70] Adding PR branch workflow
1 file changed, 1 insertion(+), 1 deletion(-)
$ git push
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 2 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (5/5), 451 bytes | 225.00 KiB/s, done.
Total 5 (delta 2), reused 0 (delta 0), pack-reused 0
remote: Validating objects: 100%
To https://git.us-west-2.codecatalyst.aws/v1/CloudFormation-CodeCatalyst/ThreeTierApp/3-tier-app
d410f15..e844b70 main -> main
PR_Branch_Workflow
is created but it has not run.PR_Branch_Workflow
, you can see the workflow. As defined in the workflow yaml above, the workflow will run the action Super-Linter_0d
first and if it is successful, only then it will run action CreateChangeSet
.1
2
$ git checkout -b test-pr-workflow
Switched to a new branch 'test-pr-workflow'
- If you are using your own CloudFormation template, make any changes to the template to create a change set.
- For the sample CloudFormation template used in this blog, you have 2 options :
- simply replace its content with this already modified template. Make sure the name of template file is same(
VPC_AutoScaling_With_Public_IPs.json
), as workflow has filename mentioned in it,OR - you can make the following changes manually to add a third subnet and its resources to the template:Add following JSON code to the sample CloudFormation template under
Resources
section.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26"PublicSubnet3" : {
"Type" : "AWS::EC2::Subnet",
"Properties" : {
"VpcId" : { "Ref" : "VPC" },
"CidrBlock" : { "Fn::FindInMap" : [ "SubnetConfig", "Public3", "CIDR" ]},
"AvailabilityZone" : {"Fn::Select": [2, {"Fn::GetAZs": ""}]},
"Tags" : [
{ "Key" : "Application", "Value" : { "Ref" : "AWS::StackId" } },
{ "Key" : "Network", "Value" : "Public" }
]
}
},
"PublicSubnetRouteTableAssociation3" : {
"Type" : "AWS::EC2::SubnetRouteTableAssociation",
"Properties" : {
"SubnetId" : { "Ref" : "PublicSubnet3" },
"RouteTableId" : { "Ref" : "PublicRouteTable" }
}
},
"PublicSubnetNetworkAclAssociation3" : {
"Type" : "AWS::EC2::SubnetNetworkAclAssociation",
"Properties" : {
"SubnetId" : { "Ref" : "PublicSubnet3" },
"NetworkAclId" : { "Ref" : "PublicNetworkAcl" }
}
},In the Mappings, LoadBalancer and AutoScaling resources, add thePublicSubnet3
like following :1
2
3
4
5
6
7"Mappings" : {
"SubnetConfig" : {
"VPC" : { "CIDR" : "10.0.0.0/16" },
"Public1" : { "CIDR" : "10.0.0.0/24" },
"Public2" : { "CIDR" : "10.0.1.0/24" },
"Public3" : { "CIDR" : "10.0.2.0/24" }
},1
2
3
4
5"WebServerFleet" : {
"Type" : "AWS::AutoScaling::AutoScalingGroup",
"DependsOn" : "PublicRoute",
"Properties" : {
"VPCZoneIdentifier" : [{ "Ref" : "PublicSubnet1" }, { "Ref" : "PublicSubnet2" }, { "Ref" : "PublicSubnet3" }],1
2
3
4
5
6
7"PublicApplicationLoadBalancer" : {
"Type" : "AWS::ElasticLoadBalancingV2::LoadBalancer",
"Properties" : {
"Subnets" : [ { "Ref" : "PublicSubnet1"}, { "Ref" : "PublicSubnet2" }, { "Ref" : "PublicSubnet3" } ],
"SecurityGroups" : [ { "Ref" : "PublicLoadBalancerSecurityGroup" } ]
}
},
test-pr-workflow
branch, using the following commands:1
2
3
$ git add .
$ git commit -m "Added third public subnet to VPC, RouteTable, NACL, LoadBalancer and ASG"
$ git push --set-upstream origin test-pr-workflow
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ git status
On branch test-pr-workflow
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: VPC_AutoScaling_With_Public_IPs.json
no changes added to commit (use "git add" and/or "git commit -a")
$ git add .
$ git commit -m "Added third public subnet to VPC, RouteTable, NACL, LoadBalancer and ASG"
[test-pr-workflow e1a1d0e] Added third public subnet to VPC, RouteTable, NACL, LoadBalancer and ASG
1 file changed, 33 insertions(+), 4 deletions(-)
$ git push --set-upstream origin test-pr-workflow
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 2 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 467 bytes | 467.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0), pack-reused 0
remote: Validating objects: 100%
To https://git.us-west-2.codecatalyst.aws/v1/CloudFormation-CodeCatalyst/ThreeTierApp/3-tier-app
* [new branch] test-pr-workflow -> test-pr-workflow
branch 'test-pr-workflow' set up to track 'origin/test-pr-workflow'.
Code
-> Repository
. You can see the 2 branches: main
and test-pr-workflow
.test-pr-workflow
, you can ask others to review the changes by creating a pull request.Create pull request
and enter following details:- Source repository:
3-tier-app
- Source branch:
test-pr-workflow
- Destination branch:
main
- Pull request title:
Merge new third subnet
- Pull request description:
New third subnet created and added to VPC, RouteTable, NACL, LoadBalancer, ASG
test-pr-workflow
and our destination for merge is the main
branch. Once this PR is created, it will trigger the PR_Branch_Workflow
that will create a CloudFormation change set (and not make any deployments!).To see the active run forPR_Branch_Workflow
, in the CodeCatalyst console, navigate to theCI/CD
->Workflows
page and ensure you select thetest-pr-workflow
branch. By default, the Workflows page always shows themain
branch. You have to switch branches to see the active workflow in other branches.
PR_Branch_Workflow
has failed
.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[INFO] --------------------------------------------
[INFO] Gathering user validation information...
[INFO] - Validating ALL files in code base...
[INFO] ----------------------------------------------
[INFO] Linting [CLOUDFORMATION] files...
[INFO] ----------------------------------------------
[INFO] File:[/github/workspace/VPC_AutoScaling_With_Public_IPs.json]
[ERROR] Found errors in [cfn-lint] linter!
[ERROR] Error code: 4. Command output:
------
W7001 Mapping 'AWSInstanceType2NATArch' is defined but not used
/github/workspace/VPC_AutoScaling_With_Public_IPs.json:56:5
------
[INFO] ----------------------------------------------
[INFO] The script has completed
[INFO] ----------------------------------------------
[ERROR] ERRORS FOUND in CLOUDFORMATION:[1]
[FATAL] Exiting with errors found!
test-pr-workflow
.1
2
3
4
5
6
7
8
9
10
11
"AWSInstanceType2NATArch" : {
"t1.micro" : { "Arch" : "NATHVM64" },
"t2.nano" : { "Arch" : "NATHVM64" },
"t2.micro" : { "Arch" : "NATHVM64" },
"t2.small" : { "Arch" : "NATHVM64" },
"t2.medium" : { "Arch" : "NATHVM64" },
"t2.large" : { "Arch" : "NATHVM64" },
"m4.large" : { "Arch" : "NATHVM64" },
"m4.xlarge" : { "Arch" : "NATHVM64" },
"c1.medium" : { "Arch" : "NATHVM64" }
},
test-pr-workflow
branch. As this is a revision to the branch, it should automatically trigger the workflow to run again.1
2
3
$ git add .
$ git commit -m "Removed unused AWSInstanceType2NATArch mapping"
$ git push --set-upstream origin test-pr-workflow
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ git add .
$ git commit -m "Removed unused AWSInstanceType2NATArch mapping"
[test-pr-workflow 64a27b5] Removed unused AWSInstanceType2NATArch mapping
1 file changed, 1 insertion(+), 11 deletions(-)
$ git push --set-upstream origin test-pr-workflow
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 2 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 319 bytes | 159.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0), pack-reused 0
remote: Validating objects: 100%
To https://git.us-west-2.codecatalyst.aws/v1/CloudFormation-CodeCatalyst/ThreeTierApp/3-tier-app
1f8e97d..64a27b5 test-pr-workflow -> test-pr-workflow
branch 'test-pr-workflow' set up to track 'origin/test-pr-workflow'.
CI/CD
-> Workflows
page. Make sure you select the test-pr-workflow
branch. A new run should now be active.1
2
3
4
5
6
7
8
9
10
11
12
[INFO] --------------------------------------------
[INFO] Gathering user validation information...
[INFO] - Validating ALL files in code base...
[INFO] ----------------------------------------------
[INFO] Linting [CLOUDFORMATION] files...
[INFO] ----------------------------------------------
[INFO] File:[/github/workspace/VPC_AutoScaling_With_Public_IPs.json]
[INFO] - File:[VPC_AutoScaling_With_Public_IPs.json] was linted
[INFO] ----------------------------------------------
[INFO] The script has completed
[INFO] ----------------------------------------------
[NOTICE] All file(s) linted successfully with no errors detected
CreateChangeSet
.Code
-> Pull Requests
, and then click on Merge
.If there are conflicts, or if the merge can't be completed, the merge button is inactive, and aNot mergeable
label is displayed. In that case, you must obtain approval from any required approvers, resolve conflicts locally if necessary, and push those changes to thetest-pr-workflow
before you can merge.
- Fast forward merge - Merges the branches and moves the destination branch pointer to the tip of the source branch. This is the default merge strategy in Git. Select this option for this blog.
- Squash and merge - Combines all commits from the source branch into single merge commit in the destination branch.
In our case, the source branch is
test-pr-workflow
, and I am deleting this branch to keep my branching structure clean and simple.test-pr-workflow
to main
branch. This will trigger the Main_Branch_Workflow
and deploy the updated CloudFormation code to the PreProdEnv
.PreProdEnvStack
and CodeCatalyst-IAM-roles
.Project settings
, click on Delete project
, and follow the instructions to delete the project.Space settings
tab and click on Delete space
.Any opinions in this post are those of the individual author and may not reflect the opinions of AWS.