🎢 Rolling Updates
Blue-green needs a whole second environment. Canary needs weighted routing and metric analysis. The rolling update needs almost nothing extra — it just upgrades the instances you already have, a few at a time, keeping the rest serving traffic. It's the default zero-downtime strategy baked into Kubernetes, and the one you'll reach for most often.
Week 13 · Day 2 (Tuesday: Deployment Strategies) · Lecture 3
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Explain how a rolling update replaces instances batch by batch with zero downtime
- Configure
maxSurgeandmaxUnavailableto trade deployment speed against spare capacity - Write readiness and liveness probes that gate traffic until a new instance is truly ready
- Drive a Kubernetes Deployment rollout, pause it, and roll it back with
kubectl - Keep two versions compatible while they coexist during the rollout
- Use connection draining and graceful shutdown so in-flight requests aren't dropped
Estimated Time: 70 minutes
Practice: Write a Deployment with a tuned rolling strategy and probes, then drive and roll back a release.
In This Lesson
What Is a Rolling Update?
A rolling update upgrades a fleet of identical instances by replacing them incrementally — one small batch at a time — within a single environment. At any moment only a fraction of instances are being swapped; the rest keep handling requests, so the service never goes fully offline.
🔧 The tire-rotation analogy
A mechanic changing all four tires doesn't lift the whole car onto a crane. They jack up one corner, swap that tire, lower it, and move to the next. The car stays mostly on the ground the entire time. A rolling update jacks up one "corner" of your fleet at a time — the service keeps running at slightly reduced capacity, never fully down.
Compared with its cousins, the rolling update's appeal is efficiency: no duplicate environment (blue-green) and no weighted-routing machinery (canary). The cost is that two versions run at once during the rollout, so they must be compatible — a theme we'll return to.
Readiness Probes Gate Traffic
Here's the single most important safety mechanism in a rolling update: Kubernetes will not send traffic to a new pod until its readiness probe passes. Without a good probe, the load balancer would route users to a pod that has started but isn't actually ready — no database connection, caches cold — and they'd get errors.
| Probe | Question it answers | On failure |
|---|---|---|
| Readiness | Should this pod receive traffic right now? | Pod is removed from the Service until it passes again |
| Liveness | Is this pod still healthy, or stuck? | Pod is restarted |
| Startup | Has a slow-booting app finished starting? | Holds off the other probes until it passes |
# Probes turn a rolling update from "hope it works" into "verified each step".
spec:
containers:
- name: my-app
image: my-app:v2
ports:
- containerPort: 8080
readinessProbe: # GATE: no traffic until this returns 200
httpGet: { path: /health/ready, port: 8080 }
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe: # restart the pod if it wedges
httpGet: { path: /health/live, port: 8080 }
initialDelaySeconds: 15
periodSeconds: 20
A meaningful readiness endpoint checks the dependencies real requests need, not just that the process is alive:
// A readiness check that actually reflects "can serve traffic".
app.get('/health/ready', async (req, res) => {
try {
await db.query('SELECT 1'); // database reachable?
await redis.ping(); // cache reachable?
if (!global.appInitialized) throw new Error('warming up');
res.status(200).json({ status: 'UP', version: process.env.APP_VERSION });
} catch (err) {
// 503 keeps this pod OUT of the load balancer until it recovers.
res.status(503).json({ status: 'DOWN', reason: err.message });
}
});
// Liveness is deliberately shallow: is the process responsive at all?
app.get('/health/live', (req, res) => res.status(200).json({ status: 'UP' }));
⚠️ Don't make liveness deep
A tempting mistake is to check the database in the liveness probe too. But liveness failure restarts the pod — so a brief database hiccup would trigger a restart storm across your whole fleet, turning a small blip into an outage. Keep liveness shallow (is the process responsive?) and put dependency checks in readiness.
Driving & Rolling Back
With a Deployment in place, triggering a rolling update is one command: change the image. Kubernetes handles the batch-by-batch replacement, honoring your surge/unavailable settings and readiness gates.
# Trigger a rolling update by pointing at the new image.
kubectl set image deployment/my-app my-app=my-app:v2
# Watch it progress batch by batch (blocks until done or failed).
kubectl rollout status deployment/my-app
# See something wrong mid-rollout? Pause it where it stands.
kubectl rollout pause deployment/my-app
# ...investigate, then resume when you're satisfied.
kubectl rollout resume deployment/my-app
# Roll back to the previous revision — fast and built in.
kubectl rollout undo deployment/my-app
# Inspect the history of revisions.
kubectl rollout history deployment/my-app
✅ Rollback is a first-class operation
Kubernetes keeps a history of previous ReplicaSets, so kubectl rollout undo is itself just another rolling update — back to the old image. You don't rebuild anything. Combine it with automated monitoring so a spike in errors triggers the undo without a human in the loop.
That automation is straightforward: watch a metric after the rollout and call undo if it breaches a threshold.
#!/usr/bin/env bash
set -euo pipefail
DEPLOY="my-app"; NS="production"
ERROR_MAX=5 # percent
WINDOW=600 # watch for 10 minutes
PROM="http://prometheus:9090"
deadline=$(( $(date +%s) + WINDOW ))
while [ "$(date +%s)" -lt "$deadline" ]; do
err=$(curl -s "$PROM/api/v1/query" --data-urlencode \
'query=sum(rate(http_requests_total{status=~"5.."}[1m]))/sum(rate(http_requests_total[1m]))*100' \
| jq -r '.data.result[0].value[1] // "0"')
echo "error rate: ${err}%"
if (( $(echo "$err > $ERROR_MAX" | bc -l) )); then
echo "Error rate over threshold — rolling back."
kubectl rollout undo "deployment/$DEPLOY" -n "$NS"
exit 1
fi
sleep 15
done
echo "Rollout stable."
Version Compatibility
Because old and new instances run simultaneously during a rolling update, they must get along. Two versions of your code talk to the same database, and possibly to each other, at the same time.
add columns, never rename or drop"] A --> C["Backward-compatible APIs
add fields, keep old ones working"] A --> D["Graceful handling
ignore unknown fields, use defaults"]
The rules mirror the expand-contract pattern from the blue-green lesson, and they matter here for the same reason: the two versions overlap.
- Deploy schema changes first, additively. Add the new column before the code that uses it. The old version simply ignores it.
- Never remove a column, table, or API field until every instance is on the new version and any data migration is done.
- Provide defaults for new columns so old code that inserts a row without them still succeeds.
// New code must still read rows written by the OLD version.
function getCustomerPhone(customer) {
// New schema populated? Use it. Otherwise fall back to the legacy field.
return customer.phone_number ?? customer.legacy_phone ?? null;
}
// And old code must survive rows/fields the NEW version adds:
function handleRequest(body) {
const name = body.name;
// Unknown future fields are simply ignored — forward compatibility.
const settings = body.settings ?? getDefaultSettings();
return process(name, settings);
}
⚠️ The classic rolling-update outage
Someone ships a migration that renames a column in the same release as the code change. The instant the migration runs, every not-yet-upgraded instance queries a column that no longer exists — and half your fleet starts throwing 500s. Always split it: add the new column (expand), migrate all instances, backfill, then drop the old column in a later release (contract).
Connection Draining
When a pod is about to be replaced, it may still be in the middle of serving requests. Connection draining (graceful shutdown) means: stop accepting new requests, finish the ones already in flight, then exit. Skip this and users get dropped mid-request during every deploy.
In Node, handle SIGTERM: stop the server from taking new connections, let existing ones complete, and close your resource pools before exiting.
const server = app.listen(8080);
// Kubernetes sends SIGTERM before removing the pod. Drain gracefully.
process.on('SIGTERM', async () => {
console.log('SIGTERM received — draining connections.');
// 1. Stop accepting new connections; finish in-flight requests.
server.close(async () => {
// 2. Release shared resources once requests are done.
await redisClient.quit();
await db.end();
console.log('Drain complete — exiting.');
process.exit(0);
});
// 3. Safety net: force-exit if draining takes too long.
setTimeout(() => process.exit(1), 30_000).unref();
});
💡 Give the pod time to drain
Kubernetes waits terminationGracePeriodSeconds (default 30) between SIGTERM and a forceful kill. Set it comfortably longer than your slowest normal request so draining actually finishes. Pair this with a readiness probe that starts failing on shutdown, so the pod leaves the load balancer before it stops accepting work.
Practice & Quiz
🏋️ Exercise 1: Tune the strategy for zero capacity loss
Goal: You run 8 replicas of a latency-sensitive API and cannot afford to drop below full capacity during a deploy, but you have quota for a couple of extra pods. Write the strategy block and explain the trade-off.
💡 Hint
To never lose capacity, new pods must come up before old ones leave. Which knob allows extra pods, and which must be zero?
✅ Solution
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 2 # spin up 2 new pods at a time (uses spare quota)
maxUnavailable: 0 # never drop below the 8 desired
Trade-off: you temporarily run up to 10 pods, consuming extra resources, in exchange for holding full serving capacity throughout the rollout.
🏋️ Exercise 2: Diagnose the 500s
Goal: During a rolling update, error rates spike briefly every time a new pod appears, then settle. Users hit "connection refused" for a second or two. What's the likely cause and fix?
✅ Solution
Traffic is reaching pods before they're ready — either there's no readiness probe, or its initialDelaySeconds is too short so it passes before the app can actually serve. Add or tighten the readiness probe to check real dependencies, so Kubernetes only adds the pod to the Service once it can genuinely handle requests.
🎯 Quick Quiz
Question 1: What does a rolling update do?
Question 2: With maxUnavailable: 0 and maxSurge: 1, what happens during the update?
Question 3: Why should the liveness probe stay shallow instead of checking the database?
Best Practices & Pitfalls
✅ Do
- Always define a readiness probe that checks real dependencies
- Start conservative: small
maxSurge,maxUnavailable: 0for capacity-sensitive services - Keep API and schema changes backward-compatible (additive first)
- Implement graceful shutdown on
SIGTERMand set a generous grace period - Monitor error rate and latency during the rollout, and automate rollback
- Use
kubectl rollout pauseto freeze a suspicious deploy while you investigate
❌ Don't
- Don't ship without a readiness probe — traffic will hit half-ready pods
- Don't put dependency checks in the liveness probe (restart storms)
- Don't rename or drop columns in the same release as the code change
- Don't set both
maxSurgeandmaxUnavailableto 0 — the rollout can't progress - Don't ignore in-flight requests; skipping drain drops users mid-request
📖 Choosing among the three strategies
Reach for a rolling update as your everyday default — cheap, built-in, zero-downtime. Choose blue-green when you need an atomic cutover and instant, total rollback. Choose canary when a bug would be costly and you want real-user validation on a small slice before committing. They also combine — a canary step ahead of a fuller rollout is common.
Summary
🎉 Key Takeaways
- A rolling update replaces instances batch by batch in one environment — zero downtime, minimal extra capacity
- maxSurge allows temporary extra pods; maxUnavailable caps how much capacity you'll lose mid-rollout
- Readiness probes gate traffic — a pod joins the load balancer only once it can truly serve
- Keep liveness shallow; deep dependency checks belong in readiness to avoid restart storms
- Two versions coexist, so keep schema and APIs backward-compatible (additive first, drop later)
- Handle SIGTERM for graceful connection draining, and lean on
kubectl rollout undofor fast rollback
📚 Additional Resources
- Kubernetes — Rolling update Deployments
- Kubernetes — Liveness, readiness & startup probes
- AWS ECS — Rolling update deployments
🚀 What's Next?
You've now seen the three core deployment strategies. All of them assume infrastructure already exists to deploy onto. The next lesson steps back to ask how that infrastructure itself is created and versioned, reproducibly, from code: Infrastructure as Code (IaC) concepts.
🎉 That's the deployment trilogy!
Blue-green, canary, and rolling updates are now in your toolkit. Next we define the infrastructure they all run on — as code.