DEV Community

Cover image for Hands-On AWS CI/CD: CodeBuild, CodeDeploy, CodePipeline & Zero-Downtime Blue/Green Releases

Hands-On AWS CI/CD: CodeBuild, CodeDeploy, CodePipeline & Zero-Downtime Blue/Green Releases

Introduction

If you've ever wondered how production teams ship code dozens of times a day without breaking things (or how they recover fast when they do), the answer almost always comes down to a solid CI/CD pipeline. In this post, I'm going to walk you through exactly how I built one end-to-end on AWS — from pushing code to a Git repository all the way through automated build, test, deploy, rollback, and finally a blue/green deployment strategy.

Here's what the full pipeline looks like at a high level:

Git Push → S3 (source) → AWS CodeBuild (build + test) → AWS CodeDeploy → EC2 (production)
                                    ↑
                           AWS CodePipeline orchestrates it all
Enter fullscreen mode Exit fullscreen mode

Let's go step by step.


Prerequisites

Before diving in, here's what was already in place in this lab environment (you'd provision these yourself in a real project):

  • An EC2 instance used as a development environment
  • A self-hosted Gitea SCM (Git-based source control)
  • An Auto Scaling Group with 2 EC2 production instances
  • An Application Load Balancer (ALB) targeting those instances
  • The CodeDeploy Agent pre-installed on production instances
  • IAM roles for CodeBuild, CodeDeploy, and CodePipeline

The application itself is a simple Node.js + Express app with an AngularJS frontend. It has:

  • A gulp-based build process
  • Karma + Jasmine unit tests
  • A dev mode (port 3000) and production mode (port 8080)

Step 1 — Committing Code to the Git Repository

The first step in any CI/CD pipeline is getting your code into source control. I connected to the EC2 dev instance via EC2 Instance Connect (browser-based SSH — no key pair needed), then:

# Navigate into the app directory and run the tests first
cd app
npm test

# Verify the app runs locally in dev mode
NODE_ENV=development DEBUG=aws-code-services:* npm start
# App is now accessible at http://<EC2-IP>:3000
Enter fullscreen mode Exit fullscreen mode

Once tests passed and the app looked good, I set up Git credentials and pushed to the remote repo:

# Configure Git identity
git config --global user.email student@platform.qa.com
git config --global user.name student
git config --global credential.helper store
echo "http://student:LabPassword123@<SCM-IP>:3000" > ~/.git-credentials

# Clone the empty remote repo
git clone http://<SCM-IP>:3000/student/app-repo.git
cd app-repo

# Copy the app source into the repo
cp -R ../app/. .

# Stage, commit, and push
git add -A
git commit -m "app v1.0"
git push
Enter fullscreen mode Exit fullscreen mode

At this point, app v1.0 is live in the remote SCM repository. The SCM was configured to automatically zip and upload the source to an S3 bucket (code-build-source-*) whenever a push lands — this is the bridge between the self-hosted SCM and CodeBuild, which doesn't natively support self-hosted Git.


Step 2 — Automated Build with AWS CodeBuild

With source code in S3, AWS CodeBuild picks it up and runs the build. The entire build is defined in a buildspec.yml file at the root of the project:

version: 0.2

phases:
  install:
    runtime-versions:
      nodejs: 18
    commands:
      - npm install   # Install ALL dependencies (including dev)

  pre_build:
    commands:
      - npm test      # Run automated unit tests
      - npm prune --production  # Remove dev dependencies

  build:
    commands:
      - npm run build # Production build via gulp (minification, bundling)

artifacts:
  files:
    - '**/*'          # Package everything for CodeDeploy
Enter fullscreen mode Exit fullscreen mode

What's happening in each phase:

Phase What it does
install Pulls in the Node.js runtime and installs all npm dependencies
pre_build Runs unit tests; if they fail, the build stops here. Then strips dev deps.
build Runs the gulp production build — minifies and bundles frontend assets
artifacts Packages the entire working directory into a ZIP and uploads to S3

