Skip to main content

Command Palette

Search for a command to run...

Transforming DevOps: Building a Production-Ready CI/CD Pipeline

Published
9 min read

A hands-on story of breaking things, learning, and automating a React app deployment to AWS

I built a fully automated CI/CD pipeline that takes a React application from code commit to production deployment on AWS in under 10 minutes. Along the way, I learned Jenkins, Docker, Terraform, Ansible, and AWS , while debugging real-world issues that taught me more than any tutorial ever could.

Tech Stack: Jenkins | Docker | Terraform | Ansible | AWS | React | Node.js

GitHub: https://github.com/akanksha106-code/Zomato


The Challenge: Manual Deployments Are Painful

Picture this: It's 2 AM. Your website is down. You need to deploy a fix. You scramble through a 20-step deployment document, praying you don't miss anything. Sound familiar?

This is exactly the problem DevOps automation solves. But as a developer transitioning into DevOps, I didn't just want to read about it—I wanted to build it.

So I set out to create a real CI/CD pipeline. Not a toy project. Not a "hello world" deployment. A production-pattern system that could actually be used in a real company.

Here's what I learned.


Part 1: The Vision

What I Wanted to Build

A system where:

  • A developer pushes code to GitHub

  • Magic happens automatically

  • The app deploys to AWS

  • Zero manual steps

  • Complete traceability

The Architecture

Developer (Git Push)
        ↓
    GitHub Repository
        ↓
    Jenkins CI/CD Server
        ↓
    [Clean → Build → Push → Provision → Deploy]
        ↓
    AWS EC2 Running App
        ↓
    Users Access Application

Simple in theory. But as I'd soon learn, the devil is in the details.


Part 2: The Setup

The Infrastructure

I started with a Jenkins server on AWS:

  • Instance Type: t2.medium (2 vCPU, 4GB RAM)

  • OS: Amazon Linux 2

  • Tools Installed: Java 21, Node.js 18, Docker, Terraform, Ansible, Git

The Application

A React-based food delivery app (Zomato clone):

  • Built with React 18.2.0

  • Material-UI for components

  • Dockerized for consistency

  • Runs on Node.js 16

The Pipeline Stages

I designed a 6-stage pipeline:

  1. Clean Workspace - Start fresh every time

  2. Git Checkout - Pull latest code

  3. Build Docker Image - Package the app

  4. Push to DockerHub - Store the artifact

  5. Terraform Provision - Create AWS infrastructure

  6. Ansible Deploy - Configure and deploy

Sounds straightforward, right? That's what I thought too.


Part 3: The Breaks

Problem #1: The Docker Tag Mystery

What Happened:

 Stage 1-4: All green
 Stage 6: Error pulling image akankshatech/zomatoapp:1.0 - 404 Not Found

My Initial Reaction: "But it just built successfully! I can see it in DockerHub!"

The Investigation: I logged into DockerHub and saw the image... tagged as latest, not 1.0.

Checked my Jenkinsfile:

sh "docker build -t akankshatech/zomatoapp:latest ."

Checked my Ansible playbook:

image: akankshatech/zomatoapp:1.0  # ← There's the problem!

The Learning: Configuration consistency matters. Every component must agree on tagging strategies. In production, I'd use semantic versioning or git commit SHAs for immutable tags.

The Fix:

# Updated Ansible playbook
image: akankshatech/zomatoapp:latest

Time Lost: 2 hours | Value Gained: Understanding of container registries and tagging strategies


Problem #2: Permission Denied

What Happened:

 permission denied while trying to connect to the Docker daemon socket
   unix:///var/run/docker.sock

The Confusion: I could run docker ps manually as the jenkins user. It worked perfectly. But in the pipeline? Permission denied.

The Investigation:

# Check groups
$ groups jenkins
jenkins : jenkins docker  # ← jenkins IS in docker group!

# Check socket permissions
$ ls -la /var/run/docker.sock
srw-rw---- 1 root docker  # ← docker group has access!

# So why doesn't it work?

The "Aha!" Moment: Group membership is assigned at login time. I had added jenkins to the docker group while Jenkins was already running. The running process didn't have the new group membership!

The Fix:

sudo usermod -aG docker jenkins
sudo systemctl restart jenkins  # ← This was the key!

