Creating a CI/CD Pipeline with CodePipeline, CodeBuild, RDS and Route53
CodePipeline is a managed product that can be used to create an automated software release process designed to catch bugs and defects quickly and prevent them entering a production environment.
In this article we'll be looking at how CodePipeline can be used in conjunction with the rest of the AWS Code suite in addition to integrations with Route53, RDS and Secrets Manager.
What We'll Be Doing
By the end of this article, we'll have covered the following components of our pipeline:
- A webhook that triggers a new deployment whenever a "push" is made on a GitHub repository
- Miscellaneous build-related secrets stored in Secrets Manager
- A build script that provisions a temporary database which we'll be using for integration tests. This will include setting up a predefined CNAME through Route53
- A teardown script that decommissions the temporary database
- A
buildspec.yml
file which sets environment variables through Secrets Manager, installs dependencies, runs unit and integration tests, and performs any necessary cleanup.
Note For this example, we will be assuming the tests are run against a monolithic Ruby on Rails application, which means there will be some terminology specific to Ruby on Rails in the sections that follow.
A Few Important Points
This article assumes a basic familarity with CI/CD principles and a working knowledge of AWS services.
We also assume a basic level of familarity with Docker and the ability to push a Docker image to the Elastic Container Registry.
Finally, we will not be covering the writing of the tests themselves. Instead we will be focusing on the automation of those tests during the deployment process.
Disclaimer The architecture described here is not necessarily the most cost-effective or efficient; the article is just designed to explore how different cloud services can be combined together to solve problems. Moreover, it would normally be better to use Infrastructure as Code to define this kind of infrastructure configuration, but for the sake of speed we will not be doing that here.
Let's Set up the Pipeline
Let's get started! The first place to start is Secrets Manager, where we'll be adding some sensitive values that will be needed during the build process.
Creating our Secrets
We will need to create two secrets to store sensitive values. One of these is for the temporary database's parameters like username and password and the other is for the CodeBuild environment.
First we create the TestDB
secret:
Next, we can add the values it stores:
- TEST_DB_CNAME
- HOSTED_ZONE_FOR_TEST_DB
- TEST_DB_NAME
- TEST_DB_USERNAME
- TEST_DB_PASSWORD
- TEST_DB_SECURITY_GROUP
TEST_DB_SECURITY_GROUP
refers to the name of a security group that will be attached to the temporary test database. This security group should be configured in advance.
TEST_DB_CNAME
refers to a CNAME that will point to the public DNS hostname of the temporary database. The subdomain will need to be part of a domain that you own and which is managed through Route53.
HOSTED_ZONE_FOR_TEST_DB
is the Route53 Hosted Zone ID in which the domain's records are managed.
The other three values will hopefully be self-explanatory and you can set them as you see fit.
Finally, we have the option to configure rotation of the key used to back the secret:
We will also need to repeat this for the CodeBuildEnvVariables
secret which contains the following values:
- DATABASE_URL
- DEVISE_JWT_SECRET_KEY, which is used to configure the Rails authentication layer
Setting up CodePipeline
Now that the secrets have been created, we can start building the pipeline.
First, let's create a pipeline.
Next, we need to choose a source integration with a version control system. In the example, I have used GitHub (Version 2).
Now, we need to create an AWS CodeBuild project.
The final step is to configure the build project. As mentioned previously, you might need to use a custom Docker image to run your build, which will require familiarity with both Docker and Elastic Container Registry. If you are using Elastic Container Registry, there will need to be an IAM role attached to the build which has sufficient permissions to pull images from it.
Most of the other boxes will be filled in by the later steps in this article, so stay tuned.
Note There is a final CodePipeline stage which is the
CodeDeploy
integration. As this article assumes a Rails application, this is likely to require either an integration with Elastic Beanstalk or Elastic Container Service, both of which are relatively painless to set up. The CodeDeploy stage is not covered in this article because it is focused more on the build.
Route53
Now that our CodeBuild configuration is nearly complete, we need to turn our attention to provisioning a temporary database to run our tests.
To do this, we'll need the database to be available at a predictable URL or else our application code will need to change every time we deploy.
In order to complete this part, a domain is required. If the domain is not managed in Route53, it will need to be transferred.
Temporary Database
In an ideal world we could have our testing database operational all the time, but to keep costs down it would be better to only provision the database when it's required, even if that comes at the expense of longer CodeBuild execution times.
What this means in practice is that we will need to add a shell script which provisions the temporary database using various AWS CLI commands spanning a number of different services.
Build Script
Let's add a shell script to provision our temporary database.
This script does the following:
- Extracts values from the Secrets Manager secret that we configured earlier
- Extracts the ID of the pre-existing test database security group
- Creates an RDS database instance using the credentials extracted from the Secrets Manager secret
- Runs a command to pause script execution until the database is operational
- Adds a Route53 CNAME record using the public DNS hostname of the database which is extracted from a CLI command issued to the RDS API
# provision-temp-db.sh
#!/bin/bash
export AWS_REGION=your-region
TEST_DB_SECRET=$(aws secretsmanager get-secret-value --secret-id TestDB --query SecretString --output text)
TEST_DB_CNAME=$(echo ${TEST_DB_SECRET} | jq -r .TEST_DB_CNAME)
HOSTED_ZONE=$(echo ${TEST_DB_SECRET} | jq -r .HOSTED_ZONE_FOR_TEST_DB)
RDS_DATABASE_NAME=$(echo ${TEST_DB_SECRET} | jq -r .TEST_DB_NAME)
RDS_USER_NAME=$(echo ${TEST_DB_SECRET} | jq -r .TEST_DB_USERNAME)
RDS_PASSWORD=$(echo ${TEST_DB_SECRET} | jq -r .TEST_DB_PASSWORD)
TEST_DB_SECURITY_GROUP=$(echo ${TEST_DB_SECRET} | jq -r .TEST_DB_SECURITY_GROUP)
SECURITY_GROUP_ID=$(aws ec2 describe-security-groups --filters Name=group-name,Values=${TEST_DB_SECURITY_GROUP} --query "SecurityGroups[0].GroupId" --output text)
aws rds create-db-instance --db-instance-identifier ${RDS_DATABASE_NAME} --engine-version 11.12 --db-name ${RDS_DATABASE_NAME} --vpc-security-group-ids ${SECURITY_GROUP_ID} --allocated-storage 20 --db-instance-class db.t2.micro --engine postgres --master-username ${RDS_USER_NAME} --master-user-password ${RDS_PASSWORD} --region ${AWS_REGION}
# Pauses execution until DB instance is operational
aws rds wait db-instance-available --db-instance-identifier ${RDS_DATABASE_NAME}
# Extract DB_URL to variable for use in Route53 CNAME record
DB_URL=$(aws rds describe-db-instances --db-instance-identifier ${RDS_DATABASE_NAME} --region ${AWS_REGION} --output text | grep rds.amazonaws.com | awk '{print $2}')
# Add CNAME record to Route53 hosted zone
CHANGE_ID=$(aws route53 change-resource-record-sets \
--hosted-zone-id "${HOSTED_ZONE}" \
--change-batch file://<(echo "{ \"Changes\": [ {
\"Action\": \"CREATE\",
\"ResourceRecordSet\": {
\"Name\": \"${TEST_DB_CNAME}\",
\"Type\": \"CNAME\",
\"TTL\": 300,
\"ResourceRecords\": [ {
\"Value\": \"${DB_URL}\"
}]}}]}") \
--output text --query ChangeInfo.Id)
# Waits for "IN_SYNC" status across Route53 DNS servers
aws route53 wait resource-record-sets-changed --id "${CHANGE_ID}"
Cleanup Script
Now we need to add a shell script responsible for tearing down the temporary database. This script will be run whether the CodeBuild execution is successful (meaning the integration tests pass) or unsuccessful (meaning we have some failing tests).
This script does the following:
- Extracts values from the Secrets Manager secret
- Deletes the temporary database instance, including any automated backups
- Removes the CNAME record from Route53
# tear-down-temp-db.sh
#!/bin/bash
TEST_DB_SECRET=$(aws secretsmanager get-secret-value --secret-id TestDB --query SecretString --output text)
HOSTED_ZONE=$(echo ${TEST_DB_SECRET} | jq -r .HOSTED_ZONE_FOR_TEST_DB)
TEST_DB_CNAME=$(echo ${TEST_DB_SECRET} | jq -r .TEST_DB_CNAME)
RDS_DATABASE_NAME=$(echo ${TEST_DB_SECRET} | jq -r .TEST_DB_NAME)
# Remove RDS temp instance
aws rds delete-db-instance --db-instance-identifier $RDS_DATABASE_NAME --delete-automated-backups --skip-final-snapshot
# Get Route53 Record Sets
URL_TO_REMOVE=$(aws route53 list-resource-record-sets --hosted-zone-id $HOSTED_ZONE --query "ResourceRecordSets[?Name == '${TEST_DB_CNAME}.'].ResourceRecords[0]" --output text)
# Delete temp DB CNAME from Route53
aws route53 change-resource-record-sets \
--hosted-zone-id "${HOSTED_ZONE}" \
--change-batch file://<(echo "{ \"Changes\": [ {
\"Action\": \"DELETE\",
\"ResourceRecordSet\": {
\"Name\": \"${TEST_DB_CNAME}\",
\"Type\": \"CNAME\",
\"TTL\": 300,
\"ResourceRecords\": [ {
\"Value\": \"${URL_TO_REMOVE}\"
}]}}]}") \
--output text --query ChangeInfo.Id
Putting It All Together
Now that we have got these two shell scripts which will provision and decommission the temporary testing database as required, we can move on with the codebuild.yml
, which acts as a series of instructions for CodeBuild:
Note that the env
, or environment, references values we previously defined in Secrets Manager. Note also how the cleanup shell script will be run in case of success or failure, ensuring that the temporary database is always decommissioned no matter what the outcome of the testing might be.
# codebuild.yml
version: 0.2
env:
secrets-manager:
DATABASE_URL: CodeBuildEnvVariables:DATABASE_URL
DEVISE_JWT_SECRET_KEY: CodeBuildEnvVariables:DEVISE_JWT_SECRET_KEY
phases:
install:
runtime-versions:
ruby: 2.7
commands:
- echo Installing bundler...
- gem install bundler
- echo Installing dependencies...
- bundle install
pre_build:
on-failure: CONTINUE
commands:
- echo Setting up database...
- ./provision-temp-db.sh
- export SECRET_KEY_BASE=$(rake secret)
- RAILS_ENV=test bundle exec rake db:setup
- RAILS_ENV=test bundle exec rake db:migrate
build:
on-failure: ABORT
commands:
- echo Running rspec tests...
- bundle exec rspec spec/
finally:
- echo Decommissioning database...
- ./tear-down-temp-db.sh
post_build:
commands:
- echo Entering post-build phase...
artifacts:
type: zip
files:
- "scripts/*"
- "config.ru"
- "Gemfile.lock"
- "Gemfile"
- "Rakefile"
- "app/**/*"
- "bin/**/*"
- "config/**/*"
- "db/**/*"
- "lib/**/*"
- "log/**/*"
- "public/**/*"
- "spec/**/*"
- "vendor/**/*"
# Add more paths to the "files" key if your application requires it
Note Because the shell scripts issue AWS CLI commands, the IAM service role attached to the CodeBuild environment will require - in addition to the ECR permissions we discussed earlier - read and write access to RDS, Route53, VPC Security Groups and Secrets Manger.
Assuming the Pipeline is now fully configured along with the CodeDeploy
stage, you should now be able to see the CodeBuild process in action next time you push to the repository.
Summary
This article was a quick guide to demonstrate how Secrets Manager, AWS CodePipeline, AWS CodeBuild, RDS and Route53 can be used to create a deployment pipeline providing a quality-controlled release process which ensures high quality by running automated integration tests against a temporary testing database.
Related Articles
Easier Cron Jobs on AWS
Scheduling repetitive recurring tasks through code is a very common…
April 25th, 2022
Passing the Solutions Architect Professional Exam
A few months ago I successfully achieved the AWS Solutions Architect…
January 17th, 2022
Jemalloc and Rails on Amazon Linux
We were facing a major memory issue with our production Ruby on Rails…
June 8th, 2022