I wrote a blog post earlier on how to create and deploy a containerized API-driven Python Streamlit web app using CICD on AWS. Although the steps I explained using ClickOps "AWS Console" were beginner-friendly, it's best to use a more automated method that's less prone to errors during production or big-scale development stages. That's where Infrastructure as a Code (IaC) comes in.
What is Infrastructure as Code (IaC)?
IaC refers to the process of managing and provisioning infrastructure through code instead of manual processes. The main benefit of IaC is that it automates the creation and management of infrastructure environments, so developers can focus on building and enhancing applications. This approach helps to control costs, reduce risks, and accelerate the process.
Another advantage of IaC is that it allows you to replicate the same environment and deploy it in another location using the same code. By designing the IaC in a modular way, it can be used anywhere with fewer restrictions, making deployment more agile. The more you design the IaC to be modular, the more agile the deployment will be.
IaC is commonly used in software development to build, test, and deploy applications.
In this article, we will explore a similar concept. I will explain the steps required to build and deploy the same environment and Web App "Streamlit Web App" used in the previous article, using AWS CloudFormation stacks and CodePipeline while increasing the Web App's scalability.
In my next blog post, I will demonstrate how to utilize this setup to integrate your web app with a GenAI model deployed using Amazon SageMaker JumpStart.
Getting Started
To begin with, we will create two ECR repositories. The first one will store the base image, while the second will store the web app image. We will then pull the base-image from Docker Hub and push it to the appropriate ECR repository. In this demo, we used the python:3.10-slim-buster as the base-image. You may follow the steps here.
Next, we need to prepare the content of the GitHub repository. This involves adding the source code, deployment scripts, definition files (such as Dockerfile, buildspec.yml, and appspec.yml), the "cfn" directory that contains all Cloudformation stacks, and the bin directory that contains all bash scripts to deploy the stacks. You can take a look at this repository sample to get an idea of how it should be structured. Each stack will have AWS resources with similar lifecycles and ownership.
After creating the repository, create a folder for each stack that contains a template.yaml file and parameters.json to define parameter values. Here's an example:
├── cfn
│ ├── compute
│ │ ├── parameters.json
│ │ └── template.yaml
│ ├── cdn
│ │ ├── parameters.json
│ │ └── template.yaml
Lastly, we will create a deployment script for each stack inside the bin directory and then deploy them in a specific order.
Note: While deploying directly to EC2 instances can be suitable for development, testing, or small-scale applications, for production environments, it's generally best to use container orchestration services like ECS or EKS for improved scalability, reliability, and maintainability.
We can discuss how to create a similar environment using either ECS or EKS, but we'll save that for another blog post. The benefit of this setup is that during the development or testing phases, you can deploy a different container using the same resources created by the stacks. You can use the same repository or another one, and you would only need to make a few changes such as updating the allowed ports by the Security Groups, Dockerfile, and the base image.
Now that the structure of the environment is in place, we can proceed to building the stacks.
Building The Stacks
We will create the necessary files for each stack, go through the configuration of the resources in each stack then develop the deployment scripts. You can clone the repository sample and update the files to suit your setup. In general, we will start the template by defining the parameters for each resource and use an external parameter file named parameters.json. Next, is the resource section, which will contain the configuration details of each resource. Finally, we will use the output sections to export the resource values that will be used in other stacks.
Before we proceed to the next steps, take a look at the solution diagram
First, we will start with the Networking stack to lay down the necessary network resources, next is the ALB stack to distribute the traffic to the Web App instances, then the Compute stack and the CDN stack.
Here is a high-level diagram of each stack and the cross-stack references.
1. Networking Stack
The Networking stack will create the base networking resources necessary for this environment. This will include the VPC that has an Internet Gateway, Route Table, two Availability Zones for redundancy, and one public subnet per Availability Zone.
To build the stack, create a Cloudformation template file inside the cfn/networking
directory similar to this (template.yaml) file.
The template description includes a list of the resources that will be generated by it.
Description: |
Networking stack Resources:
- VPC
- IGW
- Availability Zone
- Public Route Table
- route to the IGW
- route to Local
- 2 Public Subnets
Here are the resources:
2. ALB Stack
The ALB stack will create an Application Load Balancer to decouple the traffic from the Web App and distribute the traffic load across the Web App instances. Additionally, it includes a Target Group that specifies the target instances, and the ALB Security Group manages the traffic security. Create a CloudFormation template file inside the cfn/alb
directory similar to this (template.yaml).
The template description includes a list of the resources that will be generated by it.
Description: |
WebApp ALB Stack:
- ALB
- ipv4
- Internet facing
- certificate attached from Amazon Certification Manager (ACM)
- HTTPSListener
- HTTPS to WebApp Target Group
- Target Group
- ALB Security Groups
Going forward we will use an external parameters file (parameters.json) to define some parameters, so you will have to create it in the same directory similar to this ALB stack parameter file(parameters.json) file.
Update the values for each of the following ParameterKey:
- WebAppPort "The port your Web App will use"
- CertificateArn "ACM certificate Arn"
Here are the resources:
3. Compute Stack
In the Compute stack, we will create an IAM Role, Launch Template, and an Auto Scaling Group. These will define the EC2 instance configurations and types, as well as the scalability capacity required for the Web App. Additionally, we will create a Security Group that will allow the required traffic.
Create a template file inside the cfn/compute
directory similar to this (template.yaml) file.
The template description includes a list of the resources that will be generated by it.
Description: |
WebApp Compute Stack:
- Launch Template
- LT Security Group
- ASG
- IAM Role and Policies
Create the Compute stack parameters file inside the same directory, similar to this (parameters.json)file
The content of the user-data can be changed by encoding the new file using base64 base64 user-data
and updating the parameters.json file with the new value.
Here are the resources:
4. CDN Stack
In this stack, we will create a CloudFront Distribution with an ALB origin. Assuming that you have already a Domain Name registered in the Route 53 hosted zone, we will create a record to publish the distribution URL for the Web App.
Create a template file inside the cfn/cdn
directory similar to this (template.yaml) file.
The template description includes a list of the resources that will be generated by it.
Description: |
CDN Stack:
- CloudFront Distribution
- Route 53 Hosted Zone Record
Next, we will create the parameters file inside the same directory, similar to this parameters.json file.
Update the values for each of the following ParameterKey:
- DomainName
- HostedZoneRecord
- DomainHostedZoneId
- CustomDomainName
- CertificateArn
5. CICD Stack
Lastly, we will create a CodePipeline Stack and two nested stacks to create and set up CodeBuild and CodeDeploy. The pipeline will build and then deploy the container image as you update the Web app code inside the GitHub repository.
First, we need to create a pipeline artifact bucket that will be defined as a parameter. Run the following aws cli to create the bucket:
aws s3api create-bucket --bucket codepipeline-webapp-$(date +%Y%m%d%H%M%S) --region us-east-1
Second, create the main pipeline template file inside the cfn/cicd
directory. The file should be similar to this (template.yaml) file.
The template description includes a list of the resources that will be generated by it.
Description: |
WebApp CICD Stack:
- CodePipeline IAM Role
- CodeDeploy IAM Role
- Web App Pipeline
To be able to connect to the GitHub repository, we need first to create a CodeStar connection with the GitHub account.
- Run the following command to create a new CodeStar connection:
aws codestar-connections create-connection --provider-type GitHub --connection-name MyGitHubConnection
- make sure the repo is public otherwise you need to allow access to the private repo on GitHub
- Go to the AWS CodePipeline console
- Select Settings on from the left-side panel then click on Connections
- Select the connection name "MyGitHubConnection"
- Then click on "Update pending connection"
Next, we will create the parameters file inside the same directory, similar to this parameters.json file.
Update the values for each of the following ParameterKey:
- GitHubConnectionArn
- GitHubRepo
- GitHubBranch
- GitHubSourceRepo
- ArtifactBucketName
Here are the resources:
5.1 CodeBuild Stack
The CodeBuild stack will create a CodeBuild project and the necessary IAM Role to build the container image and then push it to the ECR repository
Create a template file inside the cfn/cicd/nested
directory similar to this (codebuild.yaml) file.
The template description includes a list of the resources that will be generated by it.
Description: |
AWS CodeBuild Project as part of the CICDStack:
- CodeBuild Service Role
- CodeBuild Project
5.2 CodeDeploy Stack
This Stack will create CodeDeploy Application and Deployment Group to deploy the Web app Container. For this demo purposes, we will use (in-place deployment), but it is recommended to create a Blue/Green deployment with multiple compute resources to ensure the availability of the Web App.
Create a template file inside the cfn/cicd/nested
directory similar to this (codedeploy.yaml) file.
The template description includes a list of the resources that will be generated by it.
Description: |
- AWS CodeDeploy Application
- Deployment Group
Deployment Scripts
At this stage we will create the bash deployment script for each stack, we will follow the same order we used to create the stacks by starting with the networking deployment script and so on.
You will have to define the following variables inside each script:
BUCKET="" # Cloudformation bucket
REGION="us-east-1" # replace with the relevant region
STACK_NAME=""
NOTE: if you change the STACK_NAME then you have to change the relevant stack name example: "NetworkingStack" value, in parameters.json of the other stacks.
If you do not have an existing Cloudformation artifact bucket, create a new one using the following aws cli:
aws s3api create-bucket --bucket cfn-artifact-$(date +%Y%m%d%H%M%S) --region us-east-1
Before we start, we need to install the following packages:
- AWS CloudFormation Linter Validate the syntax and properties of resources by running cfn-lint with each template.yaml file.
- Install JQ (JSON Processor) to transform the structure of the parameters.json into key=value structure. We will use jq tool inside the deployment scripts
Networking Stack Deployment Script
Create a bash file inside the bin directory similar to (networking-deploy).
The script will deploy the Networking Stack.
ALB Stack Deployment Script
Create a bash file inside the bin directory similar to (alb-deploy). This script will deploy the ALB Stack.
Compute Stack Deployment Script
Create a bash file inside the bin directory similar to (compute-deploy). This script will deploy the Compute Stack
CDN Stack Deployment Script
Create a bash file inside the bin directory similar to (cdn-deploy). This script will deploy the CDN Stack
CICD Stack Deployment Script
Create a bash file inside the bin directory similar to (cicd-deploy). This script will deploy the CICD Stack
Implementation
Build The Container Image
Although CodeBuild can be used to build the first version of the Web App image, the build project stack needs to be created outside the pipeline.
For now, we will just build the image using docker locally and then push it to the ECR repository. Here is an example
Let's Recap
We have stored the base-image and Web App image in the appropriate ECR repository. The definition files are ready and located in the root directory. The stack's templates and parameters files are in their corresponding stack folder, and all deployment scripts are available in the bin directory.
Stacks Deployment
We will walk through the steps required to deploy each stack in the right order. Starting with the Networking stack, that will create the necessary networking resources. Next, is the ALB stack which will create the ALB for the EC2 instances. After that, we will deploy the Compute stack to create the EC2 instances. Then, we will deploy the CDN stack which will create a CloudFront distribution and add it to the Route 53 host zone record. Finally, we will deploy the CICD stack which will create the pipeline to build and deploy future changes in the code.
Stacks Deployment Order
- Networking Stack: networking-deploy
- ALB Stack: alb-deploy
- Compute Stack: compute-deploy
- CDN Stack: cdn-deploy
- CICD Stack: cicd-deploy >> Note: Make sure all scripts inside the bin directory are executable
- Go to bin directory then run the first script
./networking-deploy
- Copy the ARN from the output, similar to this
Waiting for changeset to be created..
Changeset created successfully. Run the following command to review changes:
aws cloudformation describe-change-set --change-set-name arn:aws:cloudformation:us-east-1:XXXXXXXXXXXX:changeSet/awscli-cloudformation-package-deploy-XXXXXXXXXX/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXX
- To execute the change set, run the command below with the ARN you copied.
aws cloudformation execute-change-set --change-set-name arn:aws:cloudformation:us-east-1:XXXXXXXXXXXX:changeSet/awscli-cloudformation-package-deploy-XXXXXXXXXX/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXX
- Go to the AWS Cloudformation console
- Select the stack name and then Event tab to check the status of the stack
- Once the status changes to "CREATE_COMPLETE", go to the next step
Repeat the above steps for the rest of the stack in the right order using the relevant script. Once all stacks are deployed successfully, try to access the Web App using the domain name you used in the CDN stack.
Top comments (3)
It is possible to use either CDK or CFN, or even both in combination, depending on personal preference or limitations of the functions. Although additional security measures can be added to each stack and security scanning can be integrated into the CICD pipeline, the main focus here was on CloudFormation.
Launch Template always uses the latest Amazon AMI, so no EC2 patching was necessary. The "user-data" was used to install dependencies, and then pull and run the application container.
shouldn't the install of dependencies either be in the AMI, or through CodeDeploy?