Chris Padilla/Blog / Tech

Configuring a CI/CD Pipeline in CircleCI to Deploy a Docker Image to AWS ECS

Continuous Integration/Continuous Deployment has many benefits in a team's development process! Much of the manual work of pushing changes to production are automated, different processes can be created between staging and production environments, and setting up a CI/CD flow can be a way of practicing Infrastructure As Code. The benefits to having the deployment process documented are all the same as using git in your application code: it's clearly documented, changes can be reverted, and there's a single source of truth for the process.

Here I'll be continuing on from deploying a Docker Image to AWS! This time, looking at integrating the process into a CI/CD process. Namely: CircleCI!

Setup

To configure CircleCI, we'll add this file as .circleci/config.yml from the root of our application:

version: 2.1
orbs:
  aws-cli: circleci/aws-cli@4.0
  aws-ecr: circleci/aws-ecr@9.1.0
  aws-ecs: circleci/aws-ecs@4.0.0

Here I'm loading in all the necessary orbs that will support our deployment. Orbs can be thought of as a group of pre-defined jobs that support integrations. Let's continue setting things up and seeing these orbs in action:

Checkout Repo

Much of the heavy lifting here will be done by the orbs we're pulling in. The only custom job we'll need to employ is one for checking out our repo on github:

jobs:
  checkout-repo:
    docker:
      - image: cimg/node:20.14
    steps:
      - checkout

Stepping Through the Workflow

Below the jobs block, it's now time for us to setup our workflow! This is the access point where CircleCI will call our commands and run our jobs.

I'm going to start by naming the workflow build-app. Under jobs, I'll start with the checkout-repo flow we just created:

workflows:
  build-app:
    jobs:
      - checkout-repo:
          name: checkout-repo
          filters:
            branches:
              only:
                - main

Here, I'm also targeting which branch triggers a build. Anytime a PR is merged into main, the process will fire off.

Next, let's build our docker image. We're going to be configuring the aws-ecr/build_and_push_image job:

  - aws-ecr/build_and_push_image:
      requires:
        - checkout-repo
      account_id: ${ID}
      auth:
        - aws-cli/setup:
            role_arn: ${ROLE}
            role_session_name: CircleCISession
      dockerfile: Dockerfile
      repo: ${REPO}
      tag: ${CIRCLE_SHA1}
      extra_build_args: >-
        --build-arg API_KEY=${API_KEY}

Most of these will be self explanatory if you've deployed to ECR before. One thing worth noting specific to CircleCI is the requires block. Here, we're adding checkout-repo as a dependency. I want the job to run sequentially, so here I'm telling CircleCI to wait for the previous step to complete before starting this one.

Also note that I'm passing in CIRCLE_SHA1 to the tag. I'm tagging images here with the unique hashed identifier. This way, all of my images are uniquely identified in ECR. The CIRCLE_SHA1 variable comes for free in any workflow.

Finally, we'll deploy to our ECS service by updating the service:

  - aws-ecs/deploy_service_update:
      requires:
        - aws-ecr/build_and_push_image
      cluster: ${CLUSTER}
      family: ${FAMILY}
      service_name: ${SERVICE}
      container_image_name_updates: container=${CONTAINER}, tag=${CIRCLE_SHA1}
      force_new_deployment: true
      auth:
        - aws-cli/setup:
            role_arn: ${ROLE}
            role_session_name: CircleCISession

Again, much should be familiar from the CLI approach. What's worth highlighting is the container_image_name_updates property. Since I'm defining the hashed id as the tag name in the previous job, I'm going to update my container image through the arguments container=${CONTAINER}, tag=${CIRCLE_SHA1}

The force_new_deployment argument is required for new changes to be pushed if the task is already running on ECS. (Which it likely is since this is continuous deployment!)

Full Config

That's it! That's enough to get the app spun up and running. Here's the full config for context:

version: 2.1
orbs:
  aws-cli: circleci/aws-cli@4.0
  aws-ecr: circleci/aws-ecr@9.1.0
  aws-ecs: circleci/aws-ecs@4.0.0

jobs:
  checkout-repo:
    docker:
      - image: cimg/node:20.14
    steps:
      - checkout

workflows:
  build-app:
    jobs:
      - checkout-repo:
          name: checkout-repo
          filters:
            branches:
              only:
                - main
      - aws-ecr/build_and_push_image:
          requires:
            - checkout-repo
          account_id: ${ID}
          auth:
            - aws-cli/setup:
                role_arn: ${ROLE}
                role_session_name: CircleCISession
          dockerfile: Dockerfile
          repo: ${REPO}
          tag: ${CIRCLE_SHA1}
          extra_build_args: >-
            --build-arg API_KEY=${API_KEY}
      - aws-ecs/deploy_service_update:
          requires:
            - aws-ecr/build_and_push_image
          cluster: ${CLUSTER}
          family: ${FAMILY}
          service_name: ${SERVICE}
          container_image_name_updates: container=${CONTAINER}, tag=${CIRCLE_SHA1}
          force_new_deployment: true
          auth:
            - aws-cli/setup:
                role_arn: ${ROLE}
                role_session_name: CircleCISession