Changing the image of a Deployment looks like one action, but the controller turns it into dozens of small steps: start a few new Pods, wait for them to be ready, stop a few old ones, repeat. If you understand what decides the size of each step and what has to be true before the next one, you can predict how a rollout will behave, and you can explain why a bad release sometimes takes the whole service down and sometimes doesn’t.
Examples assume Kubernetes 1.30 or later. Command output below is illustrative of what kubectl prints rather than captured from a specific cluster.
Deployments manage ReplicaSets, not Pods
A Deployment doesn’t create Pods directly. It creates a ReplicaSet for each distinct Pod template, and the ReplicaSet creates Pods. The controller computes a hash of the template and adds it as the pod-template-hash label, so each ReplicaSet’s selector only matches its own Pods.
$ kubectl get rs -n shop -l app=checkout
NAME DESIRED CURRENT READY AGE
checkout-6d8f7c9b54 10 10 10 3dChange anything in spec.template (image, env var, resource request, an annotation) and the Deployment creates a new ReplicaSet. Change only spec.replicas and it doesn’t: scaling isn’t a new revision. That distinction comes up in interviews, usually as “why didn’t my rollout history get a new entry when I scaled?”
The old ReplicaSets are kept, scaled to zero, as rollout history. revisionHistoryLimit (default 10) controls how many survive, and they’re what kubectl rollout undo scales back up.
maxSurge and maxUnavailable, with real numbers
The RollingUpdate strategy has two knobs. maxSurge is how many Pods above the desired count may exist; maxUnavailable is how many below the desired count may be unavailable. Both default to 25%. Percentages are converted to Pod counts with different rounding: surge rounds up, unavailable rounds down.
For checkout with 10 replicas and defaults:
- maxSurge = ceil(2.5) = 3, so at most 13 Pods total.
- maxUnavailable = floor(2.5) = 2, so at least 8 must be available.
The controller immediately scales the new ReplicaSet to 3 (using the surge) and the old one down to 8 (using the unavailable budget). As new Pods become available, it scales the old ReplicaSet down further and the new one up, always staying inside 8 available and 13 total.
For a service where capacity matters more than speed, teams often set:
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkout
namespace: shop
spec:
replicas: 10
revisionHistoryLimit: 5
progressDeadlineSeconds: 300
minReadySeconds: 10
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 2
maxUnavailable: 0
selector:
matchLabels:
app: checkout
template:
metadata:
labels:
app: checkout
spec:
containers:
- name: api
image: registry.example.com/checkout:5.4.0
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 5maxUnavailable: 0 means the service never drops below 10 available Pods; the rollout only removes an old Pod after a new one is available. It needs spare cluster capacity for the 2 surge Pods, otherwise they sit Pending and the rollout stalls (see Pod Pending). Setting both to 0 is rejected by validation, because the rollout could never move.
What “available” means, and why readiness is the brake
The controller only counts a new Pod as available when it’s Ready, and has stayed Ready for minReadySeconds. That makes the readiness probe the brake on a rollout.
Walk through a bad release. Version 5.4.0 has a typo in the database host. With a readiness probe that actually checks the app can serve, the new Pods start, fail readiness, and never become available. With maxUnavailable: 0, the controller never removes an old Pod. You end up with 10 healthy old Pods serving traffic and 2 broken new ones that the Service ignores. Users notice nothing.
Now the same release without a readiness probe. A container that has started counts as Ready immediately. The controller sees “available” Pods, scales the old ReplicaSet down, and within a minute all 10 Pods are the broken version. The rollout reports success. This is the most common reason people say “rolling updates didn’t protect us”, and the cause is almost always a missing or trivial readiness probe. Probe separation covers what a useful one checks.
minReadySeconds adds protection against releases that pass readiness and then crash after a few seconds, for example when the first real request triggers a fatal code path. Ten seconds of stable readiness before a Pod counts is cheap insurance.
Watching, pausing and rolling back
The core commands:
kubectl set image deployment/checkout api=registry.example.com/checkout:5.4.1 -n shop
kubectl rollout status deployment/checkout -n shop
kubectl rollout history deployment/checkout -n shop
kubectl rollout undo deployment/checkout -n shop # previous revision
kubectl rollout undo deployment/checkout -n shop --to-revision=7
kubectl rollout pause deployment/checkout -n shop
kubectl rollout resume deployment/checkout -n shoprollout status blocks while the rollout progresses and prints messages like Waiting for deployment "checkout" rollout to finish: 4 out of 10 new replicas have been updated.... It exits non-zero if the progress deadline passes, which makes it a good gate in a CI pipeline.
progressDeadlineSeconds (default 600) is how long the controller waits without progress before it sets the Progressing condition to False with reason ProgressDeadlineExceeded. In the broken-release example, the condition flips after 300 seconds:
$ kubectl rollout status deployment/checkout -n shop
error: deployment "checkout" exceeded its progress deadlineHere’s what interviewers listen for: Kubernetes does not roll back automatically. The deadline only marks the Deployment as failed. The broken Pods stay, the old ones stay, and someone or something (a CI step, Argo Rollouts, Flagger) has to run kubectl rollout undo. Undo itself is just another rollout: it copies the old revision’s template into the Deployment, so it follows the same surge and readiness rules.
kubectl rollout history shows a CHANGE-CAUSE column filled from the kubernetes.io/change-cause annotation. The old --record flag that set it is deprecated, so if you want useful history, set the annotation yourself in your deploy step.
Recreate, and when rolling isn’t enough
The other strategy is Recreate: scale the old ReplicaSet to zero, then create the new one. There’s downtime, but old and new versions never run together. You’d choose it when two versions can’t coexist, for example when the new version runs a schema migration the old one can’t handle, or when the app holds a ReadWriteOnce volume only one Pod can mount.
Rolling updates also mean both versions serve traffic for a while. If 5.4.0 changes an API response format, clients may get the old format from one request and the new from the next. That’s a design concern rather than a Kubernetes one: keep changes backward compatible across one release, or use a canary or blue-green approach with a separate Deployment and traffic shifting at the Service mesh or ingress level.
Interview practice
“A team has a 4-replica Deployment with default rollout settings and no readiness probe. They push an image whose app crashes 20 seconds after startup. Describe what happens, then what you’d change.”
Answer
With 4 replicas, the defaults give maxSurge = ceil(1) = 1 and maxUnavailable = floor(1) = 1. The controller creates 1 new Pod and terminates 1 old one. With no readiness probe, each new container counts as Ready as soon as it starts, so it’s immediately “available” and the controller keeps replacing old Pods. The whole rollout can finish in seconds, so rollout status may report success before the first crash at 20 seconds, and even if it doesn’t, nothing makes the controller stop. Then every Pod crashes, restarts, and eventually enters CrashLoopBackOff. The service is fully down and nothing stopped it.
I’d add a readiness probe that hits an endpoint which only succeeds when the app can serve, set minReadySeconds to something longer than the crash window (30 seconds here) so a Pod has to stay Ready before it counts, and consider maxUnavailable: 0 so capacity never drops. In the pipeline, I’d run kubectl rollout status with a timeout and call kubectl rollout undo when it fails. For the immediate incident: kubectl rollout undo deployment/<name> brings back the previous ReplicaSet, and the CrashLoopBackOff guide helps find out why the new version crashes.