Production Readiness Challenge
The final test. Take a running application, inject real production failures - pod crashes, OOMKills, bad deployments, cost spikes, network issues - and learn to diagnose and fix each one.
Domains & Technologies
Blueprint Walkthrough
Before You Start — Read This First
Every other capstone taught you to build things. Capstone 1 built an application. Capstone 2 provisioned infrastructure. Capstone 3 created a delivery platform. Capstone 4 built a developer portal. All of them assumed things would go correctly.
This one does not.
This capstone is the difference between an engineer who has studied platform engineering and an engineer who operates it. In production, things break. Not occasionally — regularly. Memory leaks. Bad deployments. Network policies that are too strict. Databases that run out of connections. Cost spikes from a forgotten load test. You do not get to choose when these happen or what combination arrives together.
The only way to be ready is to have broken things intentionally, in a controlled environment, and learned how to find and fix each failure before it finds you at 2 AM.
Chaos engineering is the practice of deliberately injecting failures into a system to discover weaknesses before users do. Netflix invented it with Chaos Monkey — a service that randomly kills production pods to ensure the system can survive node failures. Google uses it. Razorpay runs chaos experiments before major sale events to verify the payment system holds under pressure. This capstone applies the same discipline.
How each scenario is structured:
- Setup — get the system into a known good state
- Inject — deliberately break something specific
- Detect — use observability tools to find the problem
- Fix — resolve the issue with the correct approach
- Prevent — add a safeguard so it cannot happen again undetected
Run every scenario on the staging cluster from Capstone 2. Never run chaos experiments on production without automated rollback, executive approval, and a full on-call team standing by.
Scenario 1 — Pod CrashLoopBackOff
Setup
Before injecting anything, verify the system is in a known good state.
## Verify all pods are running and healthykubectl get pods -n swiggy-clone-staging## All pods should show STATUS: Running, RESTARTS: 0 ## Verify the API is respondingcurl -s http://swiggy-clone-staging.your-cluster.example/health/ready## Should return: {"status": "ok"} echo "✅ System healthy — ready to inject failure"Inject
A CrashLoopBackOff happens when a pod starts, crashes immediately, and Kubernetes keeps restarting it in a loop. This usually means the application cannot start — often because a required environment variable or dependency is missing.
## Inject a bad environment variable — clear the database host## The backend expects DB_HOST to be set — clearing it causes it to crash on startupkubectl set env deployment/backend DB_HOST="" -n swiggy-clone-staging ## Watch what happenskubectl get pods -n swiggy-clone-staging --watch## Within 30 seconds you will see:## NAME READY STATUS RESTARTS## backend-xxx 0/1 CrashLoopBackOff 3## backend-xxx 0/1 Error 3## The restart count keeps climbingDetect
## Step 1: Confirm the CrashLoopBackOffkubectl get pods -n swiggy-clone-staging## STATUS column shows CrashLoopBackOff — the pod is crashing repeatedly ## Step 2: Read the current crash logskubectl logs deployment/backend -n swiggy-clone-staging## May be empty if the crash is very fast — use --previous instead ## Step 3: Read logs from the PREVIOUS container run## --previous reads logs from the last container that crashedkubectl logs deployment/backend -n swiggy-clone-staging --previous## Look for: "DB_HOST is required", "Cannot connect to database",## "Connection refused", or any startup error from your application code ## Step 4: Describe the pod for more detailkubectl describe pod -l app=backend -n swiggy-clone-staging | tail -30## Look for the Events section:## Warning BackOff 2m kubelet Back-off restarting failed container ## Step 5: Check if it is an environment variable problemkubectl exec deployment/backend -n swiggy-clone-staging -- env | grep DB## If the pod can start long enough to exec, you will see DB_HOST is empty## If it cannot, this command fails — which itself confirms the startup crashRemember
kubectl logswithout--previousshows the current container's logs. If a pod crashes immediately, there are no current logs. Always try--previousfirst for CrashLoopBackOff.
Fix
## Restore the correct database hostkubectl set env deployment/backend \ DB_HOST=postgres-service \ -n swiggy-clone-staging ## Watch the pods recoverkubectl rollout status deployment/backend -n swiggy-clone-staging## Output: deployment "backend" successfully rolled out ## Verify healthykubectl get pods -n swiggy-clone-staging## STATUS should return to Running, RESTARTS will show the crash count## but no new restarts should be occurringPrevent
The application should validate all required environment variables at startup and crash immediately with a clear message if any are missing. This makes CrashLoopBackOff self-diagnosing — the log message tells you exactly what is wrong.
// src/startup-validation.js// Add this as the first thing your application runs const REQUIRED_ENV_VARS = [ 'DB_HOST', 'DB_PORT', 'DB_NAME', 'DB_USER', 'DB_PASSWORD', 'REDIS_HOST', 'PORT',]; function validateEnvironment() { const missing = REQUIRED_ENV_VARS.filter(key => !process.env[key]); if (missing.length > 0) { // This message appears in kubectl logs --previous // Making it specific means the engineer reading it knows exactly what to fix console.error(`FATAL: Missing required environment variables: ${missing.join(', ')}`); console.error('The application cannot start without these values.'); console.error('Check your Kubernetes Secret and Deployment environment config.'); process.exit(1); // non-zero exit causes Kubernetes to count it as a crash }} validateEnvironment(); // call this before any other application codeScenario 2 — OOMKill — Pod Killed for Using Too Much Memory
What OOMKill is
When a Kubernetes pod exceeds its memory limit, the Linux kernel's OOM (Out of Memory) killer terminates the process. Kubernetes then restarts it. Unlike a crash from a bug, the pod did not fail — it was killed by the system for consuming too many resources.
OOMKill is common in two situations: the memory limit is set too low for the actual workload, or the application has a memory leak that causes usage to grow over time until it hits the ceiling.
Setup
## Set a very low memory limit — 32Mi is too small for a Node.js applicationkubectl set resources deployment/backend \ -c backend \ --limits=memory=32Mi \ -n swiggy-clone-staging ## Wait for pods to restart with new limitskubectl rollout status deployment/backend -n swiggy-clone-stagingInject
## Send requests that cause memory usage to spike## Install 'hey' load testing tool if not installedgo install github.com/rakyll/hey@latest ## Send 500 concurrent requests — this drives memory usage uphey -n 1000 -c 50 http://swiggy-clone-staging.your-cluster.example/api/menu## You do not need to wait for this to finish — watch the pods in another terminalDetect
## Terminal 1: Watch pods in real timekubectl get pods -n swiggy-clone-staging --watch## You will see the pod status change:## backend-xxx 1/1 Running 0 → 0/1 OOMKilled 1 ## Terminal 2: Describe the pod after it crasheskubectl describe pod -l app=backend -n swiggy-clone-staging | grep -A 10 "Last State"## Output:## Last State: Terminated## Reason: OOMKilled ← this is the key indicator## Exit Code: 137 ← exit code 137 = killed by signal 9 (SIGKILL) ## Check Prometheus for memory usage## Run this PromQL query in Grafana or the Prometheus UI:## container_memory_working_set_bytes{namespace="swiggy-clone-staging", container="backend"}## You will see the spike just before the OOMKill ## Check Grafana for the memory spike## Dashboard: Kubernetes / Compute Resources / Pod## Filter: namespace=swiggy-clone-staging, pod=backend-xxx## The memory graph will show a sharp climb to 32Mi then drop to zero (restart)RememberExit code 137 always means OOMKill. Exit code 1 means the application itself exited with an error. Exit code 0 means the application exited cleanly (probably a job that finished). These codes are your first diagnostic signal.
Fix
## Step 1: Find out how much memory the backend actually uses under normal load## Query Prometheus for P95 memory usage over the last 24 hours## PromQL: quantile_over_time(0.95,## container_memory_working_set_bytes{## namespace="swiggy-clone-staging",## container="backend"## }[24h])## If result is ~180Mi, set limit to 256Mi (1.5x headroom is a good rule) ## Step 2: Set a sensible memory limit based on actual usagekubectl set resources deployment/backend \ -c backend \ --requests=memory=128Mi \ --limits=memory=512Mi \ -n swiggy-clone-staging ## Step 3: Verify pods restart cleanlykubectl rollout status deployment/backend -n swiggy-clone-stagingkubectl get pods -n swiggy-clone-staging## RESTARTS should not be climbing anymorePrevent
## Install Vertical Pod Autoscaler (VPA) from the FinOps module## VPA watches actual CPU/memory usage and recommends correct limits kubectl apply -f https://github.com/kubernetes/autoscaler/releases/latest/download/vertical-pod-autoscaler.yaml ## Create a VPA object for the backend in recommendation-only mode## Recommendation mode: suggests values but does not change them automatically## This is the safest starting point — you review recommendations before applying## k8s/vpa-backend.yaml apiVersion: autoscaling.k8s.io/v1kind: VerticalPodAutoscalermetadata: name: backend-vpa namespace: swiggy-clone-stagingspec: targetRef: apiVersion: apps/v1 kind: Deployment name: backend updatePolicy: updateMode: "Off" ## Off = recommend only, do not change pods automaticallykubectl apply -f k8s/vpa-backend.yaml ## After 24 hours, check the VPA recommendationskubectl describe vpa backend-vpa -n swiggy-clone-staging | grep -A 10 Recommendation## Output:## Lower Bound: cpu: 50m, memory: 100Mi## Target: cpu: 120m, memory: 180Mi ← set your limits based on Target## Upper Bound: cpu: 300m, memory: 400Mi## Uncapped Target: cpu: 120m, memory: 180MiCommon MistakeSizing memory limits based on average usage. If average is 100Mi but P95 is 180Mi, setting a limit of 120Mi will OOMKill during traffic spikes. Always size limits from the P95 or P99 percentile, not the average.
Scenario 3 — Bad Deployment — Rollout Goes Wrong
Setup
## Verify a clean starting statekubectl get rollout backend -n swiggy-clone-staging## STATUS: Healthy, STABLE: backend:v1.5.0 or similar ## Confirm the application is respondingcurl -s http://swiggy-clone-staging.your-cluster.example/health/ready## Returns: {"status": "ok"}Inject
## Deploy an image tag that does not exist in the registry## This simulates a CI pipeline that accidentally pushed a wrong tag namekubectl set image rollout/backend \ backend=your-account.dkr.ecr.ap-south-1.amazonaws.com/swiggy-backend:nonexistent-broken \ -n swiggy-clone-staging ## Watch what happenskubectl get pods -n swiggy-clone-staging --watch## STATUS changes to: ImagePullBackOff## The rollout is stuck — new pods cannot start, old pods are still runningDetect
## Check the pod statuskubectl get pods -n swiggy-clone-staging## backend-new-xxx 0/1 ImagePullBackOff 0 ## Describe the failing podkubectl describe pod -l app=backend -n swiggy-clone-staging | grep -A 5 "Events:"## Events:## Warning Failed 30s kubelet Failed to pull image: not found ## Check ArgoCD — it shows the rollout healthargocd app get swiggy-clone-staging## Health Status: Degraded## This tells you ArgoCD detected the rollout did not complete successfully ## Check the Argo Rollout status directlykubectl argo rollouts get rollout backend -n swiggy-clone-staging## Status: ✖ Degraded## Strategy: Canary## Step: 0/7## The canary step shows the broken image tag — the old stable version## is still serving 100% of traffic because the canary never passed Step 1Fix
## Option 1: ArgoCD rollback via the UI## Open the ArgoCD UI → swiggy-clone-staging → HISTORY AND ROLLBACK## Click Rollback on the previous healthy revision → Confirm ## Option 2: kubectl rollout undo (fast, terminal-only)kubectl argo rollouts undo rollout/backend -n swiggy-clone-staging## This immediately restores the previous stable image tag ## Option 3: Fix forward — deploy the correct image tagkubectl set image rollout/backend \ backend=your-account.dkr.ecr.ap-south-1.amazonaws.com/swiggy-backend:v1.5.0 \ -n swiggy-clone-staging ## Verify recoverykubectl argo rollouts get rollout backend -n swiggy-clone-staging## Status: HealthyPrevent
Add image tag validation to the CI pipeline so a tag that does not exist in the registry cannot be deployed.
## .github/workflows/deploy.yml## Add this validation step before updating the Kubernetes manifest - name: Validate image exists in ECR before deploying run: | ## Check if the image tag exists in ECR ## This fails the pipeline if the image was not pushed successfully aws ecr describe-images \ --repository-name swiggy-backend \ --image-ids imageTag=${IMAGE_TAG} \ --region ap-south-1 echo "✅ Image ${IMAGE_TAG} confirmed in ECR — safe to deploy"Scenario 4 — Database Connection Exhaustion
What connection exhaustion is
Every PostgreSQL database has a maximum number of simultaneous connections (max_connections). The default is 100. If you have 20 pods each opening 10 connections, you have 200 connections — exceeding the limit. New connection attempts fail with FATAL: remaining connection slots are reserved. Every API request that needs the database returns a 500 error.
Without a connection pooler like PgBouncer, every pod holds open database connections for its entire lifetime. As you scale out, connection count grows linearly. With PgBouncer, pods connect to PgBouncer (which can handle thousands of connections) and PgBouncer maintains a smaller pool to the actual database.
Setup
## Check current connection count — baseline before injectionkubectl exec -n swiggy-clone-staging deployment/postgres -- \ psql -U postgres -c "SELECT count(*) FROM pg_stat_activity;"## Note this number — likely 5-10 with a small deploymentInject
## Remove connection pool limits from the backend and scale to 20 replicas## Each replica will open a new pool of connections to PostgreSQLkubectl set env deployment/backend \ DB_POOL_SIZE=20 \ -n swiggy-clone-staging kubectl scale deployment backend --replicas=20 -n swiggy-clone-staging ## Watch connection count climbkubectl exec -n swiggy-clone-staging deployment/postgres -- \ psql -U postgres -c "SELECT count(*) FROM pg_stat_activity;"## Runs the query every 5 seconds — watch the count climb toward 100 ## Send requests to trigger connection failureshey -n 500 -c 100 http://swiggy-clone-staging.your-cluster.example/api/menu## API will start returning 500 errors when connection limit is hitDetect
## Check PostgreSQL logs for connection errorskubectl logs deployment/postgres -n swiggy-clone-staging | \ grep -i "connection\|too many\|remaining"## Look for: FATAL: remaining connection slots are reserved for non-replication## Look for: FATAL: sorry, too many clients already ## Check the API error rate in Prometheus## PromQL: rate(http_requests_total{status=~"5..",app="backend"}[5m])## You will see a spike in 5xx errors coinciding with scale-out ## Check connection count by applicationkubectl exec -n swiggy-clone-staging deployment/postgres -- \ psql -U postgres -c " SELECT application_name, count(*) FROM pg_stat_activity GROUP BY application_name ORDER BY count DESC;"## This shows which application is consuming connectionsFix
## Step 1: Immediately scale back down to stop the bleedingkubectl scale deployment backend --replicas=3 -n swiggy-clone-staging ## Step 2: Restore sensible connection pool sizekubectl set env deployment/backend \ DB_POOL_SIZE=5 \ -n swiggy-clone-staging ## Step 3: Deploy PgBouncer as a connection pooler## k8s/pgbouncer.yaml## PgBouncer sits between the application and PostgreSQL## Pods connect to PgBouncer instead of PostgreSQL directly apiVersion: apps/v1kind: Deploymentmetadata: name: pgbouncer namespace: swiggy-clone-stagingspec: replicas: 1 selector: matchLabels: app: pgbouncer template: metadata: labels: app: pgbouncer spec: containers: - name: pgbouncer image: pgbouncer/pgbouncer:1.21.0 ports: - containerPort: 5432 env: - name: DATABASES_HOST value: postgres-service ## the real PostgreSQL service name - name: DATABASES_PORT value: "5432" - name: DATABASES_DBNAME value: swiggy - name: PGBOUNCER_POOL_MODE value: transaction ## transaction pooling: most efficient - name: PGBOUNCER_MAX_CLIENT_CONN value: "1000" ## PgBouncer accepts up to 1000 connections - name: PGBOUNCER_DEFAULT_POOL_SIZE value: "20" ## PgBouncer keeps only 20 connections to PostgreSQL resources: requests: cpu: "50m" memory: "32Mi" limits: cpu: "200m" memory: "64Mi"apiVersion: v1kind: Servicemetadata: name: pgbouncer-service namespace: swiggy-clone-stagingspec: selector: app: pgbouncer ports: - port: 5432 targetPort: 5432kubectl apply -f k8s/pgbouncer.yaml ## Update the backend to connect to PgBouncer instead of PostgreSQL directlykubectl set env deployment/backend \ DB_HOST=pgbouncer-service \ -n swiggy-clone-staging ## Verify connection count droppedkubectl exec -n swiggy-clone-staging deployment/postgres -- \ psql -U postgres -c "SELECT count(*) FROM pg_stat_activity;"## Should now show ~20 connections (PgBouncer pool) regardless of backend replica countPrevent
## k8s/resource-quota.yaml## ResourceQuota limits the maximum number of replicas in a namespace## This prevents accidental scale-out that exhausts database connections apiVersion: v1kind: ResourceQuotametadata: name: namespace-limits namespace: swiggy-clone-stagingspec: hard: ## Maximum 10 pods in this namespace — prevents runaway scaling pods: "10" ## Maximum CPU and memory across all pods in the namespace requests.cpu: "4" requests.memory: 4Gi limits.cpu: "8" limits.memory: 8GiScenario 5 — Cost Spike Investigation
Setup
## Verify Kubecost is installed (from the FinOps module)kubectl get pods -n kubecost## Should show kubecost-cost-analyzer Running ## Open the Kubecost dashboard and note the current daily cost## http://localhost:9090 (after port-forward)kubectl port-forward -n kubecost deployment/kubecost-cost-analyzer 9090:9090 &## Note the current cost for swiggy-clone-staging namespace as a baselineInject
## Create a Deployment with no resource requests and scale it to 50 replicas## Without resource requests, Kubernetes cannot schedule efficiently## Pods claim no resources on paper but consume actual CPU and memory## Kubecost cannot correctly attribute cost — and the waste is enormous kubectl create deployment cost-hog \ --image=nginx:latest \ -n swiggy-clone-staging ## Scale to 50 replicas immediatelykubectl scale deployment cost-hog --replicas=50 -n swiggy-clone-staging ## Watch nodes become pressuredkubectl describe nodes | grep -A 5 "Allocated resources"## CPU and memory allocation will spike significantlyDetect
## Open Kubecost dashboard at http://localhost:9090## Navigate to: Allocations → Namespace → swiggy-clone-staging## You will see a sudden spike in cost for the cost-hog deployment## The efficiency score will drop because there are no resource requests set ## Check from the terminalkubectl get deployment cost-hog -n swiggy-clone-staging## READY column will show 50/50 — 50 pods consuming untracked resources ## Check cluster capacity impactkubectl top nodes## CPU% and Memory% on nodes will have jumped significantly ## Prometheus alert would have fired (if configured from FinOps module):## KubecostBudgetExceeded: namespace swiggy-clone-staging exceeded daily budgetFix
## Delete the runaway Deployment immediatelykubectl delete deployment cost-hog -n swiggy-clone-staging ## Verify cost returns to baselinekubectl top nodes## CPU% and Memory% should return to previous levels within 2 minutes ## Check Kubecost shows the cost dropping back to baseline## This may take up to 15 minutes for Kubecost to reflect the changePrevent
## k8s/limit-range.yaml## LimitRange sets default resource requests on pods that do not specify them## It also sets a minimum — pods cannot be created without at least these resources apiVersion: v1kind: LimitRangemetadata: name: default-limits namespace: swiggy-clone-stagingspec: limits: - type: Container ## If a pod does not specify requests, these defaults are applied automatically defaultRequest: cpu: "50m" memory: "64Mi" ## If a pod does not specify limits, these defaults are applied default: cpu: "500m" memory: "512Mi" ## Minimum allowed values — a pod cannot request less than these min: cpu: "10m" memory: "32Mi" ## Maximum allowed values — a pod cannot request more than these max: cpu: "2" memory: "4Gi"## k8s/kyverno-require-resources.yaml## Kyverno policy: blocks any Deployment that does not have resource requests## This is enforced at admission time — the Deployment cannot be created apiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: require-resource-requestsspec: validationFailureAction: enforce ## block, do not just warn rules: - name: require-requests match: any: - resources: kinds: - Deployment validate: message: "Resource requests (CPU and memory) are required on all containers. Set spec.containers[].resources.requests." pattern: spec: template: spec: containers: - resources: requests: memory: "?*" ## ?* means: must be present and non-empty cpu: "?*"Scenario 6 — Network Policy Lockdown
Setup
## Verify pods can communicate normallykubectl exec -n swiggy-clone-staging deployment/backend -- \ curl -s http://postgres-service:5432## Should respond (even if it is a connection refused — that means networking works) kubectl exec -n swiggy-clone-staging deployment/backend -- \ nslookup google.com## DNS should resolve successfullyInject
## Apply an overly restrictive NetworkPolicy that blocks all traffic## including DNS resolution — a very common mistake kubectl apply -f - << 'EOF'apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: deny-all-broken namespace: swiggy-clone-stagingspec: podSelector: {} ## applies to ALL pods in this namespace policyTypes: - Ingress - Egress ## No ingress or egress rules = deny everything ## This blocks DNS (port 53), database connections, everythingEOF ## Watch pods start failing readiness probeskubectl get pods -n swiggy-clone-staging --watch## READY changes from 1/1 to 0/1 within 30 seconds## Readiness probes fail because backend cannot reach PostgreSQLDetect
## Test DNS resolution from inside a podkubectl exec -n swiggy-clone-staging deployment/backend -- \ nslookup postgres-service## Error: connection timed out; no servers could be reached## This confirms DNS is blocked — the NetworkPolicy is too strict ## Test service connectivitykubectl exec -n swiggy-clone-staging deployment/backend -- \ curl -v http://postgres-service:5432 --connect-timeout 5## Error: Connection timed out## Confirming egress is blocked ## Check NetworkPolicy existskubectl get networkpolicy -n swiggy-clone-staging## Shows: deny-all-broken ## Check which pods the policy applies tokubectl describe networkpolicy deny-all-broken -n swiggy-clone-staging## Pod Selector: <none> (all pods)## Policy Types: Ingress, Egress## No rules listed = total lockdown ## Check Ingress returns 503curl -v http://swiggy-clone-staging.your-cluster.example/api/menu## HTTP/1.1 503 Service Unavailable## Readiness probes are failing so pods are removed from the Service's EndpointsFix
## Step 1: Delete the broken policy immediatelykubectl delete networkpolicy deny-all-broken -n swiggy-clone-staging ## Step 2: Verify recoverykubectl get pods -n swiggy-clone-staging## READY should return to 1/1 within 10 seconds as readiness probes pass ## Step 3: Apply a CORRECT NetworkPolicy that allows DNS and same-namespace traffic## k8s/network-policy-correct.yaml## A NetworkPolicy that follows the principle of least privilege## but does NOT break DNS or legitimate service-to-service communication ## Policy 1: Allow DNS egress from all pods## Without this, pods cannot resolve service names — everything breaksapiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: allow-dns-egress namespace: swiggy-clone-stagingspec: podSelector: {} ## apply to all pods policyTypes: - Egress egress: - ports: - port: 53 protocol: UDP ## DNS uses UDP on port 53 - port: 53 protocol: TCP ## some DNS queries use TCP (large responses)## Policy 2: Allow traffic within the same namespaceapiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: allow-same-namespace namespace: swiggy-clone-stagingspec: podSelector: {} policyTypes: - Ingress - Egress ingress: - from: - podSelector: {} ## allow from any pod in the same namespace egress: - to: - podSelector: {} ## allow to any pod in the same namespace## Policy 3: Allow ingress from the ingress controllerapiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: allow-ingress-controller namespace: swiggy-clone-stagingspec: podSelector: matchLabels: app: backend policyTypes: - Ingress ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: ingress-nginx ## your ingress namespacekubectl apply -f k8s/network-policy-correct.yaml ## Verify DNS works againkubectl exec -n swiggy-clone-staging deployment/backend -- \ nslookup postgres-service## Should resolve to the ClusterIP ## Verify service connectivitycurl -s http://swiggy-clone-staging.your-cluster.example/api/menu## Should return 200Prevent
## k8s/kyverno-generate-network-policy.yaml## Kyverno auto-generates the correct default NetworkPolicies## for every new namespace — so they start with sensible defaults apiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: generate-default-network-policiesspec: rules: - name: add-allow-dns match: any: - resources: kinds: - Namespace ## generate: creates the NetworkPolicy when a new namespace is created generate: apiVersion: networking.k8s.io/v1 kind: NetworkPolicy name: allow-dns-egress namespace: "{{request.object.metadata.name}}" synchronize: true ## keeps the policy in sync if manually deleted data: spec: podSelector: {} policyTypes: - Egress egress: - ports: - port: 53 protocol: UDP - port: 53 protocol: TCPSLO Definition and Burn Rate Alerts
What an SLO is
An SLO (Service Level Objective) is a target reliability level for a service. It answers the question: how reliable does this service need to be for users to be satisfied?
An SLO is not a promise to users (that is an SLA). It is an internal target used to make engineering decisions. If the service is above the SLO, the team can invest in features. If it is below, the team stops feature work and fixes reliability.
For the swiggy-clone application, here are three meaningful SLOs:
| SLO | Target | Meaning |
|---|---|---|
| Availability | 99.9% | No more than 43.8 minutes of downtime per month |
| P95 Latency | < 500ms | 95% of requests served in under half a second |
| Error Rate | < 1% | No more than 1 in 100 requests returns a 5xx response |
Error budget
If the availability SLO is 99.9%, the error budget is 0.1% of total uptime. In a 30-day month with 43,200 minutes, the error budget is 43.2 minutes. Once that budget is exhausted, any further downtime is violating the SLO.
The error budget is the most useful concept from SRE practice. It converts an abstract percentage into concrete time that engineers can reason about. "We have 43 minutes of error budget left this month" is actionable. "We are at 99.91% availability" is not.
Prometheus recording rules and alerting rules
## monitoring/slo-rules.yaml## PrometheusRule defines recording rules and alerting rules for SLOs apiVersion: monitoring.coreos.com/v1kind: PrometheusRulemetadata: name: swiggy-clone-slos namespace: monitoring labels: release: monitoring ## must match your Prometheus operator label selectorspec: groups: ## ─── Availability SLO ────────────────────────────────────────── - name: swiggy-clone-availability interval: 30s rules: ## Recording rule: calculate availability over a 5-minute window ## This pre-computes the value so alerts evaluate faster - record: job:http_availability:rate5m expr: | sum( rate( http_requests_total{ namespace="swiggy-clone-production", app="backend", status!~"5.." ## exclude 5xx errors from the success count }[5m] ) ) / sum( rate( http_requests_total{ namespace="swiggy-clone-production", app="backend" }[5m] ) ) ## Alert: availability dropped below 99.9% (immediate — high burn rate) - alert: SLOAvailabilityBurnRateFast expr: | ( 1 - job:http_availability:rate5m ) > (14.4 * 0.001) ## 14.4x burn rate: consuming 1-hour budget in 5 minutes ## 0.001 = the 0.1% error budget for: 2m labels: severity: critical team: backend-team annotations: summary: "Availability SLO burn rate critical — action required immediately" description: "Swiggy-clone backend is burning error budget at 14.4x the sustainable rate. At this rate the monthly budget will be exhausted in under 1 hour." runbook: "https://backstage.internal/docs/default/component/swiggy-clone-backend/runbooks/availability" ## Alert: slower burn rate — will exhaust budget in ~3 days - alert: SLOAvailabilityBurnRateSlow expr: | ( 1 - job:http_availability:rate5m ) > (6 * 0.001) ## 6x burn rate: consuming 6-hour budget in 1 hour for: 15m labels: severity: warning team: backend-team annotations: summary: "Availability SLO burn rate elevated" description: "Budget will be exhausted in approximately 3 days at the current rate. Investigate root cause before it becomes critical." ## ─── Latency SLO ─────────────────────────────────────────────── - name: swiggy-clone-latency rules: - record: job:http_request_duration_p95:rate5m expr: | histogram_quantile( 0.95, sum( rate( http_request_duration_seconds_bucket{ namespace="swiggy-clone-production", app="backend" }[5m] ) ) by (le) ) - alert: SLOLatencyP95Exceeded expr: job:http_request_duration_p95:rate5m > 0.5 ## > 500ms for: 5m labels: severity: warning team: backend-team annotations: summary: "P95 latency SLO breached" description: "P95 latency is {{ $value | humanizeDuration }} — SLO target is 500ms."RememberBurn rate alerts are more useful than threshold alerts. "The error rate is 2%" tells you the current state. "At this rate you will exhaust your monthly error budget in 4 hours" tells you how urgent the problem is and drives the right response.
Production Checklist
## ─── 1. Run all 6 scenarios and verify recovery ──────────────────## For each scenario, verify these three things after recovery:kubectl get pods -n swiggy-clone-staging## All pods: STATUS=Running, RESTARTS should not be climbing curl -s http://swiggy-clone-staging.your-cluster.example/health/ready## Returns: {"status": "ok"} kubectl argo rollouts get rollout backend -n swiggy-clone-staging 2>/dev/null || \kubectl get deployment backend -n swiggy-clone-staging## Healthy or AVAILABLE=1 ## ─── 2. Verify startup validation is active ──────────────────────## Intentionally break DB_HOST and check the log message is clearkubectl set env deployment/backend DB_HOST="" -n swiggy-clone-stagingsleep 15kubectl logs deployment/backend -n swiggy-clone-staging --previous 2>/dev/null | \ grep -i "FATAL\|required\|missing"## Should see the specific missing variable message from startup-validation.js## Restore immediately:kubectl set env deployment/backend DB_HOST=postgres-service -n swiggy-clone-staging ## ─── 3. Verify PgBouncer is deployed ─────────────────────────────kubectl get deployment pgbouncer -n swiggy-clone-staging## AVAILABLE should be 1 ## ─── 4. Verify SLO alerting rules are loaded ─────────────────────kubectl get prometheusrule swiggy-clone-slos -n monitoring## Should exist ## Query the recording rule to verify it is computingcurl -s "http://localhost:9090/api/v1/query?query=job:http_availability:rate5m" | \ jq '.data.result[0].value[1]'## Should return a number between 0.99 and 1.0 ## ─── 5. Verify NetworkPolicies are correct ───────────────────────kubectl get networkpolicy -n swiggy-clone-staging## Should show: allow-dns-egress, allow-same-namespace, allow-ingress-controller## Should NOT show: deny-all-broken ## Verify DNS still works from inside the namespacekubectl run dns-test --image=busybox --restart=Never -n swiggy-clone-staging \ --rm -it -- nslookup postgres-service## Should resolve without errors## Pod is automatically deleted after the test (--rm flag) ## ─── 6. Verify LimitRange is enforced ───────────────────────────kubectl get limitrange default-limits -n swiggy-clone-staging## Should exist ## Test: try to create a pod without resource requestskubectl run no-resources --image=nginx -n swiggy-clone-staging --restart=Neverkubectl get pod no-resources -n swiggy-clone-staging -o json | \ jq '.spec.containers[0].resources'## Should show default requests/limits applied by LimitRangekubectl delete pod no-resources -n swiggy-clone-staging echo "✅ Production readiness checklist complete"Common Production Mistakes
❌ Running chaos experiments in production before staging 💥 An engineer decides to test how the production cluster handles a node failure. They drain a node during peak lunch hours. Three services were only running one replica each. They all go down simultaneously. The ordering system is unavailable for 12 minutes. Millions of rupees in orders are lost. ✅ Every chaos experiment must run in staging first. Only when you have validated recovery in staging, documented the playbook, scheduled a maintenance window, and have the full on-call team available should you consider running it in production. Netflix did not start Chaos Monkey in production on day one — they built it in a non-production environment, validated it for months, then gradually expanded.
❌ No runbook for each alert
💥 An SLOAvailabilityBurnRateFast alert fires at 3:47 AM. The on-call engineer opens PagerDuty, sees the alert, opens Grafana, and stares at it. The alert message says the error rate is high. The engineer does not know which service, which endpoint, or what the previous incidents looked like. They spend 20 minutes just figuring out where to start. The error budget burns while they investigate.
✅ Every alert must have a runbook annotation with a URL to a step-by-step guide. The guide must answer: what does this alert mean, what are the first three commands to run, what are the likely causes, and how have we fixed it before? The TechDocs system from Capstone 4 is the right place to store runbooks — searchable, versioned, discoverable from Backstage.
❌ Setting SLOs without measuring the baseline first 💥 A team defines an availability SLO of 99.9% for a new service on launch day. Three months later, they look at the data and discover the service has never been above 99.5%. The SLO was aspirational, not grounded in reality. Now every month they are in SLO breach, error budgets are permanently exhausted, and the engineering team is demoralized because they cannot ship features. ✅ Measure before you commit. Run the service for two to four weeks, collect availability and latency data, and set the SLO at the P10 or P25 of historical performance — better than the worst days, achievable without heroic effort. A 99.5% SLO you reliably hit is more valuable than a 99.9% SLO you constantly breach.
❌ Fixing the symptom instead of the cause
💥 The backend is OOMKilled repeatedly. The on-call engineer increases the memory limit from 256Mi to 512Mi. The OOMKills stop. The engineer closes the incident. Three weeks later, memory limit reaches 512Mi and OOMKills start again. This happens three more times until the limit is 4Gi — 16x the original value. Nobody investigated why memory keeps growing. The service has a memory leak that has been running unchecked for months.
✅ Increasing the memory limit is a valid immediate mitigation — it stops the bleeding. But it must be followed by a root cause investigation. Use a memory profiler (Node.js clinic heap or --inspect with Chrome DevTools) to find what is allocating memory. A growing heap that never shrinks is almost always a leak: event listeners not removed, caches without eviction policies, circular references in long-lived objects.
Debugging Playbook
The production debugging flowchart — the exact commands for every common failure.
Pod not running
## Step 1: What state is the pod in?kubectl get pods -n YOUR_NAMESPACE## CrashLoopBackOff → go to Section A## ImagePullBackOff → go to Section B## Pending → go to Section C## OOMKilled → go to Section D## Completed → go to Section E ## ── Section A: CrashLoopBackOff ────────────────────────────────kubectl logs POD_NAME -n YOUR_NAMESPACE --previous## Read the last 20 lines — look for: FATAL, Error, Cannot connect, Missing kubectl describe pod POD_NAME -n YOUR_NAMESPACE | grep -A 10 "Last State"## Check Exit Code: 1 = app error, 137 = OOMKill, 139 = segfault ## If logs are empty (crash is too fast):kubectl get events -n YOUR_NAMESPACE --sort-by='.lastTimestamp' | tail -20## Events show what Kubernetes observed ## ── Section B: ImagePullBackOff ─────────────────────────────────kubectl describe pod POD_NAME -n YOUR_NAMESPACE | grep -A 5 Events## Look for: ErrImagePull, ImagePullBackOff, unauthorized, not found ## Check if the image exists in the registry:aws ecr describe-images --repository-name YOUR_REPO \ --image-ids imageTag=YOUR_TAG --region ap-south-1## If this fails: the image tag does not exist — roll back the deployment ## Check if the ECR credentials are valid in the cluster:kubectl get secret regcred -n YOUR_NAMESPACE## If missing: the image pull secret is not configured ## ── Section C: Pending ──────────────────────────────────────────kubectl describe pod POD_NAME -n YOUR_NAMESPACE | grep -A 10 Events## Look for: Insufficient cpu, Insufficient memory, Unschedulable ## Check node capacity:kubectl describe nodes | grep -A 5 "Allocated resources"## If nodes are at 95%+ CPU or memory: cluster needs scaling ## Check for taints or node selectors:kubectl describe pod POD_NAME -n YOUR_NAMESPACE | grep -A 5 "Node-Selectors"## If nodeSelector is set, verify nodes with that label exist:kubectl get nodes -l YOUR_LABEL=YOUR_VALUE ## ── Section D: OOMKilled ────────────────────────────────────────kubectl describe pod POD_NAME -n YOUR_NAMESPACE | grep -A 5 "Last State"## Reason: OOMKilled, Exit Code: 137 ## Find actual memory usage before the kill:kubectl top pod POD_NAME -n YOUR_NAMESPACE## Compare to the memory limit in the Deployment ## Check VPA recommendation if installed:kubectl describe vpa POD_NAME-vpa -n YOUR_NAMESPACE | grep -A 10 Recommendation ## ── Section E: Completed ────────────────────────────────────────## Status Completed is normal for Jobs and CronJobs## For Deployments: Completed means the container exited with code 0## This usually means the CMD in the Dockerfile exited — not a crash kubectl logs POD_NAME -n YOUR_NAMESPACE## Should show the final output before exit## Ensure your Dockerfile CMD keeps the process running (not a one-shot script)Service returning errors
## Step 1: Determine the error typecurl -v http://YOUR_SERVICE_URL/health/ready## 502 Bad Gateway → Section F## 503 Service Unavailable → Section G## 500 Internal Server Error → Section H## Timeout (no response) → Section I ## ── Section F: 502 Bad Gateway ──────────────────────────────────## 502 means the Ingress reached the pod but the pod returned a bad response## or the connection to the pod was refused kubectl get endpoints YOUR_SERVICE_NAME -n YOUR_NAMESPACE## Verify endpoints are populated — if empty, pods are not ready kubectl get pods -n YOUR_NAMESPACE -l app=YOUR_APP## Check READY column — 0/1 means readiness probe is failing kubectl logs -l app=YOUR_APP -n YOUR_NAMESPACE | tail -30## Look for application-level errors ## ── Section G: 503 Service Unavailable ──────────────────────────## 503 means no ready pods exist to handle the request kubectl get endpoints YOUR_SERVICE_NAME -n YOUR_NAMESPACE## Output: (none) or empty list — no pods are ready kubectl get pods -n YOUR_NAMESPACE## Likely CrashLoopBackOff or all pods restarting simultaneously ## Check if NetworkPolicy is blocking Ingress:kubectl get networkpolicy -n YOUR_NAMESPACE## If deny-all exists with no Ingress rules: add allow-ingress-controller policy ## ── Section H: 500 Internal Server Error ────────────────────────## 500 means the application is running but returning errors kubectl logs -l app=YOUR_APP -n YOUR_NAMESPACE | grep -i "error\|exception" | tail -20## Find the specific error — database connection? missing config? code bug? ## Check database connectivity from the pod:kubectl exec deployment/YOUR_APP -n YOUR_NAMESPACE -- \ nc -zv postgres-service 5432## If fails: database is unreachable — check NetworkPolicy, check postgres pod ## Check error rate in Prometheus:## PromQL: rate(http_requests_total{status=~"5..",app="YOUR_APP"}[5m]) ## ── Section I: Timeout ──────────────────────────────────────────## No response at all — likely network-level block or pod not reachable kubectl get pods -n YOUR_NAMESPACE -l app=YOUR_APP## Are pods running? ## Check NetworkPolicy:kubectl exec deployment/YOUR_APP -n YOUR_NAMESPACE -- \ nslookup google.com## If DNS fails: NetworkPolicy is blocking egress including port 53 ## Check Ingress:kubectl describe ingress -n YOUR_NAMESPACE## Verify the backend service name and port match what existsPerformance degraded
## Step 1: Identify where the slowness is## PromQL: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{app="backend"}[5m]))## If P95 latency is high, find the bottleneck: ## Check CPU throttling:## PromQL: rate(container_cpu_cfs_throttled_seconds_total{container="backend"}[5m])## If non-zero: CPU limit is too low, requests are being throttled ## Check memory pressure:kubectl top pods -n YOUR_NAMESPACE## Compare actual memory to the limit in the Deployment ## Check database query time:kubectl exec deployment/postgres -n YOUR_NAMESPACE -- \ psql -U postgres -c " SELECT query, mean_exec_time, calls FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10;"## Shows the slowest queries — missing index? N+1 problem? Lock contention? ## Check cache hit rate:kubectl exec deployment/redis -n YOUR_NAMESPACE -- \ redis-cli INFO stats | grep -E "keyspace_hits|keyspace_misses"## If miss rate is high: cache is cold or keys are not being set correctlyWhat You Have Built
Five capstones. Each one building on the last. Each one mapping directly to how production Platform Engineering teams work.
✅ Capstone 1: Built a cloud-native application Node.js backend, React frontend, PostgreSQL, Redis Kubernetes manifests, health probes, HPA, PDB ✅ Capstone 2: Provisioned production AWS infrastructure EKS cluster with Terraform, VPC, RDS, ElastiCache Prometheus and Grafana, Kubecost, IAM with least privilege ✅ Capstone 3: Built a GitOps delivery platform ArgoCD App of Apps, Kustomize overlays Canary deployments with Argo Rollouts Staging → production promotion via pull request ✅ Capstone 4: Built a developer portal and golden paths Backstage software catalog with auto-discovery Golden Path Template: new service in 10 minutes TechDocs: docs as code, always current ✅ Capstone 5: Operated it under real failure conditions CrashLoopBackOff: diagnosed and prevented with startup validation OOMKill: root-caused and prevented with VPA Bad deployment: rolled back via ArgoCD in under 2 minutes Connection exhaustion: fixed with PgBouncer Cost spike: detected with Kubecost, prevented with Kyverno Network lockdown: debugged NetworkPolicy step by step SLOs defined, error budgets calculated, burn rate alerts configuredSomeone who has completed all five of these capstones has not just studied Platform Engineering. They have done it.
Every scenario in Capstone 5 is real. Not invented for teaching purposes. OOMKills happen at Hotstar during IPL. Connection exhaustion happens at Razorpay during Diwali. Bad deployments happen at every company that ships software. Network policies that are too strict have broken more production environments than deliberate attacks.
Every concept, every tool, every decision maps directly to how production Platform Engineering teams work at Indian product companies.
You are ready.
Videos & Guides
No external walkthroughs, video tutorials, or reference files attached to this blueprint yet.