Build a GitOps Delivery Platform with ArgoCD

Build a complete GitOps delivery platform - App of Apps pattern, multi-environment promotion from staging to production, ApplicationSets for scale, and progressive delivery with Argo Rollouts canary deployments.

Domains & Technologies

Domains
CAPSTONEGITOPSAPP-OF-APPSAPPLICATIONSETSARGO-ROLLOUTSCANARYPROGRESSIVE-DELIVERYKUSTOMIZEMULTI-ENVIRONMENT
Technologies
ARGOCD

Blueprint Walkthrough

Before You Start — Read This First

In Capstone 1 you deployed the application using kubectl apply. In Capstone 2 you provisioned the infrastructure with Terraform. Both of these work — but neither is how professional Platform Engineering teams manage deployments at scale.

The problem with kubectl apply for production:

  • Anyone with kubectl access can deploy anything, anytime, with no record of who did what
  • If a pod is manually edited in the cluster, nobody knows — the change just silently exists
  • Deploying to 3 environments (dev, staging, production) means running the same commands 3 times and hoping nothing differs
  • Rolling back means remembering which file you applied and applying an older version

GitOps solves all of this. Git becomes the single source of truth. Every change to the cluster is a Git commit. Every deployment is traceable, reviewable, and reversible. The cluster continuously reconciles itself to match what Git says — any manual change is automatically reverted.

In this capstone you will build a complete GitOps delivery platform using ArgoCD. By the end, deploying a new version of the application from Capstone 1 to production will be a pull request — nothing more.

What you will build:

◈ DIAGRAM
GitHub Repository (gitops-platform)
├── apps/
│ ├── staging/ ← ArgoCD Applications for staging
│ └── production/ ← ArgoCD Applications for production
├── services/
│ └── swiggy-clone/
│ ├── base/ ← shared config
│ └── overlays/
│ ├── staging/ ← staging-specific values
│ └── production/ ← production-specific values
└── applicationsets/ ← auto-create Applications for new services

After this capstone you will be able to:

  • Set up the App of Apps pattern to manage many applications from one place
  • Use Kustomize overlays to manage staging and production with one set of base manifests
  • Promote a deployment from staging to production through a pull request
  • Use ApplicationSets to automatically onboard new services without manual ArgoCD setup
  • Deploy a new version with a canary rollout — 10% traffic, then 50%, then 100% — with automatic rollback if errors spike

Time to complete: 3-4 hours.

What you need before starting:

  • ArgoCD installed on your cluster (we cover this if not already installed)
  • The application from Capstone 1 (or any application with Kubernetes manifests)
  • A GitHub account for the GitOps repository
  • Basic understanding of what ArgoCD does (covered in the GitOps module)

Part 1 — Understanding the Architecture

The Problem We Are Solving

Imagine you have 10 services and 3 environments. Without a good GitOps structure you end up with:

SQL
Option A: One big folder
k8s/
payment-service-dev.yaml
payment-service-staging.yaml
payment-service-production.yaml
order-service-dev.yaml
order-service-staging.yaml
... 30 files, all slightly different, hard to keep in sync
Option B: Copy-paste everything
3 separate repositories, each with all the manifests
When you update a label, you update it in 3 places
They drift apart over time

Neither scales. The solution uses two tools together:

Kustomize handles the "same thing, different values" problem. You write the base configuration once, then write small patch files for each environment.

App of Apps handles the "many applications" problem. One parent ArgoCD Application points to a folder of Application manifests. Adding a new application means adding one YAML file.

How Kustomize Works

Think of Kustomize like this: your base YAML is a template. Each environment's overlay is a set of sticky notes on top of that template that say "change this value here."

YAML
Base:
replicas: 2
image: swiggy-backend:latest
cpu request: 100m
Staging overlay sticky notes:
replicas: 1 (override staging needs fewer)
image: swiggy-backend:v1.5.0 (specific version)
[cpu stays at 100m not overridden]
Production overlay sticky notes:
replicas: 5 (override production needs more)
image: swiggy-backend:v1.4.9 (different version tested longer)
[cpu stays at 100m not overridden]

