Part II — The control plane and the loop
Deployments: Changing Desired State Safely
Lesson 04 gave you a ReplicaSet: a controller that watches a label selector and keeps exactly N matching Pods alive, level-triggered and idempotent. But a ReplicaSet pins one Pod template — one image, one version. To ship v2 you would have to edit the template in place (the ReplicaSet will not re-roll existing Pods) or delete the v1 set and create a v2 set. The first does nothing; the second tears down every v1 Pod at once — a hard outage — and if v2 crashes on boot, there is no button to bring v1 back. This lesson derives the Deployment: a controller of ReplicaSets that changes desired state safely, with bounded capacity loss and one-command undo.
New capability: a Deployment owns a set of ReplicaSets, creates a new one per version, shifts replicas old→new under a strategy (RollingUpdate or Recreate) that bounds capacity loss, and keeps revision history for one-command rollback.
Forces next: rolling Pods are deleted and recreated, so they get new IPs every time — a client can never be pointed at a Pod that keeps moving.
1 · Why a ReplicaSet cannot release
A ReplicaSet's whole job is one equation: observed Pods matching selector == desired count. It does not own the idea of an upgrade. If you change its template (say, bump the image from app:v1 to app:v2), the ReplicaSet does not touch the Pods it already has — they still run v1. The new template only applies to Pods it creates from now on. So you would have to manually delete v1 Pods to force replacements, with no control over the rate, no health-gating, and no record of what v1 even was.
The blunt alternative — delete the v1 ReplicaSet, create a v2 ReplicaSet — drops your replica count to 0 the instant the old Pods terminate, then climbs back up only after v2 images pull and boot. For a 10-replica service that is a full outage lasting the pull-plus-boot time (tens of seconds to minutes for a large image). And if v2 segfaults on startup, you are now staring at 10 crash-looping Pods with the v1 template gone from the cluster. You need a controller that remembers v1, introduces v2 gradually, and can reverse course. That is a Deployment.
2 · The Deployment: a controller of ReplicaSets
A Deployment is a higher-level controller whose desired state is "N Pods of this template, reached via this strategy." It does not manage Pods directly. Instead it manages ReplicaSets, each of which manages Pods — the same ownership-by-selector idea from lesson 04, stacked one level higher.
Deployment app=web, replicas=10, strategy=RollingUpdate
│ (owns, by template hash)
├── ReplicaSet app=web,pod-template-hash=v1 desired 0 ← old revision (kept)
│ └── (Pods, draining to 0)
└── ReplicaSet app=web,pod-template-hash=v2 desired 10 ← new revision
└── Pod Pod Pod Pod Pod Pod Pod Pod Pod Pod
When you change the Pod template, the Deployment controller hashes the new template into a pod-template-hash label, creates a brand-new ReplicaSet for that hash, and then drives a transfer: it scales the new ReplicaSet up and the old one down, a few replicas at a time, until the new set holds all the replicas and the old set holds zero. Crucially, it does not delete the old ReplicaSet — it leaves it scaled to 0 as a recorded revision. That retained, scaled-to-zero ReplicaSet is exactly what makes rollback a one-liner (§5).
3 · Two strategies: Recreate vs RollingUpdate
The strategy answers one question: how much capacity may I lose, and how much extra may I spend, while moving from old to new?
| Strategy | What it does | Capacity during roll | Use when |
|---|---|---|---|
| Recreate | Scale old ReplicaSet to 0, wait for all old Pods to terminate, then scale new to N. | Drops to 0 — a deliberate outage window. | The app cannot run two versions at once (e.g. an exclusive DB schema lock, a singleton). Simplicity over availability. |
| RollingUpdate (default) | Incrementally add new Pods and remove old ones, bounded by maxSurge and maxUnavailable, until all replicas are new. | Stays near N — never below N − maxUnavailable. | Almost always — any stateless or version-compatible service that must stay up. |
RollingUpdate is governed by two knobs, each an absolute count or a percentage of desired replicas (rounded — maxSurge rounds up, maxUnavailable rounds down):
The two together set the width of the moving window. The controller never lets the live, available count fall below N − maxUnavailable, and never lets the total (old + new) Pod count exceed N + maxSurge. Inside that window it works as fast as new Pods become Ready.
4 · The rollout arithmetic (replicas = 10)
Take replicas = 10, maxSurge = 25% → 3 (rounded up), maxUnavailable = 25% → 2 (rounded down). The floor on available capacity is 10 − 2 = 8; the ceiling on total Pods is 10 + 3 = 13. Watch one representative step sequence (old = v1, new = v2, "available" = Ready Pods of either version):
The exact ordering depends on how fast new Pods pass readiness, but the invariants are hard: available never drops below 8, total never exceeds 13. Now break it: set maxUnavailable = 60% → 6. The floor becomes 10 − 6 = 4. If your service needs 7 Pods to hold peak traffic under its SLO, the roll is now allowed to spend a window at 4 available — below the line — and users see errors or latency even though the deploy "succeeded." That dip is invisible in the YAML; the widget below makes it visible.
5 · History, rollback, readiness, and how traffic still finds Pods
Because the Deployment keeps each prior ReplicaSet scaled to 0, the cluster holds a revision history (default 10 revisions, set by revisionHistoryLimit). Rolling back is therefore not a redeploy — it is re-running the same scale-transfer in reverse:
What stops the controller from marching forward into a broken v2? Readiness. A new Pod counts as "available" only once it passes its readiness probe — a health check that says "this Pod can serve traffic" (covered fully in lesson 11). Until a fresh v2 Pod is Ready, it does not count toward the available total, so the floor (N − maxUnavailable) holds the line: the controller will not delete more old Pods until enough new ones are genuinely serving. If v2 never becomes Ready, the roll stalls at the floor instead of completing — your service stays up on v1 Pods while you investigate, and rollout undo reverses it. (Without good readiness probes this safety net is gone: Kubernetes counts a booting-but-broken Pod as available and rolls right over your last good replicas.)
One more thread from lesson 04 closes here. Throughout the roll, old and new Pods all carry the Deployment's selector labels (e.g. app=web) — the pod-template-hash only distinguishes ReplicaSets, not the app-level selector. So anything that finds Pods by that selector automatically tracks whichever Pods currently exist, v1 or v2, without being told. That label-query indirection is precisely how a stable front end (a Service) will keep routing to a moving set — which is the next lesson.
Failure modes & checklist
Failure modes
- maxUnavailable too high. A roll allowed to drop well below the capacity your traffic needs. Signal: error rate / latency spikes during every deploy that vanish once it finishes; the Deployment still reports success.
- maxSurge = 0 and maxUnavailable = 0. The controller can neither add nor remove a Pod — the roll deadlocks forever. Signal:
rollout statushangs; both knobs cannot be zero at once. - No (or fake) readiness probe. Booting v2 Pods are counted as available, so the floor is meaningless and the roll steamrolls your last good v1 Pods. Signal: brief total outage mid-roll even though "min available" looked safe on paper.
- Editing the ReplicaSet, not the Deployment. Changes get reverted by the Deployment controller (it owns the set) or create an orphan. Signal: your manual edits silently disappear within seconds.
- Recreate on a multi-replica user-facing service. Full outage window because capacity hits 0. Signal: every deploy shows a hard gap in availability, not a dip.
Checklist
- Change the Deployment template, never a ReplicaSet directly — let the higher controller own the roll.
- Set
maxUnavailablefrom your real capacity floor: N − maxUnavailable ≥ peak-load replica need. - Set
maxSurgefrom headroom you can afford to pay for transiently (and that the cluster can schedule). - Ship a readiness probe that returns true only when the Pod can actually serve — it is what gates the roll (lesson 11).
- Use
kubectl rollout statusin CI to gate on a finished, healthy roll; keeprevisionHistoryLimithigh enough to roll back. - Choose Recreate only when two versions genuinely cannot coexist, and accept the outage window.
Checkpoint exercise
maxUnavailable to 50% and redeploy: how far does min-available fall, and does it cross the SLO line? Finally, set both maxSurge and maxUnavailable to 0 and explain in one sentence why the roll can never make progress.Where this points next
A Deployment now lets you change desired state safely: new ReplicaSet per version, bounded capacity loss, one-command rollback. But notice what just happened to every Pod. A rolling update deletes and recreates Pods — and from lesson 02 we know a fresh Pod gets a fresh cluster IP. So after a single roll, none of the IPs a client might have cached point at a live Pod; the entire backend set has moved. Worse, this happens on every scale event and every restart, not just deploys. A stable selector lets a controller find the current Pods, but a human or a remote client still has nothing fixed to dial. That is the problem lesson 06, Networking & Services, solves: a stable virtual IP and name that load-balances across whatever Pods the selector matches right now, no matter how often they churn.
pod-template-hash) and runs the lesson-00 loop to transfer replicas old→new under a strategy — Recreate (drop to 0, then refill) or the default RollingUpdate, bounded by maxSurge (total ≤ N + maxSurge) and maxUnavailable (available ≥ N − maxUnavailable). For replicas=10 at 25%/25% the roll never exceeds 13 Pods nor drops below 8 available; push maxUnavailable too high and availability dips below your SLO even on a "successful" deploy. Old ReplicaSets are kept scaled to 0 as revision history, so rollout undo reverses the same transfer with no rebuild; readiness gates each forward step so a broken v2 stalls instead of steamrolling v1; and the shared selector labels let traffic-finding controllers track whichever Pods exist. The catch it leaves: rolled Pods get new IPs every time — which forces a stable Service.Interview prompts
- Why can't a ReplicaSet release a new version on its own? (§1 — it reconciles a Pod count against one frozen template; changing the template doesn't re-roll existing Pods, and delete-and-recreate means a 0-capacity outage with no recorded prior state to undo.)
- What exactly does a Deployment manage, and what makes rollback cheap? (§2, §5 — it manages ReplicaSets (one per template hash), not Pods directly; it keeps the old ReplicaSet scaled to 0 as a revision, so
rollout undojust scales it back up — no rebuild or redeploy.) - Define maxSurge and maxUnavailable and the two invariants they enforce. (§3, §4 — surge bounds extra Pods (total ≤ N + maxSurge, rounds up); unavailable bounds capacity loss (available ≥ N − maxUnavailable, rounds down). The roll moves inside that window.)
- For replicas=10, maxSurge=25%, maxUnavailable=25%, what are the total ceiling and available floor? (§4 — surge = 3 (up), unavailable = 2 (down); total never exceeds 13, available never drops below 8.)
- Recreate vs RollingUpdate — when and why pick Recreate? (§3 — Recreate drops capacity to 0 then refills; pick it only when two versions cannot coexist (exclusive lock, singleton, breaking schema), accepting an outage window. RollingUpdate is the default for staying up.)
- How does readiness gate a rollout, and what breaks without it? (§5 — a new Pod counts as available only after its readiness probe passes, so the floor holds until enough new Pods truly serve; without a real probe, booting/broken Pods count as available and the roll steamrolls the last good replicas.)
- During a roll, how does traffic keep reaching the right Pods, and what problem does the roll then create? (§5, "Where next" — all Pods carry the shared selector labels so a label-query tracks whichever exist; but rolled Pods are recreated with new IPs, so nothing fixed remains to dial — forcing a Service.)