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
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:
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 servicesAfter 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:
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 timeNeither 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."
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
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
## Create the GitOps repositorymkdir gitops-platform && cd gitops-platformgit init ## Create directory structuremkdir -p apps/stagingmkdir -p apps/productionmkdir -p services/swiggy-clone/basemkdir -p services/swiggy-clone/overlays/stagingmkdir -p services/swiggy-clone/overlays/productionmkdir -p infrastructure/monitoringmkdir -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.
# 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/v1kind: Deploymentmetadata: name: backend # namespace is set by the overlay — not herespec: # 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: 2apiVersion: v1kind: Servicemetadata: name: backend-servicespec: selector: app: backend ports: - name: http port: 4000 targetPort: 4000# 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/v1beta1kind: 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 basecommonLabels: app.kubernetes.io/managed-by: argocd project: swiggy-clone2.3 Staging Overlay — Lightweight Configuration
# services/swiggy-clone/overlays/staging/kustomization.yaml# This overlay customises the base for the staging environment apiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomization # Start with everything from the baseresources: - ../../base # Add staging-specific labels to all resourcescommonLabels: environment: staging # Set the namespace for all resourcesnamespace: swiggy-clone-staging # Override the container image tag# 'newTag' replaces only the tag — the registry and image name stay from baseimages: - 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 manifestpatches: # 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# services/swiggy-clone/overlays/production/kustomization.yaml apiVersion: kustomize.config.k8s.io/v1beta1kind: 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: backend2.4 Verify Kustomize Renders Correctly
Before connecting ArgoCD, verify the overlays render the manifests correctly.
## Preview what the staging overlay produceskubectl 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 overlaykubectl 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.
## Create ArgoCD namespacekubectl create namespace argocd ## Install ArgoCDkubectl apply -n argocd \ -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml ## Wait for all ArgoCD components to be readykubectl 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 installARGOCD_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 UIkubectl port-forward svc/argocd-server -n argocd 8080:443 &## Open https://localhost:8080## Username: admin## Password: the value from aboveInstall ArgoCD CLI
## The CLI lets you interact with ArgoCD from the terminal## Useful for scripting and automation ## Linuxcurl -sSL -o argocd \ https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64chmod +x argocd && sudo mv argocd /usr/local/bin/ ## Log in via CLIargocd 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:
## Connect ArgoCD to your private GitHub repository## Replace with your actual repository URLargocd repo add https://github.com/your-username/gitops-platform \ --username your-github-username \ --password your-github-personal-access-token ## Verify the connectionargocd 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.
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
# apps/staging/swiggy-clone-app.yaml# This manifest tells ArgoCD to watch the staging overlay apiVersion: argoproj.io/v1alpha1kind: Applicationmetadata: 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.iospec: # 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# 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/v1alpha1kind: Applicationmetadata: name: swiggy-clone-production namespace: argocd labels: environment: production app: swiggy-clone finalizers: - resources-finalizer.argocd.argoproj.iospec: 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: 2m4.2 Create the Root Application
This is the single Application you create manually. Everything else is managed by ArgoCD from this point.
# root-app-staging.yaml# The root Application for staging — manages all staging app manifests apiVersion: argoproj.io/v1alpha1kind: Applicationmetadata: name: root-staging namespace: argocd finalizers: - resources-finalizer.argocd.argoproj.iospec: 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# root-app-production.yaml# The root Application for production apiVersion: argoproj.io/v1alpha1kind: Applicationmetadata: name: root-production namespace: argocd finalizers: - resources-finalizer.argocd.argoproj.iospec: 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## Push everything to GitHub firstgit 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 Applicationskubectl apply -f root-app-staging.yamlkubectl apply -f root-app-production.yaml ## Watch ArgoCD create the child Applicationskubectl 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 applicationargocd app sync swiggy-clone-stagingargocd app wait swiggy-clone-staging --sync --health ## Check pods are running in stagingkubectl get pods -n swiggy-clone-stagingPart 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:
Developer pushes new code │ vCI pipeline builds image, tags it v1.5.0, pushes to ECR │ vDeveloper updates staging overlay: images: newTag: v1.5.0 git commit "deploy: update backend to v1.5.0 in staging" git push │ vArgoCD detects change → automatically deploys to staging │ vQA team tests staging environment │ vQA approves → developer opens pull request to update production overlay │ vTech lead reviews and approves the PR │ vPR merges → git commit: "deploy: promote backend v1.5.0 to production" │ vArgoCD detects OutOfSync in production │ vPlatform Engineer reviews the diff and clicks Sync in ArgoCD UI │ vv1.5.0 is now in productionEvery 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
## 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.comexport NEW_VERSION=v1.5.0 ## Build and push new imagedocker build -t ${ECR_URL}/swiggy-backend:${NEW_VERSION} ./backendaws 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 versioncd gitops-platformsed -i "s/newTag: .*/newTag: ${NEW_VERSION}/" \ services/swiggy-clone/overlays/staging/kustomization.yaml ## Commit and push — this is the deploymentgit add services/swiggy-clone/overlays/staging/kustomization.yamlgit commit -m "deploy: update backend to ${NEW_VERSION} in staging"git push origin main ## Watch ArgoCD automatically detect and deploy the changeargocd 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 runningkubectl get pods -n swiggy-clone-staging -o json | \ jq -r '.items[].spec.containers[].image'## Should show the ECR URL with :v1.5.05.2 Promote to Production via Pull Request
## 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.yamlgit 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 syncargocd app get swiggy-clone-production## STATUS: OutOfSync ← change detected but not applied yet ## Review the diff before syncingargocd 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, syncargocd app sync swiggy-clone-productionargocd app wait swiggy-clone-production --sync --health echo "✅ v1.5.0 is now running in production"5.3 Emergency Rollback
## Something went wrong with v1.5.0 in production## Rollback in 2 minutes: ## Option 1: Revert the Git commitgit revert HEAD --no-editgit 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 truthPart 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.
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 AUTOMATICALLY6.1 ApplicationSet with Git Generator
# applicationsets/production-services.yaml# This ApplicationSet auto-creates an Application for every service# in the services/*/overlays/production/ directory structure apiVersion: argoproj.io/v1alpha1kind: ApplicationSetmetadata: name: production-services namespace: argocdspec: 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# applicationsets/staging-services.yaml# Same pattern for staging — but with automated sync apiVersion: argoproj.io/v1alpha1kind: ApplicationSetmetadata: name: staging-services namespace: argocdspec: 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=truekubectl apply -f applicationsets/staging-services.yamlkubectl apply -f applicationsets/production-services.yaml ## Verify ApplicationSets created the Applicationskubectl get applications -n argocd ## Test: add a new service directory and watch it get deployed automaticallymkdir -p services/order-service/overlays/staging## Copy the swiggy-clone overlay as a starting pointcp 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-stagingkubectl get applications -n argocd --watch## order-service-staging should appear within ~3 minutesPart 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:
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 firedThis 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
## Argo Rollouts is a separate controller from ArgoCD## It extends Kubernetes with the Rollout resource typekubectl 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 rolloutscurl -LO \ https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-amd64chmod +x kubectl-argo-rollouts-linux-amd64sudo 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.
# services/swiggy-clone/base/rollout.yaml# Replace the Deployment with a Rollout resource apiVersion: argoproj.io/v1alpha1kind: Rollout # ← This is the key change — not Deployment but Rolloutmetadata: name: backendspec: # 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-ingress7.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.
# services/swiggy-clone/base/analysis-template.yaml apiVersion: argoproj.io/v1alpha1kind: AnalysisTemplatemetadata: name: backend-success-ratespec: 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.957.4 Deploy and Watch a Canary Rollout
## 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 stagingcd services/swiggy-clone/overlays/stagingsed -i 's/newTag: v1.5.0/newTag: v1.6.0/' kustomization.yamlgit add .git commit -m "deploy: update backend to v1.6.0 in staging"git push ## Watch the canary rollout in real timekubectl 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-stagingPart 8 — Production Checklist
## ─── 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-stagingkubectl 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.
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:
## Step 1: See exactly what is differentargocd 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 cacheargocd app get swiggy-clone-staging --refresh ## Step 4: Check for errors preventing syncargocd app get swiggy-clone-staging --show-operation## Look for: Operation State = Failed, with the reason in the messageRollout stuck in Paused state:
## Check rollout statuskubectl argo rollouts get rollout backend -n swiggy-clone-staging ## If stuck waiting for analysis:## Check if the AnalysisRun completed successfully or failedkubectl get analysisrun -n swiggy-clone-stagingkubectl 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 safekubectl argo rollouts promote backend -n swiggy-clone-staging ## Manually abort if something looks wrongkubectl argo rollouts abort backend -n swiggy-clone-stagingApplicationSet not creating Applications for new service:
## Check ApplicationSet statuskubectl 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 validkubectl kustomize services/your-new-service/overlays/staging/## If this errors, ArgoCD will skip this directory ## Force ApplicationSet to re-scankubectl 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.
✅ 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 backThe 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.