The base configuration stays DRY (Don't Repeat Yourself). Differences live only in the overlay.

How App of Apps Works

◈ DIAGRAM
ArgoCD watches: gitops-platform/apps/production/
apps/production/ contains:
swiggy-backend-app.yaml ← Application manifest
swiggy-frontend-app.yaml ← Application manifest
redis-app.yaml ← Application manifest
postgres-app.yaml ← Application manifest
ArgoCD creates one Application for each file
Each Application watches its own service's manifests in Git
Adding a new service = adding one YAML file to apps/production/

Part 2 — Set Up the GitOps Repository

2.1 Create the Repository Structure

Bash
## Create the GitOps repository
mkdir gitops-platform && cd gitops-platform
git init
## Create directory structure
mkdir -p apps/staging
mkdir -p apps/production
mkdir -p services/swiggy-clone/base
mkdir -p services/swiggy-clone/overlays/staging
mkdir -p services/swiggy-clone/overlays/production
mkdir -p infrastructure/monitoring
mkdir -p applicationsets
echo "✅ GitOps repository structure created"

2.2 Base Manifests — Write Once, Use Everywhere

The base directory contains the Kubernetes manifests that are identical across all environments. We extract the common parts from Capstone 1.

YAML
# services/swiggy-clone/base/deployment.yaml
# This is the COMMON configuration shared by all environments
# Environment-specific values will be overridden by overlays
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
# namespace is set by the overlay — not here
spec:
# replicas: not set here — each environment decides its own
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "4000"
prometheus.io/path: "/metrics"
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1001
containers:
- name: backend
# image tag is set by the overlay — not here
# Kustomize will replace this with the specific version
image: your-account.dkr.ecr.ap-south-1.amazonaws.com/swiggy-backend
ports:
- containerPort: 4000
env:
- name: PORT
value: "4000"
- name: DB_HOST
value: postgres-service
- name: REDIS_HOST
value: redis-service
envFrom:
- secretRef:
name: postgres-secret
# Base resources — overlays can increase these for production
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
livenessProbe:
httpGet:
path: /health/live
port: 4000
initialDelaySeconds: 15
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 4000
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 2
---
apiVersion: v1
kind: Service
metadata:
name: backend-service
spec:
selector:
app: backend
ports:
- name: http
port: 4000
targetPort: 4000
YAML
# services/swiggy-clone/base/kustomization.yaml
# This file tells Kustomize which resources are part of the base
# Every overlay starts with these resources and patches on top
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
# Also include database and cache manifests from Capstone 1
- postgres.yaml
- redis.yaml
- ingress.yaml
- hpa.yaml
- pdb.yaml
# Common labels added to ALL resources in this base
commonLabels:
app.kubernetes.io/managed-by: argocd
project: swiggy-clone

2.3 Staging Overlay — Lightweight Configuration

YAML
# services/swiggy-clone/overlays/staging/kustomization.yaml
# This overlay customises the base for the staging environment
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
# Start with everything from the base
resources:
- ../../base
# Add staging-specific labels to all resources
commonLabels:
environment: staging
# Set the namespace for all resources
namespace: swiggy-clone-staging
# Override the container image tag
# 'newTag' replaces only the tag — the registry and image name stay from base
images:
- name: your-account.dkr.ecr.ap-south-1.amazonaws.com/swiggy-backend
newTag: v1.5.0 # staging runs the latest tested version
# Patches: override specific fields without rewriting the whole manifest
patches:
# Staging only needs 1 replica (save cost)
- patch: |-
- op: replace
path: /spec/replicas
value: 1
target:
kind: Deployment
name: backend
# Staging uses smaller resource requests (save cost)
- patch: |-
- op: replace
path: /spec/template/spec/containers/0/resources/requests/cpu
value: "50m"
- op: replace
path: /spec/template/spec/containers/0/resources/requests/memory
value: "64Mi"
target:
kind: Deployment
name: backend
# Staging HPA: max 3 replicas (not 10 like production)
- patch: |-
- op: replace
path: /spec/maxReplicas
value: 3
target:
kind: HorizontalPodAutoscaler
name: backend-hpa
YAML
# services/swiggy-clone/overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
commonLabels:
environment: production
namespace: swiggy-clone-production
images:
- name: your-account.dkr.ecr.ap-south-1.amazonaws.com/swiggy-backend
newTag: v1.4.9 # production runs the version that passed staging validation
patches:
# Production: 5 replicas for load handling
- patch: |-
- op: replace
path: /spec/replicas
value: 5
target:
kind: Deployment
name: backend
# Production: larger resource requests
- patch: |-
- op: replace
path: /spec/template/spec/containers/0/resources/requests/cpu
value: "200m"
- op: replace
path: /spec/template/spec/containers/0/resources/limits/cpu
value: "1000m"
- op: replace
path: /spec/template/spec/containers/0/resources/requests/memory
value: "256Mi"
- op: replace
path: /spec/template/spec/containers/0/resources/limits/memory
value: "1Gi"
target:
kind: Deployment
name: backend

2.4 Verify Kustomize Renders Correctly

Before connecting ArgoCD, verify the overlays render the manifests correctly.

Bash
## Preview what the staging overlay produces
kubectl kustomize services/swiggy-clone/overlays/staging
## Look at the output:
## - namespace should be swiggy-clone-staging
## - replicas should be 1
## - image should have tag v1.5.0
## - labels should include environment: staging
## Preview production overlay
kubectl kustomize services/swiggy-clone/overlays/production
## - namespace should be swiggy-clone-production
## - replicas should be 5
## - image should have tag v1.4.9
echo "✅ Kustomize overlays render correctly"

Part 3 — Install ArgoCD

What Is ArgoCD and How Does It Work?

ArgoCD is a GitOps tool that runs inside your Kubernetes cluster. It watches your Git repository and continuously compares what is in Git (the desired state) with what is actually running in the cluster (the actual state).

When they differ — someone pushed a new image tag to Git, or someone manually edited something in the cluster — ArgoCD either:

  • Alerts you (OutOfSync status) and waits for you to approve the sync
  • Automatically syncs (if you enable automated sync)

Think of ArgoCD as a very attentive employee whose entire job is to make sure the cluster matches Git. It never sleeps, never forgets, and never makes a typo.

Bash
## Create ArgoCD namespace
kubectl create namespace argocd
## Install ArgoCD
kubectl apply -n argocd \
-f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
## Wait for all ArgoCD components to be ready
kubectl wait --for=condition=Ready pods \
--all -n argocd --timeout=300s
echo "All ArgoCD pods ready"
## Get the initial admin password
## ArgoCD auto-generates a secure password on first install
ARGOCD_PASSWORD=$(kubectl get secret argocd-initial-admin-secret \
-n argocd \
-o jsonpath="{.data.password}" | base64 -d)
echo "ArgoCD admin password: ${ARGOCD_PASSWORD}"
echo "Save this — you will need it to log in"
## Access the ArgoCD UI
kubectl port-forward svc/argocd-server -n argocd 8080:443 &
## Open https://localhost:8080
## Username: admin
## Password: the value from above

Install ArgoCD CLI

Bash
## The CLI lets you interact with ArgoCD from the terminal
## Useful for scripting and automation
## Linux
curl -sSL -o argocd \
https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
chmod +x argocd && sudo mv argocd /usr/local/bin/
## Log in via CLI
argocd login localhost:8080 \
--username admin \
--password "${ARGOCD_PASSWORD}" \
--insecure ## --insecure because we are using port-forward, not a real TLS cert
echo "✅ ArgoCD CLI configured"

Connect ArgoCD to Your GitHub Repository

Before ArgoCD can read your manifests, you need to give it access to the repository. For public repositories this is not needed. For private repositories:

Bash
## Connect ArgoCD to your private GitHub repository
## Replace with your actual repository URL
argocd repo add https://github.com/your-username/gitops-platform \
--username your-github-username \
--password your-github-personal-access-token
## Verify the connection
argocd repo list
## Shows: REPO URL | TYPE | STATUS
## Status should be "Successful"

Part 4 — App of Apps Pattern

Why App of Apps?

Managing many applications manually means creating an ArgoCD Application for each service individually. When you have 20 services, that is 20 separate kubectl apply commands. When you add a new service, you have to remember to create its ArgoCD Application. When you delete a service, you have to remember to delete its Application.

The App of Apps pattern automates all of this. One parent Application watches a directory of Application manifests. Any Application YAML you add to that directory gets automatically created by ArgoCD.

◈ DIAGRAM
Root Application (you create this once)
watches: gitops-platform/apps/production/
apps/production/ contains:
backend-app.yaml → ArgoCD creates Application "backend"
frontend-app.yaml → ArgoCD creates Application "frontend"
redis-app.yaml → ArgoCD creates Application "redis"
You add a new file:
postgres-app.yaml → ArgoCD automatically creates Application "postgres"

4.1 Create the Application Manifests

YAML
# apps/staging/swiggy-clone-app.yaml
# This manifest tells ArgoCD to watch the staging overlay
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: swiggy-clone-staging
namespace: argocd
labels:
environment: staging
app: swiggy-clone
# This finalizer means: when this Application is deleted,
# also delete the Kubernetes resources it manages
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
# project controls what this Application is allowed to deploy to
# 'default' has no restrictions — we set up proper projects later
project: default
source:
# Replace with your actual GitHub repository URL
repoURL: https://github.com/your-username/gitops-platform
targetRevision: HEAD # always use the latest commit
path: services/swiggy-clone/overlays/staging
destination:
server: https://kubernetes.default.svc
namespace: swiggy-clone-staging # matches namespace in the overlay
syncPolicy:
automated:
prune: true # delete resources removed from Git
selfHeal: true # revert manual changes
syncOptions:
- CreateNamespace=true # create namespace if it doesn't exist
retry:
limit: 3
backoff:
duration: 30s
factor: 2
maxDuration: 2m
YAML
# apps/production/swiggy-clone-app.yaml
# Production is the same structure but NO automated sync
# Every production deployment requires a manual approval
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: swiggy-clone-production
namespace: argocd
labels:
environment: production
app: swiggy-clone
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/your-username/gitops-platform
targetRevision: HEAD
path: services/swiggy-clone/overlays/production
destination:
server: https://kubernetes.default.svc
namespace: swiggy-clone-production
syncPolicy:
# NO automated sync for production
# Changes sit in OutOfSync state until a human reviews and approves
syncOptions:
- CreateNamespace=true
retry:
limit: 3
backoff:
duration: 30s
factor: 2
maxDuration: 2m

4.2 Create the Root Application

This is the single Application you create manually. Everything else is managed by ArgoCD from this point.

YAML
# root-app-staging.yaml
# The root Application for staging — manages all staging app manifests
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root-staging
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/your-username/gitops-platform
targetRevision: HEAD
path: apps/staging # watches the apps/staging directory
destination:
server: https://kubernetes.default.svc
namespace: argocd # Application resources live in argocd namespace
syncPolicy:
automated:
prune: true
selfHeal: true
YAML
# root-app-production.yaml
# The root Application for production
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root-production
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/your-username/gitops-platform
targetRevision: HEAD
path: apps/production
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
Bash
## Push everything to GitHub first
git add .
git commit -m "feat: add App of Apps structure with Kustomize overlays"
git push origin main
## Now create the two root Applications
## ArgoCD will immediately detect them and create all child Applications
kubectl apply -f root-app-staging.yaml
kubectl apply -f root-app-production.yaml
## Watch ArgoCD create the child Applications
kubectl get applications -n argocd --watch
## You should see:
## root-staging Synced Healthy
## root-production Synced Healthy
## swiggy-clone-staging Synced Healthy
## swiggy-clone-production OutOfSync (waiting for manual sync)
## Sync the staging application
argocd app sync swiggy-clone-staging
argocd app wait swiggy-clone-staging --sync --health
## Check pods are running in staging
kubectl get pods -n swiggy-clone-staging

Part 5 — Environment Promotion Workflow

What Is Promotion?

Promotion is the process of moving a new version from staging to production. In most teams it looks like this:

Bash
Developer pushes new code
v
CI pipeline builds image, tags it v1.5.0, pushes to ECR
v
Developer updates staging overlay:
images: newTag: v1.5.0
git commit "deploy: update backend to v1.5.0 in staging"
git push
v
ArgoCD detects change → automatically deploys to staging
v
QA team tests staging environment
v
QA approves → developer opens pull request to update production overlay
v
Tech lead reviews and approves the PR
v
PR merges → git commit: "deploy: promote backend v1.5.0 to production"
v
ArgoCD detects OutOfSync in production
v
Platform Engineer reviews the diff and clicks Sync in ArgoCD UI
v
v1.5.0 is now in production

Every deployment is:

  • Tracked in Git history
  • Reviewed as a pull request
  • Approved by a human before going to production
  • Instantly reversible with git revert

5.1 Deploy a New Version to Staging

Bash
## Simulate the CI pipeline: build and push a new image version
## (In real life, GitHub Actions does this automatically)
export ECR_URL=your-account.dkr.ecr.ap-south-1.amazonaws.com
export NEW_VERSION=v1.5.0
## Build and push new image
docker build -t ${ECR_URL}/swiggy-backend:${NEW_VERSION} ./backend
aws ecr get-login-password --region ap-south-1 | \
docker login --username AWS --password-stdin ${ECR_URL}
docker push ${ECR_URL}/swiggy-backend:${NEW_VERSION}
## Update the staging overlay with the new version
cd gitops-platform
sed -i "s/newTag: .*/newTag: ${NEW_VERSION}/" \
services/swiggy-clone/overlays/staging/kustomization.yaml
## Commit and push — this is the deployment
git add services/swiggy-clone/overlays/staging/kustomization.yaml
git commit -m "deploy: update backend to ${NEW_VERSION} in staging"
git push origin main
## Watch ArgoCD automatically detect and deploy the change
argocd app get swiggy-clone-staging --watch
## Health status will show Progressing then Healthy
## The new pods will have the new image
## Verify the new version is running
kubectl get pods -n swiggy-clone-staging -o json | \
jq -r '.items[].spec.containers[].image'
## Should show the ECR URL with :v1.5.0

5.2 Promote to Production via Pull Request

Bash
## After staging validation, promote to production
## In a real team this is a pull request from a branch
## For this tutorial we commit directly to main
PROD_VERSION=v1.5.0 # same version that passed staging
sed -i "s/newTag: .*/newTag: ${PROD_VERSION}/" \
services/swiggy-clone/overlays/production/kustomization.yaml
git add services/swiggy-clone/overlays/production/kustomization.yaml
git commit -m "deploy: promote backend ${PROD_VERSION} to production [approved by: team-lead]"
git push origin main
## Production Application is now OutOfSync — waiting for manual sync
argocd app get swiggy-clone-production
## STATUS: OutOfSync ← change detected but not applied yet
## Review the diff before syncing
argocd app diff swiggy-clone-production
## Shows exactly what will change:
## - image: swiggy-backend:v1.4.9
## + image: swiggy-backend:v1.5.0
## If the diff looks correct, sync
argocd app sync swiggy-clone-production
argocd app wait swiggy-clone-production --sync --health
echo "✅ v1.5.0 is now running in production"

5.3 Emergency Rollback

Bash
## Something went wrong with v1.5.0 in production
## Rollback in 2 minutes:
## Option 1: Revert the Git commit
git revert HEAD --no-edit
git push origin main
## ArgoCD detects the change and rolls back automatically
## Option 2: ArgoCD rollback (rolls back without Git — for true emergencies)
argocd app rollback swiggy-clone-production
## Rolls back to the previous sync revision immediately
## Follow up with a Git revert afterward to keep Git as the source of truth

Part 6 — ApplicationSets — GitOps at Scale

The Problem ApplicationSets Solve

Imagine you have 15 microservices and 3 environments. With App of Apps you have 45 Application YAML files (15 services × 3 environments). Adding a new service means creating 3 new files (one per environment). Deleting a service means deleting 3 files.

ApplicationSets generate Application objects automatically from a template. Instead of writing 45 files, you write one ApplicationSet that generates all 45 automatically.

◈ DIAGRAM
ApplicationSet with Git generator:
"For every directory in services/*/overlays/production/,
create an ArgoCD Application"
services/swiggy-clone/overlays/production/ → Application: swiggy-clone-production
services/order-service/overlays/production/ → Application: order-service-production
services/payment-service/overlays/production/ → Application: payment-service-production
When you add services/user-service/overlays/production/:
→ Application: user-service-production is created AUTOMATICALLY

6.1 ApplicationSet with Git Generator

YAML
# applicationsets/production-services.yaml
# This ApplicationSet auto-creates an Application for every service
# in the services/*/overlays/production/ directory structure
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: production-services
namespace: argocd
spec:
generators:
# Git generator: scans the repository for matching directories
# and creates one Application per match
- git:
repoURL: https://github.com/your-username/gitops-platform
revision: HEAD
# Match any directory that follows the pattern services/*/overlays/production
directories:
- path: "services/*/overlays/production"
# Template: what each generated Application looks like
# {{ path.basename }} = the service name (e.g. swiggy-clone)
# {{ path }} = the full path (e.g. services/swiggy-clone/overlays/production)
template:
metadata:
# Name format: service-name-production
name: "{{path.basenameNormalized}}-production"
namespace: argocd
labels:
environment: production
# Which ApplicationSet created this Application
app.kubernetes.io/managed-by: applicationset
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/your-username/gitops-platform
targetRevision: HEAD
path: "{{path}}" # the matched directory
destination:
server: https://kubernetes.default.svc
# Namespace format: service-name-production
namespace: "{{path.basenameNormalized}}-production"
syncPolicy:
syncOptions:
- CreateNamespace=true
# No automated sync for production — manual approval required
YAML
# applicationsets/staging-services.yaml
# Same pattern for staging — but with automated sync
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: staging-services
namespace: argocd
spec:
generators:
- git:
repoURL: https://github.com/your-username/gitops-platform
revision: HEAD
directories:
- path: "services/*/overlays/staging"
template:
metadata:
name: "{{path.basenameNormalized}}-staging"
namespace: argocd
labels:
environment: staging
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/your-username/gitops-platform
targetRevision: HEAD
path: "{{path}}"
destination:
server: https://kubernetes.default.svc
namespace: "{{path.basenameNormalized}}-staging"
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Bash
kubectl apply -f applicationsets/staging-services.yaml
kubectl apply -f applicationsets/production-services.yaml
## Verify ApplicationSets created the Applications
kubectl get applications -n argocd
## Test: add a new service directory and watch it get deployed automatically
mkdir -p services/order-service/overlays/staging
## Copy the swiggy-clone overlay as a starting point
cp services/swiggy-clone/overlays/staging/kustomization.yaml \
services/order-service/overlays/staging/kustomization.yaml
git add services/order-service/
git commit -m "feat: add order-service to staging"
git push
## Watch ArgoCD automatically create and deploy order-service-staging
kubectl get applications -n argocd --watch
## order-service-staging should appear within ~3 minutes

Part 7 — Progressive Delivery with Argo Rollouts

What Is Progressive Delivery and Why Does It Matter?

Even with GitOps, deploying a new version to all pods at once is risky. If there is a bug in v1.5.0, 100% of users hit it immediately.

Progressive delivery is the practice of gradually shifting traffic to a new version while monitoring for problems. The most common pattern is a canary deployment:

◈ DIAGRAM
Before deployment: 100% traffic → v1.4.9 (stable)
Step 1: Deploy canary
10% traffic → v1.5.0 (canary)
90% traffic → v1.4.9 (stable)
Wait 5 minutes, check error rate
Step 2: If metrics look good, increase
50% traffic → v1.5.0
50% traffic → v1.4.9
Wait 5 minutes, check error rate
Step 3: If metrics still good, full promotion
100% traffic → v1.5.0
v1.4.9 removed
If at ANY step error rate exceeds 5%:
100% traffic → v1.4.9 (automatic rollback)
v1.5.0 removed
Alert fired

This is how Swiggy, Zomato, and Razorpay deploy to production with confidence. Instead of "deploy and pray," they deploy to a small percentage, verify it is healthy, and gradually increase.

7.1 Install Argo Rollouts

Bash
## Argo Rollouts is a separate controller from ArgoCD
## It extends Kubernetes with the Rollout resource type
kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts \
-f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml
kubectl wait --for=condition=Ready pods \
--all -n argo-rollouts --timeout=120s
## Install the kubectl plugin for managing rollouts
curl -LO \
https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-amd64
chmod +x kubectl-argo-rollouts-linux-amd64
sudo mv kubectl-argo-rollouts-linux-amd64 /usr/local/bin/kubectl-argo-rollouts
echo "✅ Argo Rollouts installed"

7.2 Convert Deployment to Rollout

A Rollout is a drop-in replacement for a Kubernetes Deployment. Everything about it is the same — pods, labels, selectors, resource requests — except it adds the canary strategy configuration.

YAML
# services/swiggy-clone/base/rollout.yaml
# Replace the Deployment with a Rollout resource
apiVersion: argoproj.io/v1alpha1
kind: Rollout # ← This is the key change — not Deployment but Rollout
metadata:
name: backend
spec:
# replicas is set by the overlay (same as before)
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "4000"
prometheus.io/path: "/metrics"
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1001
containers:
- name: backend
image: your-account.dkr.ecr.ap-south-1.amazonaws.com/swiggy-backend
ports:
- containerPort: 4000
env:
- name: PORT
value: "4000"
- name: DB_HOST
value: postgres-service
- name: REDIS_HOST
value: redis-service
envFrom:
- secretRef:
name: postgres-secret
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
livenessProbe:
httpGet:
path: /health/live
port: 4000
initialDelaySeconds: 15
periodSeconds: 10
readinessProbe:
httpGet:
path: /health/ready
port: 4000
initialDelaySeconds: 10
periodSeconds: 5
# ── Canary Strategy ─────────────────────────────────────────
strategy:
canary:
# Steps execute in order when a new version is deployed
steps:
# Step 1: Send 10% of traffic to the new version
# The remaining 90% still goes to the old stable version
- setWeight: 10
# Step 2: Wait 5 minutes and observe metrics
# If someone manually approves, skip the wait
- pause: {duration: 5m}
# Step 3: Analyse metrics automatically
# If the AnalysisTemplate passes, continue
# If it fails, automatically roll back to the stable version
- analysis:
templates:
- templateName: backend-success-rate
# Step 4: Increase to 50% traffic
- setWeight: 50
# Step 5: Wait another 5 minutes
- pause: {duration: 5m}
# Step 6: Analyse again before full promotion
- analysis:
templates:
- templateName: backend-success-rate
# Step 7: 100% — promotion complete
# Old version removed automatically
# Traffic management via Nginx Ingress
# Argo Rollouts adjusts Ingress weights automatically
trafficRouting:
nginx:
stableIngress: backend-ingress

7.3 AnalysisTemplate — Automatic Pass/Fail Criteria

An AnalysisTemplate defines what "success" means for a canary deployment. Argo Rollouts queries Prometheus during the analysis steps and decides whether to continue or roll back.

YAML
# services/swiggy-clone/base/analysis-template.yaml
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: backend-success-rate
spec:
metrics:
- name: success-rate
# Query Prometheus for the success rate of the backend
# This calculates: (successful requests / total requests) over 5 minutes
provider:
prometheus:
address: http://monitoring-kube-prometheus-prometheus.monitoring.svc:9090
query: |
sum(
rate(
http_requests_total{
app="backend",
status!~"5.." # exclude 5xx errors
}[5m]
)
)
/
sum(
rate(
http_requests_total{app="backend"}[5m]
)
)
# Analyse once per minute, for 5 minutes
interval: 1m
count: 5
# Success condition: 99% or higher success rate
successCondition: result[0] >= 0.99
# Failure condition: below 95% — rollback immediately
failureCondition: result[0] < 0.95

7.4 Deploy and Watch a Canary Rollout

Bash
## Update the base kustomization to use the Rollout instead of Deployment
## Edit services/swiggy-clone/base/kustomization.yaml:
## Replace deployment.yaml with rollout.yaml
## Add analysis-template.yaml
git add services/swiggy-clone/base/
git commit -m "feat: convert backend to Rollout with canary strategy"
git push
## Trigger a new deployment by updating the image tag in staging
cd services/swiggy-clone/overlays/staging
sed -i 's/newTag: v1.5.0/newTag: v1.6.0/' kustomization.yaml
git add .
git commit -m "deploy: update backend to v1.6.0 in staging"
git push
## Watch the canary rollout in real time
kubectl argo rollouts get rollout backend -n swiggy-clone-staging --watch
## Output looks like:
## Name: backend
## Namespace: swiggy-clone-staging
## Status: ॥ Paused
## Strategy: Canary
## Step: 1/7
## SetWeight: 10
## ActualWeight: 10
## Images:
## your-ecr/swiggy-backend:v1.5.0 (stable) 9 pods
## your-ecr/swiggy-backend:v1.6.0 (canary) 1 pod
## After 5 minutes, watch it move to Step 3 (analysis)
## If analysis passes, it moves to 50%
## If analysis fails, it automatically rolls back
## Manual commands:
## Approve and skip pause:
kubectl argo rollouts promote backend -n swiggy-clone-staging
## Abort rollout immediately (100% back to stable):
kubectl argo rollouts abort backend -n swiggy-clone-staging

Part 8 — Production Checklist
Bash
## ─── 1. Kustomize overlays render without errors ─────────────
kubectl kustomize services/swiggy-clone/overlays/staging | kubectl apply --dry-run=client -f -
kubectl kustomize services/swiggy-clone/overlays/production | kubectl apply --dry-run=client -f -
## Both should show "configured" or "created" — no errors
## ─── 2. Root Applications are Synced and Healthy ─────────────
kubectl get applications -n argocd \
-o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.sync.status}{"\t"}{.status.health.status}{"\n"}{end}'
## root-staging and root-production should both show Synced Healthy
## ─── 3. Staging has automated sync, Production does not ──────
kubectl get application swiggy-clone-staging -n argocd \
-o jsonpath='{.spec.syncPolicy.automated}'
## Should show: {"prune":true,"selfHeal":true}
kubectl get application swiggy-clone-production -n argocd \
-o jsonpath='{.spec.syncPolicy.automated}'
## Should show: null (not configured — manual sync required)
## ─── 4. Different image tags in staging and production ────────
kubectl get rollout backend -n swiggy-clone-staging \
-o jsonpath='{.spec.template.spec.containers[0].image}'
## Should show v1.5.0 or v1.6.0
kubectl get rollout backend -n swiggy-clone-production \
-o jsonpath='{.spec.template.spec.containers[0].image}'
## Should show v1.4.9 (production runs the older, validated version)
## ─── 5. ApplicationSets are managing Applications ─────────────
kubectl get applicationsets -n argocd
## Should show: staging-services and production-services
## ─── 6. Argo Rollouts controller is running ──────────────────
kubectl get pods -n argo-rollouts
## Should show rollouts-controller Running
## ─── 7. Rollback works ───────────────────────────────────────
## Test rollback by aborting the canary (safe in staging)
kubectl argo rollouts abort backend -n swiggy-clone-staging
kubectl argo rollouts status backend -n swiggy-clone-staging
## Should show: Degraded (aborted) then return to Healthy after rollback
echo "✅ Production checklist complete"

