Containers
Create a CI/CD pipeline for Amazon ECS with GitHub Actions and AWS CodeBuild Tests
Amazon Elastic Container Service (Amazon ECS) is a fully managed container orchestration service that makes it easy to operate containerized workloads at scale. It also integrates with other core AWS services, such as Amazon Route 53, AWS Identity and Access Management (IAM), and Amazon CloudWatch. Establishing an effective and efficient CI/CD pipeline is critical for containerized applications, regardless of the platform you are using to manage your containers.
In this post, we explore and demo how organizations using GitHub as a source code repository can use GitHub Actions to build a complete CI/CD pipeline for applications deployed on Amazon ECS. This post also illustrates how AWS CodeBuild can be used with GitHub Actions to execute application tests as part of a complete CI/CD pipeline.
If you are new to Amazon ECS or AWS CodeBuild, check out Getting Started with Amazon ECS and Developer Tools on AWS.
GitHub Actions
For organizations using GitHub as a source code repository, GitHub Actions provide a way to implement complex CI/CD functionality directly in GitHub by initiating a workflow on any GitHub event.
A GitHub Action is an individual unit of functionality that can be combined with other GitHub Actions to create workflows, which are triggered in response to certain GitHub events, for example, pull, push, or commit. Workflows run inside managed environments on GitHub-hosted servers.
To support CI/CD operations for applications running on Amazon ECS, AWS has open-sourced the following JavaScript-based GitHub Actions at github.com/aws-actions:
- github.com/aws-actions/configure-aws-credentials – Configure AWS credential and region environment variables for use in other GitHub Actions
- github.com/aws-actions/amazon-ecr-login – Log in the local Docker client to one or more Amazon Elastic Container Registry (ECR) registries
- github.com/aws-actions/amazon-Amazon ECS-render-task-definition – Insert a container image URI into an Amazon ECS task definition JSON file
- github.com/aws-actions/amazon-Amazon ECS-deploy-task-definition – Registers an Amazon ECS task definition and deploys it to an Amazon ECS service
By constructing a workflow using custom GitHub Actions, in addition to those provided by AWS and GitHub, we can easily deliver CI/CD functionality for our Amazon ECS application directly from GitHub.
Testing with AWS CodeBuild
AWS CodeBuild supports webhooks when GitHub is used as the source code repository. This allows CodeBuild to work with source code stored in GitHub, and use webhooks to trigger a build of the source code every time a code change is pushed to the GitHub repository.
You can use CodeBuild for more than just compiling your application code or building an application container image. With CodeBuild, you can easily run a variety of tests against your code, such as unit tests, static code analysis, and integration tests. When building a CI/CD pipeline, including a “test” stage is critical to maintaining application quality standards.
In this post, we don’t use CodeBuild as the “build” action of the CI/CD pipeline. Instead, we use CodeBuild as an environment to execute tests. Using CodeBuild as a testing platform can simplify running your tests for applications that are deployed on AWS. In addition to providing a managed environment that easily integrates with other AWS services, you can execute a CodeBuild project to perform integration tests with components that are not reachable outside of AWS. You can also use CodeBuild to Generate Test Reports from the results of unit and integration tests. For the example in this post, we use CodeBuild to perform simple unit tests and report the status to GitHub.
Example architecture
The following diagram shows the high-level architecture that we implement. This architecture represents a complete CI/CD pipeline that uses a GitHub workflow to automatically coordinate building, testing, and deploying an application to ECS for every commit to the repository. This GitHub workflow uses the AWS open-source GitHub Actions to coordinate build and deploy tasks, and uses CodeBuild to execute application tests. We also introduce a custom GitHub Action to this pipeline to evaluate the status of CodeBuild tests.
Our GitHub repository consists of a simple web application and accompanying infrastructure files. The goal of our CI/CD pipeline is to execute unit tests, build a container image, upload the container image to ECR, and update an ECS Task Definition for every commit to the GitHub repository.
Creating a GitHub Repository
To get started creating the components necessary to build our example architecture, we need a GitHub repository. If you are unfamiliar with this process, check out Creating a Repository on GitHub to get stared. For this example, the following files should be placed in the GitHub repository:
-
app.py
: a simple Flask web application. This application contains one endpoint that reverses and returns the requested URI
"""Main application file"""
from flask import Flask
app = Flask(__name__)
@app.route('/<random_string>')
def returnBackwardsString(random_string):
"""Reverse and return the provided URI"""
return "".join(reversed(random_string))
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
-
app_test.py
: a unit test file forapp.py
to ensure that the string is reversed correctly
"""Unit test file for app.py"""
from app import returnBackwardsString
import unittest
class TestApp(unittest.TestCase):
"""Unit tests defined for app.py"""
def test_return_backwards_string(self):
"""Test return backwards simple string"""
random_string = "This is my test string"
random_string_reversed = "gnirts tset ym si sihT"
self.assertEqual(random_string_reversed, returnBackwardsString(random_string))
if __name__ == "__main__":
unittest.main()
-
requirements.txt
: dependencies forapp.py
flask==1.0.2
-
buildspec.yml
: instructions for AWS CodeBuild to run unit tests. Refer to Build Specification Reference for more information about the capabilities and syntax available for buildspec files
version: 0.2
phases:
install:
runtime-versions:
python: 3.8
pre_build:
commands:
- pip install -r requirements.txt
- python app_test.py
-
Dockerfile
: instructions for building the application container image
FROM python:3
# Set application working directory
WORKDIR /usr/src/app
# Install requirements
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# Install application
COPY app.py ./
# Run application
CMD python app.py
-
task-definition.json
: specifications for an ECS Task Definition. Note: in this file, replace the placeholder value with your AWS Account ID
{
"requiresCompatibilities": [
"FARGATE"
],
"inferenceAccelerators": [],
"containerDefinitions": [
{
"name": "ecs-devops-sandbox",
"image": "ecs-devops-sandbox-repository:00000",
"resourceRequirements": null,
"essential": true,
"portMappings": [
{
"containerPort": "8080",
"protocol": "tcp"
}
]
}
],
"volumes": [],
"networkMode": "awsvpc",
"memory": "512",
"cpu": "256",
"executionRoleArn": "arn:aws:iam::<YOUR_AWS_ACCOUNT_ID>:role/ecs-devops-sandbox-execution-role",
"family": "ecs-devops-sandbox-task-definition",
"taskRoleArn": "",
"placementConstraints": []
}
The end result of this step should be a GitHub repository containing all application, test, and deployment files.
Creating ECS Infrastructure
To build the architecture described in the solution overview, you will need the following ECS components:
- ECR Repository: store versioned application container images
- ECS Cluster: provides compute power to run application container instances
- ECS Task Definition: specifies application container image version and environment considerations
- ECS Service: specifies how task definition will be deployed onto underlying compute resources
To build this infrastructure, we will be use the AWS Cloud Development Kit (CDK). If you are new to the CDK, see Getting Started with the AWS CDK. In this post, we will be using the CDK with Python 3.7.
To create our application infrastructure:
1. Configure your AWS CLI with an IAM user that has permissions to create the resources (VPC, ECS, ECR, IAM Role) described in the template below. Refer to Managing IAM Permissions for more details on creating custom users and policies.
2. Run the following commands to initialize your CDK project
# Create a project directory
mkdir ecs-devops-sandbox-cdk
# Enter the directory
cd ecs-devops-sandbox-cdk
# Use the CDK CLI to initiate a Python CDK project
cdk init --language python
# Activate your Python virtual environment
# NOTE: For Windows users, replace with ".env\Scripts\activate.bat"
source .env/bin/activate
# Install CDK Python general dependencies
pip install -r requirements.txt
# Install CDK Python ECS dependencies
pip install aws_cdk.aws_ec2 aws_cdk.aws_ecs aws_cdk.aws_ecr aws_cdk.aws_iam
3. Replace the contents of the file ecs_devops_sandbox_cdk/ecs_devops_sandbox_cdk_stack.py
(automatically created by the CDK) with the code below
"""AWS CDK module to create ECS infrastructure"""
from aws_cdk import (core, aws_ecs as ecs, aws_ecr as ecr, aws_ec2 as ec2, aws_iam as iam)
class EcsDevopsSandboxCdkStack(core.Stack):
def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
super().__init__(scope, id, **kwargs)
# Create the ECR Repository
ecr_repository = ecr.Repository(self,
"ecs-devops-sandbox-repository",
repository_name="ecs-devops-sandbox-repository")
# Create the ECS Cluster (and VPC)
vpc = ec2.Vpc(self,
"ecs-devops-sandbox-vpc",
max_azs=3)
cluster = ecs.Cluster(self,
"ecs-devops-sandbox-cluster",
cluster_name="ecs-devops-sandbox-cluster",
vpc=vpc)
# Create the ECS Task Definition with placeholder container (and named Task Execution IAM Role)
execution_role = iam.Role(self,
"ecs-devops-sandbox-execution-role",
assumed_by=iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
role_name="ecs-devops-sandbox-execution-role")
execution_role.add_to_policy(iam.PolicyStatement(
effect=iam.Effect.ALLOW,
resources=["*"],
actions=[
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
))
task_definition = ecs.FargateTaskDefinition(self,
"ecs-devops-sandbox-task-definition",
execution_role=execution_role,
family="ecs-devops-sandbox-task-definition")
container = task_definition.add_container(
"ecs-devops-sandbox",
image=ecs.ContainerImage.from_registry("amazon/amazon-ecs-sample")
)
# Create the ECS Service
service = ecs.FargateService(self,
"ecs-devops-sandbox-service",
cluster=cluster,
task_definition=task_definition,
service_name="ecs-devops-sandbox-service")
4. Run the following command from the root directory of your CDK project
# Create the CloudFormation stack
cdk deploy
This code uses the CDK to build and deploy a CloudFormation stack that contains the ECS infrastructure required for our application. In the code above, we are initially specifying the Task Definition to run with an example container from a public AWS sample registry. This sample container is replaced with our application container when our CI/CD pipeline updates the Task Definition. We are using the container from the sample registry to allow the Service to stabilize before any application container images are added to our ECR repository.
The end result of this step should be a Cluster, ecs-devops-sandbox-cluster
, running a Service, ecs-devops-sandbox-service
, that consists of a Task Definition, ecs-devops-sandbox-task-definition
.
Create a CodeBuild Project
CodeBuild is used to execute our application tests and provide the status of these tests to GitHub. If the tests do not pass, CodeBuild marks the build as Failed
and this status is reported to GitHub. In our architecture, we rely on webhooks that are automatically created by CodeBuild to trigger a build of our application code every time there is a commit to the master
branch of the GitHub repository.
To create a CodeBuild Project with GitHub integration:
1. Navigate to AWS CodeBuild and select Create build project
2. Under Project Configuration, for Project name, enter ecs-devops-sandbox
3. Under Source, for Source Provider, select GitHub
4. Under Source, for Repository, select Connect using OAuth and select Connect to GitHub
5. At the pop-out window, log into the GitHub account that owns the repository you wish to use
6. Under Source, for Repository, select Repository in my GitHub account
7. Under Source, for GitHub repository, select the repository you configured earlier
8. Under Source – Additional configuration, for Build Status – optional, check the box
9. Under Primary source webhook events, for Webhook – optional, check the box
10. Under Environment, for Environment image, select Managed image
11. Under Environment, for Operating system, select Ubuntu
12. Under Environment, for Runtime, select Standard
13. Under Environment, for Image, select aws/AWS CodeBuild/standard:3.0
14. Leave remaining default values and select Create build project
Create a GitHub Workflow
AWS has provided a starter GitHub workflow that takes advantage of the AWS open-source GitHub Actions to build and deploy containers on ECS for each commit to master
branch of the repository.
To add the starter GitHub workflow to your GitHub repository:
1. Under the Actions tab, select New workflow
2. Search for Deploy to Amazon ECS and select Set up this workflow
3. Follow the instructions outlined in the starter workflow file for using this workflow with your application. Note that we have already created an ECR repository, ECS task definition, ECS cluster, and ECS service
In practice, we want our GitHub workflow to build and deploy a new container to ECS only if the tests executed in CodeBuild were successful. The CodeBuild project we created earlier reports the success or failure of its build execution to GitHub for every commit. If the CodeBuild project fails for a commit, GitHub marks this commit with a status of failure
. If the CodeBuild project has not yet reported its success or failure, GitHub marks this commit with a status of pending
(learn more about GitHub commit statuses).
In this example, we are executing the GitHub workflow and the CodeBuild project in parallel on each commit to the master
branch. To ensure that our workflow does not deploy a commit that does not pass the required tests, we introduce an additional step into the starter GitHub workflow to check the status of the commit.
The code below shows the complete GitHub workflow (based on the starter workflow provided by AWS) that includes a new GitHub Action, “Check commit status.” This new GitHub Action uses the environment and variables provisioned by GitHub to query the status of the current commit from GitHub APIs. If the current commit is pending
, we check again after 10 seconds. If the commit status is failure
, i.e. the CodeBuild tests did not pass, then the GitHub workflow fails.
on:
push:
branches:
- master
name: Deploy to Amazon ECS
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
steps:
- name: Check commit status
id: commit-status
run: |
# Check the status of the Git commit
CURRENT_STATUS=$(curl --url https://api.github.com/repos/${{ github.repository }}/commits/${{ github.sha }}/status --header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' | jq -r '.state');
echo "Current status is: $CURRENT_STATUS"
while [ "${CURRENT_STATUS^^}" = "PENDING" ];
do sleep 10;
CURRENT_STATUS=$(curl --url https://api.github.com/repos/${{ github.repository }}/commits/${{ github.sha }}/status --header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' | jq -r '.state');
done;
echo "Current status is: $CURRENT_STATUS"
if [ "${CURRENT_STATUS^^}" = "FAILURE" ];
then echo "Commit status failed. Canceling execution";
exit 1;
fi
- name: Checkout
uses: actions/checkout@v1
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: <YOUR_AWS_REGION>
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build, tag, and push image to Amazon ECR
id: build-image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: ecs-devops-sandbox-repository
IMAGE_TAG: ${{ github.sha }}
run: |
# Build a docker container and
# push it to ECR so that it can
# be deployed to ECS.
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
- name: Fill in the new image ID in the Amazon ECS task definition
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: ecs-devops-sandbox
image: ${{ steps.build-image.outputs.image }}
- name: Deploy Amazon ECS task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: ecs-devops-sandbox-service
cluster: ecs-devops-sandbox-cluster
wait-for-service-stability: true
Results
With the AWS CodeBuild project and GitHub workflow in place, we can add new features to our application that automatically deploy to Amazon ECS if all tests are passed.
This example shows a modification to the application code (app.py
) that does not pass the unit test.
"""
Main application file
"""
from flask import Flask
app = Flask(__name__)
@app.route('/<random_string>')
def returnBackwardsString(random_string):
"""Reverse and return the provided URI"""
return "Breaking the unit test"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
When this code is checked into the repository, we can see that CodeBuild is automatically invoked via webhook and executes the application tests.
CodeBuild then reports the unsuccessful test execution to GitHub.
Because our GitHub workflow detected the status of the commit to be failure
, the workflow did not proceed past the “Check commit status” step.
This next example shows a modification to the application code (app.py
) that introduces some additional logging statements, but still passes the unit test.
"""
Main application file
"""
from flask import Flask
import logging
app = Flask(__name__)
# Initialize Logger
LOGGER = logging.getLogger()
LOGGER.setLevel(logging.INFO)
@app.route('/<random_string>')
def returnBackwardsString(random_string):
"""Reverse and return the provided URI"""
LOGGER.info('Received a message: %s', random_string)
return "".join(reversed(random_string))
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
When this code is checked into the repository, we can see that CodeBuild is again invoked via webhook, and successfully executes the application tests.
CodeBuild then reports its successful test execution to GitHub.
The GitHub workflow detects the status of the commit to be success
, and the remaining steps in the workflow successfully execute the deployment of the application to ECS.
Finally, we can see that the ECS Task Definition has been updated to reflect the new application container image. Note that the Git commit hash is used as the container image tag.
Conclusion
In this post, you saw how GitHub Actions can be used to support applications running on Amazon ECS. For organizations seeking to use GitHub Actions as a part of their DevOps infrastructure, combining GitHub Actions with other CI/CD services (like AWS CodeBuild) can be a simple and powerful way to deliver feature-rich CI/CD pipelines. For more information about GitHub Actions, see GitHub Actions Features.