Zero-touch deployment of a React e-commerce app from GitHub to EC2 using AWS CodePipeline and CodeDeploy.
Push to main, walk away. The pipeline builds the app, versions the artifact in S3, and rolls it
out to EC2 with health checks and automatic rollback — no SSH, no manual scp, no downtime window.
Why this project exists
Manually deploying a build to a server is fine once. It stops being fine the tenth time: someone
copies the wrong folder, forgets to restart the web server, or deploys from a dirty local branch,
and nobody can say which commit is live.
This project replaces that with a pipeline where the commit SHA is the single source of truth for
what is running in production. The goal was not "get a React app onto EC2" — it was to build the
release path properly: versioned artifacts, an agent-driven deploy with lifecycle hooks, health
verification before traffic is served, and rollback on failure.
flowchart LR
DEV[Developer] -->|git push main| GH[GitHub repository]
GH -->|CodeStar Connection webhook| CP[AWS CodePipeline]
subgraph CP_STAGES [Pipeline]
direction TB
SRC[Source stage] --> BUILD[Build stage<br/>CodeBuild + Node.js]
BUILD --> DEPLOY[Deploy stage<br/>CodeDeploy]
end
CP --> SRC
BUILD -->|versioned artifact| S3[(S3 artifact bucket)]
S3 --> DEPLOY
DEPLOY -->|pull + lifecycle hooks| AGENT[CodeDeploy agent on EC2]
AGENT --> NGINX[Nginx serving /var/www/globalmart]
NGINX --> USER[End user]
AGENT -.->|deploy + app logs| CW[CloudWatch Logs]
CP -.->|state change events| SNS[SNS notification]
IAM[IAM roles:<br/>pipeline, build, deploy, instance profile] -.-> CP_STAGES
Trust boundaries worth noting: the pipeline role can read the artifact bucket and start
deployments; the EC2 instance profile can only read that one bucket. The instance never
holds credentials that can modify the pipeline. Compromising the web server does not give an
attacker the ability to ship code.
Pipeline stages
| Stage | Provider | What happens | Failure behaviour |
|---|---|---|---|
| Source | GitHub via CodeStar Connections | Webhook fires on push to main, source zipped to S3 |
Pipeline stops, no artifact produced |
| Build | AWS CodeBuild (buildspec.yml) |
npm ci then npm run build, output packaged with deploy scripts |
Pipeline stops before anything reaches the server |
| Deploy | AWS CodeDeploy (appspec.yml) |
Agent pulls artifact, runs lifecycle hooks, swaps web root | Automatic rollback to last successful revision |
CodeDeploy runs OneAtATime against the deployment group, with the deployment marked failed if
ValidateService cannot get a 200 from the app — so a broken build never stays live.
Deployment lifecycle hooks
ApplicationStop → scripts/stop_server.sh stop nginx gracefully
BeforeInstall → scripts/before_install.sh install deps, clear old web root
Install → (CodeDeploy copies files) artifact → /var/www/globalmart
AfterInstall → scripts/after_install.sh set ownership + permissions
ApplicationStart → scripts/start_server.sh start and enable nginx
ValidateService → scripts/validate_service.sh curl health check, fail non-200
Repository layout
Globalmart-cicd-pipeline/
├── appspec.yml # CodeDeploy instructions — MUST be at repo root
├── buildspec.yml # CodeBuild build steps
├── scripts/
│ ├── before_install.sh
│ ├── after_install.sh
│ ├── start_server.sh
│ ├── stop_server.sh
│ └── validate_service.sh
├── src/ # React application source
├── public/
├── screenshots/ # pipeline runs, deploy events, live app
└── README.md
Configuration
buildspec.yml
version: 0.2
phases:
install:
runtime-versions:
nodejs: 20
pre_build:
commands:
- npm ci
build:
commands:
- npm run build
post_build:
commands:
- echo "Build completed for commit $CODEBUILD_RESOLVED_SOURCE_VERSION"
artifacts:
files:
- build/**/*
- appspec.yml
- scripts/**/*
appspec.yml and scripts/ must be listed in artifacts — if they are not in the bundle,
CodeDeploy has nothing to execute and fails immediately.
appspec.yml
version: 0.0
os: linux
files:
- source: /build
destination: /var/www/globalmart
hooks:
ApplicationStop:
- location: scripts/stop_server.sh
timeout: 60
runas: root
BeforeInstall:
- location: scripts/before_install.sh
timeout: 300
runas: root
AfterInstall:
- location: scripts/after_install.sh
timeout: 120
runas: root
ApplicationStart:
- location: scripts/start_server.sh
timeout: 120
runas: root
ValidateService:
- location: scripts/validate_service.sh
timeout: 120
runas: root
scripts/validate_service.sh
The hook that makes this a real pipeline rather than a file copy:
#!/bin/bash
set -e
for i in {1..10}; do
code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/ || true)
if [ "$code" = "200" ]; then
echo "Health check passed on attempt $i"
exit 0
fi
echo "Attempt $i returned $code, retrying..."
sleep 3
done
echo "Health check failed — triggering rollback"
exit 1
IAM roles used
Four distinct roles, each scoped to one job. This is the part most tutorials collapse into one
over-permissioned role.
| Role | Attached to | Permissions |
|---|---|---|
CodePipelineServiceRole |
CodePipeline | Read/write artifact bucket, start CodeBuild, create CodeDeploy deployments, use the CodeStar connection |
CodeBuildServiceRole |
CodeBuild project | Write artifacts to S3, write CloudWatch Logs |
CodeDeployServiceRole |
CodeDeploy application |
AWSCodeDeployRole — describe and tag EC2 instances |
EC2InstanceProfile |
EC2 instance |
s3:GetObject on the artifact bucket only, plus CloudWatch agent permissions |
The instance profile is intentionally read-only and single-bucket. No s3:*, no wildcard resource.
Setup: reproduce this pipeline
1. Prepare the EC2 instance
# Amazon Linux 2023, t3.micro, security group: 22 (your IP only) + 80 (0.0.0.0/0)
sudo dnf update -y
sudo dnf install -y nginx ruby wget
sudo systemctl enable --now nginx
# CodeDeploy agent (region-specific bucket)
cd /home/ec2-user
wget https://aws-codedeploy-ap-south-1.s3.ap-south-1.amazonaws.com/latest/install
chmod +x ./install
sudo ./install auto
sudo systemctl status codedeploy-agent # must be active
Tag the instance Name=globalmart-prod — CodeDeploy targets the deployment group by tag.
2. Create the AWS resources
- S3 bucket for artifacts, versioning enabled, all public access blocked
- CodeDeploy application (EC2/On-premises) + deployment group targeting the instance tag
- CodeBuild project pointing at
buildspec.yml - CodePipeline: Source (GitHub via CodeStar Connection) → Build → Deploy
- Attach the instance profile to the EC2 instance
3. Verify
git commit --allow-empty -m "test: trigger pipeline"
git push origin main
Then confirm, in order:
- CodePipeline shows Source succeeded within ~30 seconds
- CodeBuild logs show
npm run buildcompleting - CodeDeploy shows all five lifecycle events green
-
curl http://<EC2-public-IP>returns the app - On the instance:
tail -f /var/log/aws/codedeploy-agent/codedeploy-agent.log
Troubleshooting: what actually broke
Real failures hit while building this, and the fixes. Kept here because these are the things
tutorials skip.
ApplicationStop failed on the very first deployment
The hook script from the previous revision runs during ApplicationStop, and on deployment #1
there is no previous revision. Fix: use "Ignore ApplicationStop lifecycle event failures" on the
first run, or make the script idempotent with systemctl stop nginx || true.
InstallError: The deployment failed because a specified file already exists
CodeDeploy will not overwrite files it did not place there. Fix: rm -rf /var/www/globalmart/*
in before_install.sh, or set the file overwrite behaviour on the deployment group.
ScriptFailed with exit code 126
Shell scripts committed without the execute bit. Fix: git update-index --chmod=+x scripts/*.sh,
or chmod +x inside before_install.sh before the hooks run.
npm run build killed on a t2.micro
1 GB of RAM is not enough for a Vite/CRA production build; the OOM killer takes the process and
the log just says "Killed." Fix: build in CodeBuild rather than on the instance (which is what
this pipeline does), or add swap on the instance if you must build there.
Deployment stuck at Pending, then timed out
The CodeDeploy agent could not reach the service. Causes seen: agent not running, no instance
profile attached, or a private subnet with no NAT and no VPC endpoint. Fix: systemctl status, confirm the instance profile, check egress.
codedeploy-agent
AccessDenied pulling the artifact from S3
The instance profile lacked s3:GetObject on the artifact bucket, and separately the bucket was
in a different region than the deployment. Fix: scope the policy to that bucket ARN and keep
bucket, pipeline, and instance in one region.
Monitoring and observability
- CloudWatch Logs: CodeBuild build logs, plus the CodeDeploy agent log shipped from the instance
- CloudWatch Alarm on EC2
StatusCheckFailedand high CPU - SNS topic subscribed to CodePipeline state-change events, so a failed deploy emails immediately
- Nginx access and error logs streamed via the CloudWatch agent
Cost and teardown
Running cost on free tier is effectively zero; outside it, roughly a few dollars a month —
t3.micro on-demand, S3 storage in the low cents, CodePipeline at one dollar per active pipeline
per month, CodeBuild billed per build minute.
Teardown, in this order, to avoid orphaned charges:
# 1. delete the pipeline 2. delete the CodeBuild project
# 3. delete the CodeDeploy app + deployment group
# 4. empty and delete the artifact bucket (versioned — delete all versions)
# 5. terminate EC2 6. release any Elastic IP 7. delete the IAM roles
An unreleased Elastic IP on a terminated instance is the classic surprise bill.
Production hardening — what I would add next
Honest list of what this pipeline does not do yet:
- Terraform for the whole stack, so the pipeline itself is version-controlled and not click-configured
- Blue/green deployment behind an ALB and Auto Scaling group, replacing in-place deploys and eliminating the brief serve gap
- Test and security gates in the build stage: unit tests,
npm audit, and SAST as blocking steps - Manual approval action before production, with a staging deployment group ahead of it
- S3 + CloudFront for the static React build instead of EC2 — cheaper, faster, and no server to patch
- Secrets via Parameter Store / Secrets Manager injected at build time rather than baked into the artifact
- Container path: Docker image to ECR, deployed to ECS Fargate, for reproducible runtime environments
What this project demonstrates
- End-to-end CI/CD design: source, build, artifact versioning, deploy, verify, roll back
- CodeDeploy lifecycle hooks and health-gated releases, not just automated file copying
- Least-privilege IAM across four separate service roles
- Linux service administration and web server configuration on EC2
- Debugging distributed deploy failures from agent logs
- Cost awareness and clean resource teardown
Pipeline executions, CodeDeploy lifecycle events, EC2 status, and the running application
Author
Mayank Thakur — Cloud, DevOps and SRE
GitHub · LinkedIn · mayankthakur9181@gmail.com


Top comments (0)