DEV Community

Cover image for CI/CD for your Dockerized App with AWS CodeBuild, CodeDeploy and CodePipeline (Part 3/3)
Tanmoy Basak Anjan
Tanmoy Basak Anjan

Posted on

CI/CD for your Dockerized App with AWS CodeBuild, CodeDeploy and CodePipeline (Part 3/3)

This is the final part of a three-part series where we build a production-ready, auto-scaled and continuously deployed Node.js application on AWS.

In Part 1, we dockerized a Node.js app, pushed the image to AWS ECR and deployed it to an EC2 instance. In Part 2, we created an AMI, a launch template, an Application Load Balancer and an Auto Scaling Group so our app scales automatically.

In this part, we wire up a full CI/CD pipeline. Every push to main on GitHub will:

  1. Trigger AWS CodeBuild to build a new Docker image and push it to ECR
  2. Trigger AWS CodeDeploy via AWS CodePipeline to deploy the new image to every running EC2 instance

Make sure your code is hosted in a GitHub repository before continuing.


Prepare the codebase

Before touching AWS, we need to add three files to the repository: a build spec for CodeBuild and a set of deployment scripts for CodeDeploy.

Add buildspec.yml

Create a buildspec.yml file at the root of your repository. CodeBuild reads this file to know how to build and push your Docker image.

# Do not change version. This is the buildspec version, not your file version.
version: 0.2

env:
  variables:
    AWS_REGION: "<aws_region>"         # e.g. "ap-south-1"
    AWS_ACCOUNT_ID: "<aws_account_id>" # find this in your ECR repository URI
    IMAGE_REPO_NAME: "my-app"          # your ECR repository name
    IMAGE_TAG: "latest"
  parameter-store:
    APP_NAME: "/my-app/APP_NAME"       # add any other env variables here

phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...
      - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG .
      - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker image...
      - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
Enter fullscreen mode Exit fullscreen mode

What this does:

  • Defines the AWS account variables and ECR repository details
  • Reads environment variables from Parameter Store (the same ones from Part 1)
  • Logs in to ECR, builds the Docker image and pushes the new image

Add deployment scripts

CodeDeploy needs scripts that run on each EC2 instance during a deployment. Create a folder called aws-scripts in your repository and add the following three files.

aws-scripts/before_install.sh

#!/bin/bash
echo "Before install"
Enter fullscreen mode Exit fullscreen mode

aws-scripts/application_stop.sh

#!/bin/bash
echo "Application stop"
Enter fullscreen mode Exit fullscreen mode

aws-scripts/application_start.sh

This is the important one. It pulls the latest Docker image, refreshes the .env file from Parameter Store, restarts the container and reloads nginx.

#!/bin/bash

cd /home/ubuntu

AWS_ACCOUNT_ID="<aws_account_id>"
AWS_REGION="<aws_region>"
IMAGE_TAG="latest"
APP_NAME="my-app"
PORT="8000"

# Pull latest image from ECR
aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com
docker pull $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$APP_NAME:$IMAGE_TAG

# Refresh .env from Parameter Store
aws ssm get-parameters-by-path \
    --path "/${APP_NAME}" --recursive --with-decrypt \
    | jq -r '.Parameters[] | (.Name | split("/")[-1]) + "=" + (.Value)' \
    | tee /home/ubuntu/.env

# Stop existing containers and clean up
docker stop $(docker ps -q)
docker system prune -f

# Start the updated container
docker run --env-file .env -p $PORT:$PORT -d $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$APP_NAME:$IMAGE_TAG

# Restart nginx
sudo nginx -t
sudo service nginx restart
Enter fullscreen mode Exit fullscreen mode

Add appspec.yml

Create an appspec.yml file at the root of your repository. This tells CodeDeploy which scripts to run at each stage of the deployment.

version: 0.0
os: linux
files:
  - source: /
    destination: /home/ubuntu/my-app
    overwrite: true
hooks:
  ApplicationStart:
    - location: aws-scripts/application_start.sh
      timeout: 3000
      runas: ubuntu
file_exists_behavior: OVERWRITE
Enter fullscreen mode Exit fullscreen mode

Commit and push all these changes to main before moving on to AWS.


AWS CodeBuild

CodeBuild will run our buildspec.yml every time the pipeline is triggered. It will build the Docker image and push it to ECR.

Create an IAM role for CodeBuild

Search for IAM in the AWS panel and go to Roles. Click Create role.

  • Under Trusted entity type, select AWS service
  • Under Use case, select CodeBuild
  • Add these permission policies: AmazonEC2ContainerRegistryFullAccess, AmazonSSMFullAccess
  • Name the role my-app-code-build-role
  • Click Create role

Create the CodeBuild project

Search for CodeBuild in the AWS panel and click Create project.

Follow these steps:

  • Project name: my-app-code-build, Project type: Default
  • Source: Choose GitHub, connect your GitHub account and select your repository

