Creating a Continuous Deployment workflow using Github Actions to deploy your application to ECS
Tags: aws, docker, github, devops
Published: 2021-09-23 09:16:34

A well-written Continuous Deployment (CD) pipeline ensures that on every merge to a release branch, an artifact is created and deployed to the correct environment.  

While working with containerized applications the CD pipeline needs to contain the following steps

- Code checkout
- Install dependencies
- Build the image
- Push to a container registry
- Use the latest image for the next deployment
- Trigger a new deployment

In this tutorial, we will write a CD pipeline that does all of the above tasks using Github Actions. We will deploy our application using AWS ECS.

AWS ECS is a fully managed container orchestration service from AWS. It helps you easily deploy, manage and scale containerized applications. 

This tutorial assumes that you have a solid understanding of 

- [AWS ECS](https://docs.aws.amazon.com/ecs/index.html)
- [Github Actions](https://docs.github.com/en/actions)
- [Docker](https://docs.docker.com/)

It also makes the implicit assumption that your application is already deployed on ECS. At the end of this tutorial you will be able automate your deployments on merge to a release branch. 

In this tutorial I will take you through how to

- Trigger a workflow on merge to a release branch.
- Build and push the image to the Elastic Container Registry
- Update the task-definition using the newly created image
- Deploy your application to the new environment

## Starter Project

Please clone the following repository: [https://github.com/wednesday-solutions/ecs-cd-starter](https://github.com/wednesday-solutions/ecs-cd-starter) 

## Setup database connection

Update the relevant database connection details in the `.env.development`

```jsx
DB_URI=postgres://<role>:<password>@<host>:<port>/<db>
POSTGRES_HOST=<host>
POSTGRES_DB=<db>
POSTGRES_USER=<user>
POSTGRES_PASSWORD=<password>
```

## Add secrets

We need to add the following secrets

- AWS_ACCESS_KEY_ID
ACCESS_KEY_ID with access to deploy
- AWS_SECRET_ACCESS_KEY
Associated SECRET_ACCESS_KEY
- AWS_REGION
Region in which the cluster is deployed
- AWS_ECR_REPOSITORY
Name of the ECR repository that we will push the image to.

**Step 1**

Add the **AWS_ACCESS_KEY_ID** and **AWS_SECRET_ACCESS_KEY** secrets**.**

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7vyl3yoir82nqem9dfbh.png)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/soqwi4pi8btgoicr4zwi.png)

**Step 2**

Add the AWS deployment region

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/adihop4emv6sbu5dl5mm.png)

**Step 3**

Get the repository name from the AWS console and add it as a secret.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rzj2j7wp1e1rnlr9qym9.png)

I have omitted the `-dev` since that represents the stage. We will be using the same workflow to deploy to multiple environments and will hence infer the stage at runtime.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t22lguy96a0p7ngwcg9f.png)

## Create the task-definition.json for all the environments

**Step 1**

Go to the following URL: [https://ap-south-1.console.aws.amazon.com/ecs/home?region=ap-south-1#/taskDefinitions](https://ap-south-1.console.aws.amazon.com/ecs/home?region=ap-south-1#/taskDefinitions)

I'm using `ap-south-1` as the AWS region. Please change the URL according to the region you are in

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f6lgw6zmi7brd1qkbpcd.png)

**Step 2**
Select the task definition for your environment and project. In my case its the `ecs-cd-starter-dev`

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7uizly9279e64gl3pt0q.png)

**Step 3**

Select the latest revision and go to the `JSON` tab

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3cqwo6yku9aceijhaza0.png)

**Step 4**

Copy the JSON.
Run the following snippet in the terminal

```jsx
mkdir task-definition
touch task-definition/dev.json
```

Paste the JSON from the AWS console into the newly created file.

I use `dev.json` since `dev` is my default branch name. I want the code that is pushed to this branch deployed to the `dev` environment.

You will need to repeat this step for the `qa` and `production`  environments.

## Trigger  the workflow to run on merge to a release branch

**Step 1 - Create workflow**

Create a new workflow for continuous deployment in the `.github/workflows` folder

```jsx
touch .github/workflows/cd.yml
```

**Step 2 - Setup triggers**

Identify your release branches. The first release branch will be your default branch which should also be the branch that the team typically raises a pull request to when they want to add a new feature.