The CodeBuild project was configured to:

  • Use an AWS-managed Docker container (no infrastructure to manage)
  • Store build artifacts in a dedicated S3 bucket (code-build-artifacts-*)
  • Log everything to Amazon CloudWatch Logs for debugging

I triggered a manual build to verify the setup, watched the phase details, and confirmed the artifact landed in S3. ✅


Step 3 — Configuring AWS CodeDeploy

This is where the actual deployment to EC2 happens. CodeDeploy uses an appspec.yml at the project root to know how to deploy:

version: 0.0
os: linux

files:
  - source: /
    destination: /home/ec2-user/app

hooks:
  ApplicationStop:
    - location: scripts/stop_server.sh
      timeout: 300

  ApplicationStart:
    - location: scripts/start_server.sh
      timeout: 300

  ValidateService:
    - location: scripts/validate_service.sh
      timeout: 300
Enter fullscreen mode Exit fullscreen mode

The lifecycle hooks are critical:

  • ApplicationStop — gracefully stops any running instance of the Node server
  • ApplicationStart — starts the server in production mode on port 8080
  • ValidateService — curls port 8080 and fails the deployment if the app doesn't respond

If any hook script returns a non-zero exit code, CodeDeploy fails the deployment and triggers a rollback automatically.

Creating the Deployment Application and Groups

In the CodeDeploy console, I created an application named lab-app and two deployment groups:

In-place deployment group (in-place):

  • Targets the Auto Scaling Group (lab-app-prod-asg)
  • Uses CodeDeployDefault.OneAtATime — updates one instance at a time, keeping the other serving traffic
  • ALB integration with connection draining enabled
  • Automatic rollback on failure ✅

Blue/green deployment group (blue-green):

  • Same ASG and ALB configuration
  • CodeDeploy provisions fresh EC2 instances for every deployment (no config drift)
  • Original instances kept running until cutover completes
  • Traffic switches via the ALB — zero downtime
  • Automatic rollback on failure ✅

Step 4 — Wiring It All Together with AWS CodePipeline

CodePipeline is the orchestrator that connects source → build → deploy into a single automated workflow.

I created a pipeline named lab-app with these stages:

[Source] → [Build] → [Production]
  S3         CodeBuild   CodeDeploy (in-place)
Enter fullscreen mode Exit fullscreen mode