CodeBuild - project name and source configuration

  • Environment: On-demand, Managed image, EC2, Container
  • Operating system: Ubuntu, Standard runtime
  • Service role: Choose the my-app-code-build-role we just created

CodeBuild - environment and service role configuration

  • Buildspec: Choose Use a buildspec file — this picks up the buildspec.yml from the repository
  • Artifacts: No artifacts — we only need to push to ECR
  • Click Create project

CodeBuild - buildspec and artifacts configuration

You can go to your CodeBuild project and click Start build to verify the build works before setting up the pipeline.


AWS CodeDeploy

CodeDeploy handles getting the latest image from ECR onto each EC2 instance in our Auto Scaling Group.

Prerequisite: The AMI we created in Part 2 already has the CodeDeploy agent installed. If your instances are running, they should have it. You can verify with sudo service codedeploy-agent status on any running instance.

Create an IAM role for CodeDeploy

Go to IAMRoles and click Create role.

  • Under Trusted entity type, select AWS service
  • Under Use case, select CodeDeploy
  • Keep the default AWSCodeDeployRole permission that is pre-selected
  • Name the role my-app-code-deploy-role
  • Click Create role

After creating the role, we need to attach a few more permissions. Click on the role, then Add permissionsAttach policies, and add the following:

  • AmazonEC2ContainerRegistryFullAccess — to pull images from ECR
  • AmazonS3FullAccess — to access pipeline artifacts from S3
  • AutoScalingFullAccess — to interact with the Auto Scaling Group
  • AWSCodePipeline_FullAccess — to work within the pipeline

Create a CodeDeploy application

Search for CodeDeploy in the AWS panel, go to Applications and click Create application.

  • Application name: my-app-code-deployment-application
  • Compute platform: EC2/On-premises

CodeDeploy - create application

Click Create application.

Create a deployment group

Inside the application we just created, click Create deployment group.

  • Deployment group name: my-app-deployment-group
  • Service role: Choose my-app-code-deploy-role

CodeDeploy - deployment group name and service role

  • Environment configuration: Select Amazon EC2 Auto Scaling groups and choose my-app-asg (the Auto Scaling Group from Part 2)
  • Deployment settings: CodeDeployDefault.AllAtOnce

CodeDeploy - environment configuration and deployment settings

  • Load balancer: Uncheck Enable load balancing

CodeDeploy - load balancer unchecked

Click Create deployment group.


AWS CodePipeline

Now we connect everything together. The pipeline will watch for pushes to main, trigger CodeBuild to build and push a new image, then trigger CodeDeploy to roll it out to all instances.

Search for CodePipeline in the AWS panel and click Create pipeline.

Follow these steps:

  • Creation options: Build custom pipeline
  • Pipeline name: my-app-code-pipeline, Execution mode: Queued
  • Service role: Create a new service role — no need to reuse an existing one. Click next.

CodePipeline - pipeline name and service role

  • Source: Choose GitHub (via GitHub App), select your repository and main branch
  • Under Webhook events, select Start your pipeline on push or pull request event. Under Webhook event filters, set Event type to Push, Filter type to Branch and value to main. This ensures only pushes to main fire the pipeline. Click next.

CodePipeline - source stage with GitHub and webhook configuration

  • Build stage: Under Other build providers, choose AWS CodeBuild and select my-app-code-build. No environment variables needed — they come from Parameter Store. Set artifacts to No artifacts. Click next.

CodePipeline - build stage with CodeBuild

  • Test stage: Skip this stage.
  • Deploy stage: Choose AWS CodeDeploy. Select my-app-code-deployment-application as the application and my-app-deployment-group as the deployment group. Click next.

CodePipeline - deploy stage with CodeDeploy

  • Review everything and click Create pipeline.

CodePipeline - review page

CodePipeline - pipeline created and running

Note: If you run into issues with No artifacts in either the build or deploy stage, switch them to SourceArtifact to resolve it.


Test the pipeline

Inside your CodePipeline, click Release change to trigger a manual run and make sure everything is wired up correctly.

Once that succeeds, test the full automated flow:

  1. Push a code change to the main branch on GitHub
  2. Watch CodeBuild pick up the push, build the Docker image and push it to ECR
  3. Watch CodeDeploy roll out the new image to your running EC2 instances
  4. Visit your load balancer DNS (my-app-lb-xxxxxxxx.<aws_region>.elb.amazonaws.com) to see the updated application

Wrapping up

Over this three-part series we went from a simple Node.js app to a fully production-grade deployment on AWS:

  • Part 1: Dockerized the app, pushed it to ECR and deployed it to a single EC2 instance with environment variables managed in Parameter Store
  • Part 2: Created a base AMI, a launch template, an Application Load Balancer and an Auto Scaling Group so the app scales automatically
  • Part 3: Set up a full CI/CD pipeline — every push to main now automatically builds a new Docker image and deploys it to every running instance

You now have an auto-scaled, load-balanced, continuously deployed application running on AWS. 🎉

Top comments (0)