Common Production Mistakes

Enabling automated sync for production 💥 A developer merges a PR that accidentally deletes a NetworkPolicy from the production overlay. ArgoCD detects the Git change and immediately syncs — deleting the NetworkPolicy in production. Now all inter-pod traffic is blocked. The application is down. ✅ Production should never have automated sync. Changes should sit in OutOfSync state until a human reviews the diff in the ArgoCD UI and explicitly clicks Sync.


Using HEAD in production and staging pointing to the same commit 💥 A developer pushes a broken change to the main branch. ArgoCD syncs it to staging — expected. But production is also pointing to HEAD on the same branch. ArgoCD syncs it to production too. Both environments are broken at the same time. ✅ Staging and production should track different versions. Use environment-specific image tags in the overlays. Consider separate branches for staging and production with a promotion process between them.


No sync waves — resources deploy in wrong order 💥 The ApplicationSet deploys all resources simultaneously. The backend Deployment starts before the PostgreSQL StatefulSet is ready. The backend pods fail their readiness probes because the database is not up. They keep crashing. Eventually the database starts, pods recover — but it takes 5 minutes and generates many error logs and alerts. ✅ Use sync wave annotations to control order. Database resources get wave -1 (deploy first), applications get wave 0 (deploy second), post-deploy jobs get wave 1.

