Deployment
A Kubernetes controller that manages a set of identical pods — ensuring the desired number of replicas always runs, handling rolling updates to new versions with zero downtime, and automatically replacing crashed or deleted pods.
Deployment — The Standard Way to Run Applications in Kubernetes
What is a Deployment in Simple Terms?
If a Pod is a single running container, a Deployment is the manager that keeps a group of identical pods healthy and up to date. You tell the Deployment: run 3 copies of my payment API. The Deployment then creates 3 pods and permanently watches over them. If one crashes, it starts a replacement immediately. If you push a new version, it replaces pods one by one without downtime.
At Zerodha, every trading microservice runs as a Deployment. When engineers push a new build, the Deployment handles the entire rollout — no manual pod management, no downtime, no human error.
+------------------------------------------+| Deployment: payment-api || desired replicas: 3 |+------------------------------------------+ | | | v v v+----------+ +----------+ +----------+| Pod 1 | | Pod 2 | | Pod 3 || v3.1.0 | | v3.1.0 | | v3.1.0 || Running | | Running | | Running |+----------+ +----------+ +----------+ Pod 2 crashes -- Deployment auto-creates Pod 4: +----------+ +----------+ +----------+ +----------+| Pod 1 | | Pod 2 | | Pod 3 | | Pod 4 || v3.1.0 | | Terminat | | v3.1.0 | | v3.1.0 || Running | | -ing | | Running | | Starting |+----------+ +----------+ +----------+ +----------+ Deployment controller creates replacement immediatelyHow a Deployment Works Internally
A Deployment does not manage pods directly. It creates a ReplicaSet, which then manages the actual pods. This indirection is what makes rolling updates work cleanly.
+------------------------------------------+| Deployment || (you manage this) |+------------------------------------------+ | | creates and manages v+------------------------------------------+| ReplicaSet v3.1.0 || Created automatically by Deployment || Ensures 3 pods of v3.1.0 are running |+------------------------------------------+ | | | v v v +--------+ +--------+ +--------+ | Pod 1 | | Pod 2 | | Pod 3 | +--------+ +--------+ +--------+ After rolling update to v3.2.0: +------------------------------------------+| Deployment |+------------------------------------------+ | | v v+------------------+ +------------------+| ReplicaSet v3.1.0| | ReplicaSet v3.2.0|| (scaled to 0) | | (scaled to 3) || kept for rollback| | currently active |+------------------+ +------------------+ | +---------+---------+ v v v +-------+ +-------+ +-------+ | Pod 1 | | Pod 2 | | Pod 3 | | v3.2.0| | v3.2.0| | v3.2.0| +-------+ +-------+ +-------+A Complete Deployment Specification
apiVersion: apps/v1kind: Deploymentmetadata: name: payment-api namespace: production labels: app: payment-api team: paymentsspec: # -- How many pod replicas to maintain ------------ replicas: 3 # -- Which pods this Deployment owns -------------- # Must match labels in template.metadata.labels exactly selector: matchLabels: app: payment-api # -- Rolling Update Configuration ----------------- strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 # Allow 1 extra pod during update (4 pods at peak) maxUnavailable: 0 # Never go below 3 pods — zero downtime guaranteed # -- Pod Template --------------------------------- # Everything below here is the spec for each individual pod template: metadata: labels: app: payment-api # MUST match selector.matchLabels above version: v3.1.0 spec: containers: - name: payment-api image: registry.razorpay.in/payment-api:v3.1.0 ports: - containerPort: 8080 name: http # Environment variables env: - name: NODE_ENV value: "production" - name: DB_PASSWORD valueFrom: secretKeyRef: name: db-credentials key: password # Resources — REQUIRED for HPA and scheduler to work correctly resources: requests: cpu: "250m" memory: "256Mi" limits: cpu: "1000m" memory: "512Mi" # Readiness probe — controls when pod receives traffic # Pod is NOT added to Service endpoints until this passes readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 10 # Wait 10s before first check periodSeconds: 5 # Check every 5s failureThreshold: 3 # Remove from Service after 3 failures # Liveness probe — controls when Kubernetes restarts the container livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 30 # Give the app 30s to start periodSeconds: 15 failureThreshold: 3 # Restart after 3 consecutive failures # Graceful shutdown terminationGracePeriodSeconds: 30 # Give app 30s to finish in-flight requests # RBAC identity for this pod serviceAccountName: payment-processor automountServiceAccountToken: false # Disable if app doesnt need K8s APIThe Rolling Update — How Zero Downtime Works
This is the most important thing to understand about Deployments. When you update the image tag, this sequence happens:
Before update: 3 pods running v3.1.0 +----------+ +----------+ +----------+| Pod 1 | | Pod 2 | | Pod 3 || v3.1.0 | | v3.1.0 | | v3.1.0 || Ready | | Ready | | Ready |+----------+ +----------+ +----------+ Step 1: maxSurge=1 creates a new pod (4 pods total) +----------+ +----------+ +----------+ +----------+| Pod 1 | | Pod 2 | | Pod 3 | | Pod 4 || v3.1.0 | | v3.1.0 | | v3.1.0 | | v3.2.0 || Ready | | Ready | | Ready | | Starting |+----------+ +----------+ +----------+ +----------+ (new pod) Step 2: Pod 4 passes readinessProbe -- Pod 1 terminated +----------+ +----------+ +----------+| Pod 2 | | Pod 3 | | Pod 4 || v3.1.0 | | v3.1.0 | | v3.2.0 || Ready | | Ready | | Ready |+----------+ +----------+ +----------+ Step 3: Repeat until all pods are v3.2.0 +----------+ +----------+ +----------+| Pod 4 | | Pod 5 | | Pod 6 || v3.2.0 | | v3.2.0 | | v3.2.0 || Ready | | Ready | | Ready |+----------+ +----------+ +----------+At every step, maxUnavailable: 0 ensures 3 pods are always Ready and serving traffic. Users never see downtime.
Essential Deployment Commands
# Apply a Deployment from a YAML filekubectl apply -f deployment.yaml -n production # List all Deployments in a namespacekubectl get deployments -n production# NAME READY UP-TO-DATE AVAILABLE AGE# payment-api 3/3 3 3 12d # READY 3/3 = 3 pods running and passing readiness checks# UP-TO-DATE 3 = all 3 pods are on the latest template version# AVAILABLE 3 = 3 pods are available to serve traffic # Watch a rolling update in progresskubectl rollout status deployment/payment-api -n production# Waiting for deployment "payment-api" rollout to finish: 1 of 3 updated...# Waiting for deployment "payment-api" rollout to finish: 2 of 3 updated...# deployment "payment-api" successfully rolled out # Trigger a rolling update by changing the image tagkubectl set image deployment/payment-api \ payment-api=registry.razorpay.in/payment-api:v3.2.0 \ -n production # Scale replicas up or downkubectl scale deployment payment-api --replicas=6 -n production # Instantly rollback to the previous versionkubectl rollout undo deployment/payment-api -n production # Rollback to a specific revision numberkubectl rollout undo deployment/payment-api \ --to-revision=2 \ -n production # View rollout historykubectl rollout history deployment/payment-api -n production# REVISION CHANGE-CAUSE# 1 Initial deployment# 2 Updated to v3.1.0# 3 Updated to v3.2.0 # Pause a rolling update mid-way (to canary test one pod)kubectl rollout pause deployment/payment-api -n production # Resume a paused rolloutkubectl rollout resume deployment/payment-api -n production # Force a restart of all pods (without changing the image)# Useful after updating a ConfigMap or Secretkubectl rollout restart deployment/payment-api -n productionDeployment vs StatefulSet vs DaemonSet — When to Use Which
+------------------------------------------+| Deployment || || Use for: stateless applications || Pod identity: random (api-7d9f8c-xkp2q) || Storage: shared or none || Examples: APIs, web servers, workers |+------------------------------------------+ +------------------------------------------+| StatefulSet || || Use for: stateful applications || Pod identity: stable (postgres-0) || Storage: dedicated PVC per pod || Examples: PostgreSQL, Kafka, Redis |+------------------------------------------+ +------------------------------------------+| DaemonSet || || Use for: one agent per node || Pod identity: one per node || Storage: host filesystem access || Examples: Fluentd, Node Exporter, Calico |+------------------------------------------+Checking Deployment Health — Reading the Status
# Get detailed Deployment statuskubectl describe deployment payment-api -n production # Key sections to look at in the output:# Replicas: 3 desired | 3 updated | 3 total | 3 available | 0 unavailable# Conditions:# Available: True (MinimumReplicasAvailable)# Progressing: True (NewReplicaSetAvailable) # Check the ReplicaSets owned by this Deploymentkubectl get replicasets -n production -l app=payment-api# NAME DESIRED CURRENT READY# payment-api-7d9f8c 3 3 3 <- current version# payment-api-6b8d4a 0 0 0 <- previous version (kept for rollback) # See all pods belonging to a Deploymentkubectl get pods -n production -l app=payment-api # Check live resource usage of all pods in a Deploymentkubectl top pods -n production -l app=payment-apiAnnotating Deployments for Rollout History
By default, rollout history shows blank CHANGE-CAUSE entries. Add the --record flag or use annotations to track what changed:
# Record the change cause when updatingkubectl set image deployment/payment-api \ payment-api=registry.razorpay.in/payment-api:v3.2.0 \ -n production \ --record # Or annotate manually before applyingkubectl annotate deployment payment-api \ kubernetes.io/change-cause="Release v3.2.0 — added UPI retry logic" \ -n production # Now history shows meaningful messageskubectl rollout history deployment/payment-api -n production# REVISION CHANGE-CAUSE# 2 Release v3.1.0 — payment gateway integration# 3 Release v3.2.0 — added UPI retry logicDeployment Troubleshooting Guide
| Problem | Symptom | How to Diagnose |
|---|---|---|
| Pods not starting | READY shows 0/3 | kubectl describe deployment — check Events section |
| Rolling update stuck | UP-TO-DATE less than desired | kubectl describe pod <new-pod> — readinessProbe failing |
| Deployment not updating | Image change not applied | Check if kubectl apply was run — verify with kubectl get deployment -o yaml |
| Pods crashing after update | CrashLoopBackOff after rollout | kubectl logs <pod> --previous then kubectl rollout undo |
| Traffic going to old pods | Service routing incorrectly | Check Service selector matches Deployment pod labels exactly |
# Full diagnostic sequence for a Deployment problem # Step 1 — Check the Deployment overviewkubectl get deployment payment-api -n production # Step 2 — Read the detailed status and eventskubectl describe deployment payment-api -n production # Step 3 — Look at the pods individuallykubectl get pods -n production -l app=payment-api # Step 4 — Read logs from a failing podkubectl logs <pod-name> -n production --previous # Step 5 — If the new version is bad, rollback immediatelykubectl rollout undo deployment/payment-api -n productionRemember
kubectl rollout undois your emergency button in production. If you deploy a broken version and your error rate spikes, run this command immediately. Kubernetes switches all traffic back to the previous version in under 30 seconds — faster than redeploying the old image manually.
TipAlways set
maxUnavailable: 0in your rolling update strategy for production Deployments. This guarantees that your pod count never drops below the desired number during an update — meaning zero capacity reduction during releases. The cost is slightly slower rollouts, which is always the right tradeoff for production traffic.
Common MistakeNot setting
readinessProbeon your Deployment containers. Without a readiness probe, Kubernetes assumes a pod is ready the moment the container starts — even if your application takes 30 seconds to connect to the database and warm up its cache. Users hit the pod immediately and see errors. Always define a readiness probe that returns 200 only when the application is genuinely ready to serve requests.
SecurityNever use
latestas your image tag in a Deployment. Thelatesttag makes rollbacks impossible — if you roll back to a previous revision, Kubernetes pullslatestagain and gets the broken new image. Always use specific immutable tags like the Git commit SHA (registry.razorpay.in/api:a3f9c2d) or semantic version (registry.razorpay.in/api:v3.1.0) so every revision in your rollout history maps to a specific known build.
Frequently Asked Questions
Why does a Deployment manage a ReplicaSet instead of pods directly?
A Deployment doesn't touch pods itself — it creates and manages a ReplicaSet, which in turn owns the pods. This extra layer is what makes rolling updates and rollbacks possible: when you change the pod template, the Deployment creates a new ReplicaSet and gradually shifts replica counts from the old one to the new one, keeping the old ReplicaSet around (scaled to zero) so `kubectl rollout undo` can instantly reactivate the previous version.
What's a common Deployment misconfiguration that causes outages during updates?
Not setting readiness probes. Without one, Kubernetes considers a pod ready the moment its container starts, so a rolling update can route live traffic to a pod that's still initializing — connecting to a database, warming a cache — causing errors during every deploy. Pair a proper readiness probe with sensible `maxUnavailable`/`maxSurge` values in the rolling update strategy; the defaults (25%/25%) are often too aggressive for small replica counts like 2 or 3.