DEV Community

Cover image for GlobalMart — CI/CD Pipeline on AWS
Mayank Thakur
Mayank Thakur

Posted on Edited on

GlobalMart — CI/CD Pipeline on AWS

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.


Architecture

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/**/*
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then confirm, in order:

  1. CodePipeline shows Source succeeded within ~30 seconds
  2. CodeBuild logs show npm run build completing
  3. CodeDeploy shows all five lifecycle events green
  4. curl http://<EC2-public-IP> returns the app
  5. 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
codedeploy-agent
, confirm the instance profile, check egress.

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 StatusCheckFailed and 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
Enter fullscreen mode Exit fullscreen mode

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)