YAML
metadata:
annotations:
argocd.argoproj.io/sync-wave: "-1" ## deploys before wave 0

Canary with only 2 total pods — percentages are meaningless 💥 The backend Deployment has replicas: 2. The canary is set to 10%. 10% of 2 pods = 0.2 pods. Kubernetes rounds up to 1 pod. The "canary" is actually serving 50% of traffic — not 10%. The whole gradual rollout strategy is bypassed. ✅ For meaningful traffic percentages, use Nginx Ingress weight-based routing (not pod-count-based routing) or ensure enough replicas that 10% is at least 1 pod and represents a real traffic split.


Deleting the root Application without cascade delete 💥 An engineer deletes the root-production Application to "clean up." Without the finalizer, ArgoCD deletes the Application object but leaves all the managed child Applications running. Those Applications continue syncing but are now unmanaged — the root no longer knows about them. The production environment is now orphaned. ✅ Always keep the resources-finalizer.argocd.argoproj.io finalizer on parent Applications. When you delete the root Application, ArgoCD cascades the deletion to all child Applications and their managed resources cleanly.


Debugging Playbook

Application stuck in OutOfSync:

Bash
## Step 1: See exactly what is different
argocd app diff swiggy-clone-staging
## Shows: - (what Git says) vs + (what cluster has)
## Read carefully — is this an expected difference or a problem?
## Step 2: Check if the difference is caused by something that should be ignored
## Common legitimate differences:
## - HPA-managed replicas (should add ignoreDifferences for /spec/replicas)
## - Istio-injected sidecars (should ignore injected containers)
## Step 3: Force a refresh to check if ArgoCD has stale cache
argocd app get swiggy-clone-staging --refresh
## Step 4: Check for errors preventing sync
argocd app get swiggy-clone-staging --show-operation
## Look for: Operation State = Failed, with the reason in the message