The Learning: Linux group mechanics aren't dynamic. Processes inherit group membership at startup. This is why configuration management tools restart services after group changes.

Time Lost: 3 hours of frustrated Googling | Value Gained: Deep understanding of Linux permissions and process management


Problem #3: The Sudo Password Puzzle

What Happened:

 sudo: a terminal is required to read the password
   sudo: a password is required

Why This Was Confusing: Ansible has become: true in the playbook. SSH key authentication works. Why is it asking for a password?

The Understanding:

  • SSH key authentication ≠ Sudo authentication

  • SSH lets you connect to the server

  • Sudo is a separate authorization layer

  • Ansible can't provide a password in automated pipelines (no interactive terminal)

The Fix:

$ sudo visudo
# Added:
jenkins ALL=(ALL) NOPASSWD:ALL

The Learning: In production, this is a security risk. Better approaches:

  • Limited sudo permissions for specific commands only

  • Use AWS Systems Manager Session Manager instead of SSH

  • Container-based deployments that don't need sudo

Time Lost: 1 hour | Value Gained: Understanding authentication vs. authorization


Problem #4: The Port Mapping Mixup

What Happened: App deployed successfully, but accessing http://server-ip:8084 showed nothing.

The Investigation:

# Check container
$ docker ps
CONTAINER   STATUS    PORTS
zomatoapp   Up        0.0.0.0:8084->8081/tcp  # ← Wait, what?

# Check Dockerfile
EXPOSE 3000  # ← App listens on 3000, not 8081!

The Root Cause: I had copied the Ansible playbook from a different tutorial where the app used port 8081. My app uses 3000.

The Fix:

# Updated Ansible playbook
published_ports:
  - "8084:3000"  # Changed from 8084:8081

The Learning: Don't blindly copy-paste configuration. Understand what each parameter does. Port mapping is HOST:CONTAINER, not the other way around.


Part 4: The Build