Pipeline configuration highlights:

  • Source stage: Watches the S3 bucket (code-build-source-*) for source.zip changes. When a new zip lands, the pipeline fires.
  • Build stage: Delegates to the lab-app CodeBuild project. The output BuildArtifact is passed downstream.
  • Production stage: I added this manually after creating the pipeline (you can't rename stages, so skip the default "Deploy" to avoid the misleading label). It runs a CodeDeploy action pointing at the lab-app application and in-place deployment group. Automatic rollback on stage failure is enabled.

One important note: the Pipeline checks S3 periodically for changes. In production, you'd configure EventBridge (CloudWatch Events) for near-instant triggers, or use a native SCM integration if you're on GitHub, GitLab, or BitBucket.


Step 5 — Following a Successful Deployment

With the pipeline in place, I triggered it manually via Release change and watched each stage:

  1. Source → Succeeded (green) — latest source.zip pulled from S3
  2. Build → Succeeded — CodeBuild ran all phases, artifact uploaded
  3. Production → In Progress — CodeDeploy started the in-place rollout

Clicking into the CodeDeploy deployment view showed the lifecycle events per instance in real time:

BeforeAllowTraffic → ApplicationStop → ApplicationStart → ValidateService → AfterAllowTraffic
Enter fullscreen mode Exit fullscreen mode

Since OneAtATime was configured, one instance was taken out of the ALB at a time, upgraded, validated, then returned to service before the second instance was touched. The app stayed available throughout.

Final verification: I grabbed the ALB DNS name and loaded it in the browser. No "development mode" banner — confirmed production mode. Refreshing showed different server IPs alternating, proving both instances were serving traffic behind the load balancer. ✅


Step 6 — Intentional Failure and Automatic Rollback

This is where it gets interesting. I simulated a bad deployment by pushing v1.1 — a version where someone accidentally changed the server's listening port from 8080 to 80.

cp -R commits/v1_1/. app-repo/
cd app-repo
git add -A
git commit -m "app v1.1"
git push
Enter fullscreen mode Exit fullscreen mode

The pipeline triggered automatically. CodeBuild passed (the unit tests didn't catch a port misconfiguration — a realistic scenario). The deployment reached the ValidateService hook, which tried to curl localhost:8080... and got a connection refused.

What happened next, automatically:

  1. ValidateService script failed → deployment failed on instance 1
  2. CodeDeploy's OneAtATime config meant instance 2 was skipped — it never received the broken version
  3. CodeDeploy triggered an automatic rollback — a new deployment was initiated using the last successful revision
  4. The rollback deployment showed Initiating event: codeDeployRollback in the deployment history

The app was never fully broken in production. Only one instance briefly served the broken version, and it was rolled back before any real user impact. This is exactly why the ValidateService hook exists — your automated unit tests can't catch everything, but a post-deploy smoke test can.


Step 7 — Blue/Green Deployment

Finally, I switched the pipeline to use the blue-green deployment group and pushed v1.2 — a legitimate new feature (message emphasis toggles in the accumulator app).

cp -R commits/v1_2/. app-repo/
cd app-repo
git add -A
git commit -m "app v1.2"
git push
Enter fullscreen mode Exit fullscreen mode

The blue/green deployment flow:

  1. New (green) instances provisioned — CodeDeploy creates fresh EC2 instances from the ASG configuration
  2. App installed on green instancesv1.2 deployed and validated on the new instances
  3. Traffic rerouted — ALB gradually shifts traffic from blue (original) to green (new), OneAtATime
  4. Original (blue) instances retained — kept running for rollback capability

Watching the CodeDeploy console during this was genuinely satisfying: you could see the replacement instances appear, the lifecycle events complete, and traffic start routing to them — while the original instances continued serving users without interruption.

Final verification: Refreshed the app. New feature (emphasis toggles) was visible. Server IPs in the bottom corner were different from before — confirming entirely new instances were serving traffic, not the same ones from the in-place deployment. That's immutable infrastructure in action.


Key Takeaways

Concept What You Learned
buildspec.yml Defines CodeBuild phases: install → pre_build → build → artifacts
appspec.yml Defines CodeDeploy lifecycle hooks: stop → start → validate
In-place deployment Rolling update on existing instances; faster but risks config drift
Blue/green deployment New instances every time; zero-downtime cutover; immutable infra
Automatic rollback ValidateService hook + rollback config = self-healing pipeline

Architecture Diagram

Developer (EC2 dev-instance)
        │
        │ git push
        ▼
    SCM (Gitea)
        │
        │ webhook → uploads source.zip
        ▼
    Amazon S3 (source bucket)
        │
        │ triggers CodePipeline
        ▼
  AWS CodePipeline
   ┌────┴────────────────────────────────┐
   │                                     │
[Source]     [Build]              [Production]
  S3    →  CodeBuild        →    CodeDeploy
           (build + test)        (in-place or
           artifact → S3          blue/green)
                                      │
                              ┌───────┴───────┐
                              │               │
                           Instance 1    Instance 2
                           (EC2, prod)   (EC2, prod)
                              └───────┬───────┘
                                      │
                           Application Load Balancer
                                      │
                                   Users
Enter fullscreen mode Exit fullscreen mode

What's Next

This pipeline covers the core CI/CD loop. From here, you could extend it with:

  • Manual approval stage in CodePipeline before production (for regulated environments)
  • SNS notifications on pipeline success/failure
  • CloudWatch alarms tied to CodeDeploy to trigger rollbacks on metrics (not just script failures)
  • EventBridge rules for instant pipeline triggers instead of S3 polling
  • Parameter Store / Secrets Manager integration in the buildspec for managing environment variables securely

Built as part of the AWS CI/CD hands-on lab. Published under the AWS Builders community.

Tags: #aws #devops #cicd #codepipeline #codedeploy #codebuild #cloud #awscommunity

Top comments (0)