Rollout stuck in Paused state:

Bash
## Check rollout status
kubectl argo rollouts get rollout backend -n swiggy-clone-staging
## If stuck waiting for analysis:
## Check if the AnalysisRun completed successfully or failed
kubectl get analysisrun -n swiggy-clone-staging
kubectl describe analysisrun <run-name> -n swiggy-clone-staging
## Look at: metric results, error messages
## If Prometheus is unreachable:
kubectl exec -n swiggy-clone-staging deployment/backend -- \
wget -qO- http://monitoring-kube-prometheus-prometheus.monitoring.svc:9090/api/v1/query?query=up
## If this fails, check the Prometheus service name
## Manually promote if you are confident the version is safe
kubectl argo rollouts promote backend -n swiggy-clone-staging
## Manually abort if something looks wrong
kubectl argo rollouts abort backend -n swiggy-clone-staging

ApplicationSet not creating Applications for new service:

Bash
## Check ApplicationSet status
kubectl describe applicationset staging-services -n argocd | tail -20
## Check if the directory structure matches the pattern
## The path in ApplicationSet is: services/*/overlays/staging
## Your directory must match exactly:
ls services/your-new-service/overlays/staging/
## Must contain kustomization.yaml
## Check if kustomization.yaml is valid
kubectl kustomize services/your-new-service/overlays/staging/
## If this errors, ArgoCD will skip this directory
## Force ApplicationSet to re-scan
kubectl annotate applicationset staging-services -n argocd \
argocd.argoproj.io/refresh="$(date +%s)"

