Streamlining Microservices: Building and Deploying Docker Images to a private ECR with GitHub Actions

Streamlining Microservices: Building and Deploying Docker Images to a private ECR with GitHub Actions

Introduction:

This will be the first in a series of posts outlining a continuous delivery pipeline that takes code from a GitHub repo and ultimately deploys running pods into a Kubernetes cluster via ArgoCD, the full workflow of which is demonstrated above.

For this blog post we will be focusing on the "Upload to ECR" section:

Use Case:

We want to be able to make changes to the code in our microservice monorepo, create a GitHub release, and have the images of each of our services created, tagged and pushed to our private ECR repo for use in deployments.

The tech involved:

For this we will be using:

  • GitHub for hosting our code repo, and handling releases.
  • GitHub Actions for the workflow.
  • Docker for the container platform.
  • AWS ECR to act as a Container registry.

Microservice Monorepo:

This is where we keep the code for our various microservices, where an example file structure for this repo would be:

.
├── docker-compose.yml
├── Makefile
├── README.md
├── auth-service
│   ├── Dockerfile
│   ├── go.mod
│   └── main.go
├── user-service
│   ├── Dockerfile
│   ├── go.mod
│   └── main.go
├── payment-service
│   ├── Dockerfile
│   ├── go.mod
│   └── main.go
└── email-service
    ├── Dockerfile
    ├── go.mod
    └── main.go

Each service in its own directory, with a Dockerfile, which is used to build the binaries and docker images within the docker-compose.yml file.

For example, during the development process an engineer may manually run make build or something similar that would in turn trigger docker compose to run and build an up to date image of each service. We will essentially be baking this process into an automated workflow with GitHub actions.

GitHub Actions

GitHub Actions allow us to automate and execute software development workflows, functioning as a workflow management service built into GitHub, there are many different use cases for GitHub Actions (including Automated Testing, Deployments, Triggering workflows in other services, etc.) but for this post we will be focusing on the ability to automate our software development workflow, and deploy container images to ECR.

Docker & AWS ECR

Docker is a container application platform, as outlined above each of our services has its own Dockerfile and throughout the development process, we use docker compose to build docker images from our code. ECR is a fully managed container registry provided by AWS, which we will be using to store the tagged images which are the output of our workflow.

The workflow

Below are the steps that our workflow will take:

  1. A developer merges a change that they have made in the repo
  2. The developer creates a Github release with a tag
  3. The images for our services are automatically built and tagged with the release tag.
  4. Our tagged images are pushed to our private ECR repo.

Putting it together:

In this example we will be assuming:

  • That there is a Makefile that has a definition like in the repo:
build_mail:
	@echo "Building mail binary..."
	cd ./mail-service && env GOOS=linux CGO_ENABLED=0 go build -o ${MAIL_BINARY} ./cmd
	@echo "Done!"

action_build: build_mail build_gateway build_auth 
	@echo "building to push to registry"
	docker compose up -d --no-deps --build

Which will build our binaries which will later be copied into the file systems of our container images during the image build process.

  • That we have an existing private ECR repository with the correct corresponding names

GitHub Action: Building, Tagging and Uploading to ECR:

  1. For deploying to a private ECR you will need to create AWS IAM (Identity and Access Management) user with the "AmazonEC2ContainerRegistryFullAccess" permissions.
  2. Get the AWS Access Key ID and the Secret Access Key for this user you have created.
  3. Create 3 GitHub Actions Secrets in the repo, this can be done by:
    1. Navigating to the repo
    2. Going to Settings
    3. Navigating to the Secrets section and clicking Actions
    4. Create 3 secrets using the details obtained earlier:
      1. AWS_ACCESS_KEY_ID
      2. AWS_SECRET_ACCESS_KEY
      3. AWS_REGION
  4. Create a GitHub Actions workflow:
    1. Create a directory called ".github/workflows"

Inside this directory, create a YAML file called upload_to_ecr.yaml

name: Release to ECR

on:
  release:
    types: [published]

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
    
      - name: Set up AWS CLI
        uses: aws-actions/configure-aws-credentials@v3
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}
    
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2
    
      - name: Build and push images
        run: |
          # Extract the AWS Account ID
          ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)
    
          # Get the ECR repository URL
          ECR_URL=${ACCOUNT_ID}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com
          
    	  PROJECT_NAME=$(basename "$PWD")
    	  
          RELEASE_TAG=${{ github.event.release.tag_name }}
    
          # Run the make command to build images
          make action_build
    
          # Define the services you want to exclude
          EXCLUDE_SERVICES=("event-queue")
    
          # Tag and push each service image
          for SERVICE in $(docker compose config --services); do
              # Check if the service is in the excluded list
              if [[ " ${EXCLUDE_SERVICES[@]} " =~ " ${SERVICE} " ]]; then
                  echo "Skipping ${SERVICE}"
                  continue
              fi
    
              TAG_NAME="${PROJECT_NAME}-${SERVICE}:latest"
    
              # Check if the image exists
              if [[ "$(docker images -q ${TAG_NAME} 2> /dev/null)" == "" ]]; then
                  echo "Image ${TAG_NAME} not found, skipping."
                  docker image ls -a
                  continue
              fi
    
              IMAGE_NAME="${ECR_URL}/${PROJECT_NAME}/${SERVICE}:${RELEASE_TAG}"
    
              docker tag ${TAG_NAME} ${IMAGE_NAME}
              docker push ${IMAGE_NAME}
          done

The above is the definition for our workflow, here's an explanation of the above:

  1. At the start, is the name of our action: Release to ECR, this is how our action will be named within GitHub actions.
  2. Next up, is the jobs section is where we define each of the jobs for our workflow, where we have a job called build-and-push this will run on the latest Ubuntu environment that is available in the GitHub Actions platform.
  3. Following that, we have the definition for each of the steps for this job:
    1. Starting with actions/checkout, this is a standard action which makes sure that the code stored in our repo is available within the workflow for the duration of the job.
    2. Next, we use the aws-actions/configure-aws-credentials@v3 action to set up the AWS CLI using the secrets that we created and configured earlier, this will set up our authorised connection to AWS.
    3. In the next step, we use the aws-actions/amazon-ecr-login@v2 action, which will use the connection to AWS from the previous step to secure a connection to AWS ECR.
    4. Finally, the Build and push images step will build our ECR URL from details that we provided as well as some details which it can retrieve from the AWS CLI and run our make action_build command, which builds our binaries, and then builds our images, it will then loop through each service defined in the docker compose (excluding some services that we can provide to exclude) and tag each image with the release tag and ECR repository location, finally pushing the images to the ECR

Next, is the schedule or trigger for our action to run, for this we have:

 on:
   release:
     types: [published]

Our action will trigger on the publishing of a release for this repo.

Wrapping it up:

Automating the deployment of images to your ECR repository can significantly speed up your development process, if you followed the steps outline in this post you can set up a robust and simple pipeline that makes sure you have the right images available when you need, just by publishing a release in your microservices repository.

The key benefits of this approach is to increase your overall development efficiency, and reduce any manual steps like manually building and tagging every image.

Don't forget to subscribe for more tips and tutorials on streamlining your development processes.

Thanks for reading!

Further reading:

For those interested to read more, here are some links to related topics for further reading: