Overview
Running automated tests in a CI environment is an important step toward improving code quality, code standards and ensuring that only working code is merged into other environments.
With tools available in AWS, we have some low hanging fruit for teams to add some automated safeguards.
We’re going to walk through a Pull Request Workflow for CodeCommit and CodeBuild using Terraform to configure the AWS services, and Docker/Docker Compose to build our code and run tests.
Example Application
https://github.com/devato/aws-pull-request-workflow
Goals
- Create/Update PR triggers build, reports results to codecommit.
- CodeCommit PR requires CodeBuild approval.
- Terraform for Infrastructure as Code.
Pre-Requisites
-
An existing
CodeCommit
repository. -
An existing
IAM
user and roles allowing you to commmit code to the repository. - (Optional) An existing S3 bucket for storing Terraform State.
- (Optional) Docker and Docker Compose running on your local machine.
- Basic understanding of Docker and Docker Compose.
The Plan
With the pre-requisites and goals in mind, let’s take a look at a basic setup:
The main services we need to manage are:
- CodeCommit
- EventBridge (formerly Cloudwatch Events)
- CodeBuild
Getting Started
My advice is to use our Example Application that includes the Terraform configuration to quickly get up and running.
Once you have a good handle on things work, it’ll be easier to translate the processes into your own projects.
Example Application
We have a basic Elixir Phoenix app that has some very simple tests that connect to a Postgres Database, all managed by Docker Compose.
Clone our example repo:
$ git clone [email protected]:devato/aws-pull-request-workflow.git
And test that the test process will run: (docker needs to be running)
$ docker-compose --env MIX_ENV=test run --rm test
The reason it’s important to run your tests locally is so you can understand how to debug your system to eliminate potential issues in CodeBuild.
Push to CodeCommit
Once you have the tests passing locally, configure git to point to your CodeCommit Repo:
(To simplify things, ensure your CodeCommit repo is named awsapp
. Otherwise you’ll need to change the variables in .infrastructure/cicd/main.tf
)
$ git remote add origin <https/ssh url for codecommit>
$ git push origin
NOTE: There are many factors to consider when configurating IAM users for CodeCommit which are outside the scope of this article.
If you have any issues getting your code into CodeCommit, please resolve them here first.
Prepare Terraform State Backend
The resources needed to automatically build and test your code will be created using “Infrastructure as Code” (IoC) via Terraform. You don’t really need experience with Terraform to complete the next steps, but like running local tests, understanding how the pieces fit together and work will help you debug any issues in AWS.
NOTE: If you would like to store Terraform state in S3, please ensure your bucket is created before starting.
If you’d rather skip remote state storage for this example, just remove the backend
block from .infrastructure/cicd/main.tf
:
terraform {
required_version = ">= 0.12.4"
# Comment this out to skip remote state storage
backend "s3" {
bucket = "awsapp-cicd-terraform-remote-state"
key = "cicd/awsapp/state"
region = "us-east-1"
}
}
Install Terraform & aws-cli
In order to allow Terraform to interact with AWS on your behalf, you’ll need to install and configure Terraform
and the aws-cli
.
- Install AWS-CLI: https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-mac.html
- Install Terraform: https://learn.hashicorp.com/tutorials/terraform/install-cli
Create AWS Credentials
Create AWS Credentials in the AWS Console and configure your local machine to use them.
NOTE: AWS Roles/Policies needed to manage the required resources are complex, so to keep things simple, just add Administrator privileges to your chosen user which can be fine-tuned at your convenience.
Configure aws-cli by entering the details:
$ aws configure
AWS Access Key ID [****************XXXX]:
AWS Secret Access Key [****************XXXX]:
Default region name [us-east-1]:
Default output format [json]:
Now your local machine is ready to interact with AWS using Terraform.
Initialize Terraform
The Terraform Infrastructure as Code is included in the Example Repo.
The commands will need to be executed from the .infrastructure/cicd
directory.
$ cd .infrastructure/cicd
$ terraform init
Initializing modules...
...
Use terraform plan to review which resources will be created/changed:
$ terraform plan
Create Resources
If everything looks good in the terminal thus far, we should be good to create the required resources.
Open main.tf
and update the account_id
in the CodeBuild
configuration module:
module "cicd_awsapp_codebuild" {
...
account_id = "XXXXXXXXXXXX"
...
}
This is the Account ID beside in the Account dropdown found in the AWS Console
Now you can apply the config:
$ terraform apply
This will display a full plan, and ask you to verify the command:
...
Plan: 9 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter yes
and after a few seconds, you should see that plan was applied:
...
Apply complete! Resources: 9 added, 0 changed, 0 destroyed.
This created:
-
EventBridge Rule
that responds topullRequestCreated
andpullRequestSourceBranchUpdated
events from CodeCommit. -
CodeBuild
project and role. -
IAM Roles
with the necessary permissions trigger builds, pull the code, and comment on the Pull Request.
You can explore these services in the AWS console, and because they’re created with Terraform, they’re tracked and stored in S3 (or locally) and can be easily updated or removed from your account.
With these resources in place we’re now able to automatically build our code on the events listed above.
Terraform File Structure
Explore the .infrasturcture
directory to further understand the permissions and configuration for each AWS service.
-
There is a
main.tf
file that holds the config for our CI workflow. -
This pulls in
modules
from the module directory. - Each module has it’s own set of variables and config.
We’re not going into too much detail about Terraform and different ways to organize IoC. So let’s continue with our Pull Request Workflow.
CodeCommit Approval Rule Template
One of our goals was to require CodeBuild approval before our Pull Requests can be merged.
At the time of writing CodeCommit Approval Templates are not yet available to Terraform, so we’ll have to create it manually.
In the AWS Console, search for CodeCommit then navigate to Approval rule templates
.
Create a new template with the following details:
Name: awsapp Codebuild approval template
Description: Requires CodeBuild role approval.
Number of approvals needed: 1
Approver Type:
-
Select
IAM user name or assumed role
-
Value: value of
codebuild_role
variable frommain.tf
followed by/*
(awsapp-codebuild-role/*
)
Associated Repositories: awsapp
Pull Request Workflow in Action
With all the ceremony out of the way, and now that we have a full understanding of how the AWS resources work together, let’s create a Pull Request and see everything in action.
Create a Pull Request
From your local machine, create a new branch and make some changes:
$ git checkout -b pull-request
$ touch trigger
$ git add .
$ git commit -am 'Trigger build'
$ git push origin
From the AWS Console, go to CodeCommit select your repo, and hit Create pull request
,
give it a title and create it.
If you now click on Build
-> Build Projects
, and select your build project:
You should see your build project running:
You can click on the running build and follow the logs to see how it’s progressing.
CodeBuild Configuration
This CodeBuild instance is based on Linux and has Docker and Docker Compose pre-installed and uses aws/codebuild/standard:4.0
image.
Codebuild uses the file buildspec.yml
as instructions for how to build/test your application.
Here are the contents in the Example Application:
# buildspec.yml
version: 0.2
env:
variables:
AWS_DEFAULT_REGION: "us-east-1"
MIX_ENV: "test"
phases:
install:
commands:
- BUILD_NAME=$(echo $CODEBUILD_BUILD_ID | cut -d':' -f1)
- BUILD_ID=$(echo $CODEBUILD_BUILD_ID | cut -d':' -f2)
- BUILD_URL=$(echo "https://$AWS_DEFAULT_REGION.console.aws.amazon.com/codesuite/codebuild/projects/$BUILD_NAME/build/$BUILD_NAME%3A$BUILD_ID")
pre_build:
commands:
- echo "Revoking approval"
- aws codecommit update-pull-request-approval-state --pull-request-id $PULL_REQUEST_ID --approval-state REVOKE --revision-id $REVISION_ID;
- echo "Checkout code"
- git checkout $SOURCE_COMMIT
build:
commands:
- docker-compose --env MIX_ENV=test run --rm test
post_build:
commands:
- |
if [ $CODEBUILD_BUILD_SUCCEEDING = 1 ]; then
aws codecommit update-pull-request-approval-state --pull-request-id $PULL_REQUEST_ID --approval-state APPROVE --revision-id $REVISION_ID;
content="✔️ Pull request build SUCCEEDED! "
else
content="❌ Pull request build FAILED "
fi
- aws codecommit post-comment-for-pull-request --pull-request-id $PULL_REQUEST_ID --repository-name $REPOSITORY_NAME --before-commit-id $DESTINATION_COMMIT --after-commit-id $SOURCE_COMMIT --content "$content"
Buildspec Breakdown:
- Sets some ENV variables
- Runs through different phases:
-
install
: Sets env variables needed by other phases. -
pre_build
: revokes any previously approvals made by the Codebuild role, and checks out code from Pull Request branch. -
build
: uses Docker Compose to stand up DB and run tests. -
post_build
: Conditionally approves Pull Request on success, then comments on Pull Request with either success or failure message.
This is the most basic workflow and a buildspec.yml file is entirely customizable.
Notice how aws-cli
commands are available directly in the Build Image. This is enabled by the privileged
variable in CodeBuild’s Terraform configuration.
Back to Pull Request Workflow
At this point we’re assuming that all systems are green. The build passes, approves the pull request and submits a comment with the build results.
You should see the comment with a link to the build in the Activity
tab of your Pull Request:
You’ll also notice that the Pull Request as been approved, and under Approvals
the CodeBuild role should be listed:
Break the Build
Let’s test the failing build process by breaking a test and commiting it to our Pull Request branch:
Open test/aws_app_web/controllers/page_controller_test.exs
And change the response code in the test to 404:
defmodule AwsAppWeb.PageControllerTest do
use AwsAppWeb.ConnCase
test "GET /", %{conn: conn} do
conn = get(conn, "/")
assert html_response(conn, 404) =~ "Welcome to Phoenix!"
end
end
Push the code to the pull-request
branch in Codecommit.
When the build is complete, you should see the failure message on the Pull Requests Activity
tab:
You’ll also notice that the Pull Request is no longer Approved
and the approval has been removed.
Wrapping Up
Let’s review our Goals:
- ✔️ Create/Update PR triggers build, reports results to codecommit.
- ✔️ CodeCommit PR requires CodeBuild approval.
- ✔️ Terraform for Infrastructure as Code.
When teams of developers are creating Pull Requests and pushing lots of code, having your tests automatically run and approving pull requests is a great way to reduce the work needed for code review.
This has been the most basic example of a Pull Request workflow using Codecommit <> Codebuild and can serve as a starting point for implementing CI/CD for your projects.
Hit me up on twitter if you have any questions or find any issues with this workflow @tmartin8080