In my case this is the `dev` branch. Typically you would have 2 more environments.

- qa
- production

So let's trigger this workflow whenever there is a push on one of these branches.

```jsx
name: ECS Starter CD Develop

on:
  push:
    branches:
      - dev
      - qa
      - master
jobs:
  docker-build-push-deploy:
    name: Docker build, push and deploy
    runs-on: ubuntu-latest
    steps:
```

Paste the above code into the newly created `cd.yml` file

**Step 3 - Checkout code**

This step pulls the latest code.

```jsx
...
    steps:
      - name: Checkout
        uses: actions/checkout@v2
```

**Step 4 - Get branch name**

Get the current branch name. This step will fetch the current git branch name and store it. It can now be accessed like so: `${{steps.vars.outputs.stage}}`

```jsx
...
    steps:
      ...
      - name: Get branch name
        id: vars
        run: echo ::set-output name=stage::${GITHUB_REF#refs/*/}
```

**Step 5 - Configure AWS Credentials**

Configure AWS Credentials and region. Use the values from Github secrets to configure the AWS Credentials.
To get a better understanding of all of the configuration options please go through the documentation here: [https://github.com/aws-actions/configure-aws-credentials#usage](https://github.com/aws-actions/configure-aws-credentials#usage)

```jsx
...
    steps:
      ...
      - 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: ${{ secrets.AWS_REGION }}
```

**Step 6 - Login to ECR**

Use the aws-actions/amazon-ecr action to log in to AWS ECR. 

To get a better understanding of all of the configuration options please go through the documentation here: [https://github.com/aws-actions/amazon-ecr-login#usage](https://github.com/aws-actions/configure-aws-credentials#usage)

```jsx
...
    steps:
      ...
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1
```

**Step 7 - Build tag and push image to ECR**

We now need to build the docker image,. tag and push it to AWS ECR. Use the commit hash to tag the image. 

```jsx
...
    steps:
      ...
      - name: Build, tag, and push image to Amazon ECR
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: ${{ secrets.AWS_ECR_REPOSITORY }}-${{steps.vars.outputs.stage}}
          AWS_REGION: ${{ secrets.AWS_REGION }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG3
```

**Step 8 - Render the task definition**

We create a new task-definition revision by updating the value of image. We will point to the image that we just pushed to ECR. 

```jsx
...
    steps:
      ...
      - name: Render Amazon ECS task definition
        id: ecs-cd-starter-container
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: task-definition/${{steps.vars.outputs.stage}}.json #1
          container-name: ecs-cd-starter-${{steps.vars.outputs.stage}} #2
          image: ${{ steps.login-ecr.outputs.registry }}/${{ secrets.AWS_ECR_REPOSITORY }}-${{steps.vars.outputs.stage}}:${{ github.sha }}
```

1. the task-definition folder will contain task-definitions for all of the environments. I create json files with the name of the environment and access it using `${{steps.vars.outputs.stage}}.json`  in the workflow
2. I name my containers with the stage as the suffix. I reference it using `<container-name>${{steps.vars.outputs.stage}}.json` in the workflow. 
3. We reference the image that we just pushed to the ECR registry.

**Step 9 - Deploy to ECS**

We use the latest revision of the task-definition that we just created to deploy the application to ECS. I use the same `-branchName` suffix when naming my service and cluster

```jsx
...
...
    steps:
      ...
      - name: Deploy to Amazon ECS service
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.pawlyclinic-api-container.outputs.task-definition }}
          service: ecs-cd-starter-${{ steps.vars.outputs.stage }}
          cluster: ecs-cd-starter-${{ steps.vars.outputs.stage }}
```

**Step 10 - Logout of ECR**

Once the deployment is done logout of ECR

```jsx
...
    steps:
      ...
      - name: Logout of Amazon ECR
        if: always()
        run: docker logout ${{ steps.login-ecr.outputs.registry }}
```

## Where to go from here

Now that you have setup a CD pipeline to deploy your application to ECS I would recommend reading our article on ["how to execute batch jobs in a multi-container environment"](https://www.wednesday.is/writing-tutorials/part-1-executing-batch-jobs-in-a-multi-container-environment-using-nodejs-and-express)

I hope you enjoyed this tutorial on how to create a CD pipeline to deploy your ECS application. If you have any questions or comments, please join the forum discussion below.