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.
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 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) ---
### 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 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." ``` 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/ ``` ---
### 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" ``` ---
### 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" ``` ---
### 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 ```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 ``` ---
### 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 │ 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 ``` ---
In Capstone 1 you deployed the application using kubectl apply. In Capstone 2 you provisioned the infrastructure with Te...
The Problem We Are Solving Imagine you have 10 services and 3 environments. Without a good GitOps structure you end up w...
2.1 Create the Repository Structure 2.2 Base Manifests — Write Once, Use Everywhere The base directory contains the Kube...
What Is ArgoCD and How Does It Work? ArgoCD is a GitOps tool that runs inside your Kubernetes cluster. It watches your G...
Why App of Apps? Managing many applications manually means creating an ArgoCD Application for each service individually....
What Is Promotion? Promotion is the process of moving a new version from staging to production. In most teams it looks l...
The Problem ApplicationSets Solve Imagine you have 15 microservices and 3 environments. With App of Apps you have 45 App...
What Is Progressive Delivery and Why Does It Matter? Even with GitOps, deploying a new version to all pods at once is ri...
---...
❌ Enabling automated sync for production 💥 A developer merges a PR that accidentally deletes a NetworkPolicy from the p...
Application stuck in OutOfSync: Rollout stuck in Paused state: ApplicationSet not creating Applications for new service:...
You now have a complete GitOps delivery platform. Every deployment is a Git commit. Every rollback is a Git revert. Ever...
Aligns directly with DevOps, Site Reliability (SRE), and Platform Engineering job descriptions.