What You Have Built

You now have a complete GitOps delivery platform. Every deployment is a Git commit. Every rollback is a Git revert. Every new service gets deployed automatically.

◈ DIAGRAM
✅ GitOps repository with clean base/overlay Kustomize structure
✅ Staging environment: automated sync, deploys on every push
✅ Production environment: manual approval required, change is reviewed before syncing
✅ App of Apps pattern managing all applications from one root
✅ ApplicationSets auto-creating Applications for new services
✅ Promotion workflow: staging → production via pull request
✅ Canary deployments: 10% → 50% → 100% with automatic rollback
✅ Prometheus-based analysis deciding whether to promote or roll back

The three capstones now form a complete picture:

  • Capstone 1 — you have a real application
  • Capstone 2 — you have real infrastructure for it to run on
  • Capstone 3 — you have a proper delivery platform to deploy it

A Platform Engineer who has completed these three capstones has done real work. Not simulated work. Not tutorial work. Real, production-pattern work that directly maps to what teams like Razorpay, Hotstar, and CRED do every day.

Next: Capstone 4 — Build a Mini Internal Developer Platform. You will wrap everything you have built in a Backstage developer portal so other teams can self-serve new services without knowing anything about the underlying Kubernetes, Terraform, or GitOps setup.

Videos & Guides

No external walkthroughs, video tutorials, or reference files attached to this blueprint yet.