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
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+Jasmineunit 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
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 [email protected]
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
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)
Pipeline configuration highlights:
Source stage: Watches the S3 bucket (code-build-source-*) forsource.zipchanges. When a new zip lands, the pipeline fires.
Build stage: Delegates to thelab-appCodeBuild project. The outputBuildArtifactis 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 thelab-appapplication andin-placedeployment 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:
Source → Succeeded (green) — latestsource.zippulled from S3
Build → Succeeded — CodeBuild ran all phases, artifact uploaded
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
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
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:
ValidateServicescript failed → deployment failed on instance 1- CodeDeploy's
OneAtATimeconfig meant instance 2 was skipped — it never received the broken version - CodeDeploy triggered an automatic rollback — a new deployment was initiated using the last successful revision
- The rollback deployment showed
Initiating event: codeDeployRollbackin 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.
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
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
SOCIAL SHARE CARD GENERATOR