Skip to main content

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.

◈ DIAGRAM
+------------------------------------------+
| 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 immediately

How 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.

◈ DIAGRAM
+------------------------------------------+
| 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

YAML
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-api
namespace: production
labels:
app: payment-api
team: payments
spec:
# -- 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 API

The 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:

◈ DIAGRAM
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

Bash
# Apply a Deployment from a YAML file
kubectl apply -f deployment.yaml -n production
# List all Deployments in a namespace
kubectl 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 progress
kubectl 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 tag
kubectl set image deployment/payment-api \
payment-api=registry.razorpay.in/payment-api:v3.2.0 \
-n production
# Scale replicas up or down
kubectl scale deployment payment-api --replicas=6 -n production
# Instantly rollback to the previous version
kubectl rollout undo deployment/payment-api -n production
# Rollback to a specific revision number
kubectl rollout undo deployment/payment-api \
--to-revision=2 \
-n production
# View rollout history
kubectl 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 rollout
kubectl rollout resume deployment/payment-api -n production
# Force a restart of all pods (without changing the image)
# Useful after updating a ConfigMap or Secret
kubectl rollout restart deployment/payment-api -n production

Deployment vs StatefulSet vs DaemonSet — When to Use Which

◈ DIAGRAM
+------------------------------------------+
| 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

Bash
# Get detailed Deployment status
kubectl 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 Deployment
kubectl 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 Deployment
kubectl get pods -n production -l app=payment-api
# Check live resource usage of all pods in a Deployment
kubectl top pods -n production -l app=payment-api

Annotating Deployments for Rollout History

By default, rollout history shows blank CHANGE-CAUSE entries. Add the --record flag or use annotations to track what changed:

Bash
# Record the change cause when updating
kubectl set image deployment/payment-api \
payment-api=registry.razorpay.in/payment-api:v3.2.0 \
-n production \
--record
# Or annotate manually before applying
kubectl annotate deployment payment-api \
kubernetes.io/change-cause="Release v3.2.0 — added UPI retry logic" \
-n production
# Now history shows meaningful messages
kubectl 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 logic

Deployment 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
Bash
# Full diagnostic sequence for a Deployment problem
# Step 1 — Check the Deployment overview
kubectl get deployment payment-api -n production
# Step 2 — Read the detailed status and events
kubectl describe deployment payment-api -n production
# Step 3 — Look at the pods individually
kubectl get pods -n production -l app=payment-api
# Step 4 — Read logs from a failing pod
kubectl logs <pod-name> -n production --previous
# Step 5 — If the new version is bad, rollback immediately
kubectl rollout undo deployment/payment-api -n production
Remember

kubectl rollout undo is 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.

Tip

Always set maxUnavailable: 0 in 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 Mistake

Not setting readinessProbe on 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.

Security

Never use latest as your image tag in a Deployment. The latest tag makes rollbacks impossible — if you roll back to a previous revision, Kubernetes pulls latest again 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.