After fixing these issues (and a few more I've spared you), I finally had a working pipeline.

The Pipeline in Action

Trigger: Push code to GitHub

[STAGE 1] Clean Workspace .....................  5s
[STAGE 2] Git Checkout ........................  8s
[STAGE 3] Build Docker Image ..................  156s
[STAGE 4] Push to DockerHub ...................  45s
[STAGE 5] Terraform Provision AWS .............  98s
[STAGE 6] Ansible Configure & Deploy ..........  134s

Total Time: 7 minutes 26 seconds
Status: SUCCESS

What Actually Happens

Stage 3: Build Docker Image

FROM node:16-slim
WORKDIR /app
COPY package*.json ./
RUN npm install              # ← Cached if packages unchanged!
COPY . .
RUN npm run build           # ← Creates production bundle
EXPOSE 3000
CMD ["npm", "start"]

Docker's layer caching is brilliant:

  • First build: 3 minutes

  • Subsequent builds (code changed, dependencies same): 45 seconds

  • 80% faster!

Stage 5: Terraform Provision

resource "aws_instance" "test-server" {
  ami           = "ami-0532be01f26a3de55"  # Amazon Linux 2
  instance_type = "t3.small"               # 2 vCPU, 2GB RAM

  # Creates server, waits for SSH, generates inventory
  provisioner "local-exec" {
    command = "echo '${self.public_ip}' > ./inventory"
  }
}

Terraform is declarative magic:

  • Reads configuration files

  • Compares to current AWS state

  • Creates only what's needed

  • Tracks everything in state file

Stage 6: Ansible Deploy

tasks:
  - name: Update packages
    yum: name="*" state=latest

  - name: Install Docker
    yum: name=docker state=present

  - name: Start Docker
    systemd: name=docker state=started enabled=yes

  - name: Deploy container
    docker_container:
      name: zomatoapp
      image: akankshatech/zomatoapp:latest
      state: started
      restart_policy: always
      published_ports:
        - "8084:3000"

Idempotency in action:

  • Run once: Installs everything

  • Run again: "Already done, skipping"

  • Run 100 times: Same result


Part 5: The Result

Container Status

$ docker ps
CONTAINER ID   IMAGE                            STATUS        PORTS
a1b2c3d4e5f6   akankshatech/zomatoapp:latest   Up 3 hours    0.0.0.0:8084->3000/tcp

Metrics That Matter

MetricBefore AutomationAfter Automation
Deployment Time45-60 minutes8-10 minutes
Manual Steps23 steps0 steps (1 git push)
Error Rate~30% (human error)<5% (mostly code bugs)
Rollback Time60+ minutes5 minutes (redeploy previous version)
Environment Consistency"Works on my machine"Identical everywhere
Documentation15-page Word docGit repository (self-documenting)

Part 6: The Learnings

Technical Skills Gained

1. Jenkins Administration

  • Plugin management, credential storage, pipeline scripting (Groovy), tool configuration

2. Docker Mastery

  • Multi-stage builds, layer caching optimization, port mapping, container orchestration basics

3. Terraform (Infrastructure as Code)

  • Resource management, state handling, provisioners, AWS provider

4. Ansible (Configuration Management)

  • Playbook writing, idempotency, module usage, SSH connection management

5. AWS Cloud Services

  • EC2 instance management, security groups, IAM roles and policies, key pair management

6. Linux System Administration

  • User and group management, systemd service control, package management (yum), file permissions

Problem-Solving Skills

Before this project: I'd Google an error and copy-paste the first solution.

After this project: I now:

  1. Read the full error message

  2. Understand what the system is trying to tell me

  3. Form a hypothesis

  4. Test systematically

  5. Understand the root cause

  6. Implement a proper fix

  7. Document what I learned

This is the real value.


Part 7: The Lessons

Lesson 1: Tutorials Teach Syntax, Problems Teach Concepts

The tutorial showed me the commands. The errors taught me:

  • How Docker layers work

  • Why Linux groups exist

  • What Terraform state does

  • How Ansible achieves idempotency

The problems were the best teachers.

Lesson 2: Production Is Different

This project works for learning. Production needs:

  • High availability

  • Security hardening

  • Monitoring and alerting

  • Disaster recovery

  • Cost optimization

  • Compliance requirements

Knowing the gap is as important as building the project.

Lesson 3: DevOps Is More Than Tools

It's about:

  • Culture: Breaking silos between dev and ops

  • Automation: Eliminating manual toil

  • Measurement: You can't improve what you don't measure

  • Sharing: Knowledge sharing and collaboration

The tools are just enablers.

Lesson 4: Documentation Matters

When I hit an error at 11 PM, good documentation saved me hours. I learned to:

  • Write clear error messages

  • Document configuration decisions

  • Keep a troubleshooting log

  • Share knowledge with others

This blog post is part of that.

Lesson 5: Learning Never Stops

After completing this project, I realized how much more there is:

  • Kubernetes and container orchestration

  • Service mesh (Istio, Linkerd)

  • GitOps (ArgoCD, Flux)

  • Observability (Prometheus, Grafana, Jaeger)

  • Chaos engineering

  • FinOps (cloud cost optimization)

DevOps is a journey, not a destination.


Key Takeaways

This project transformed me from someone who reads about DevOps to someone who does DevOps.

I can now:

  • Build CI/CD pipelines

  • Automate infrastructure

  • Debug complex systems

  • Make architectural decisions

  • Speak confidently about these tools in interviews

This is portfolio-worthy work.


Technical Appendix

Repository Structure

Zomato/
├── src/                    # React source code
├── public/                 # Static assets
├── terraform-files/
│   ├── main.tf            # Infrastructure definition
│   ├── provider.tf        # AWS provider
│   ├── ansible.cfg        # Ansible config
│   ├── ansiblebook.yml    # Deployment playbook
│   └── inventory          # Generated dynamically
├── Dockerfile             # Container definition
├── Jenkinsfile1          # Pipeline definition
├── package.json          # Dependencies
└── README.md             # Documentation

Key Configuration Files

Dockerfile:

FROM node:16-slim
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]

Jenkinsfile (Simplified):

pipeline {
    agent any
    stages {
        stage("Clean") { steps { cleanWs() } }
        stage("Checkout") { steps { git 'repo-url' } }
        stage("Build") { steps { sh 'docker build -t app .' } }
        stage("Push") { steps { sh 'docker push app' } }
        stage("Provision") { steps { sh 'terraform apply' } }
        stage("Deploy") { steps { ansiblePlaybook(...) } }
    }
}

Let's Connect!

I'm actively looking for DevOps opportunities where I can build and improve CI/CD pipelines, work with cloud infrastructure, and learn from experienced engineers.

Find me: