BitBootcampBitBootcamp ← all explainers 🔄 Review ⚙ Settings

DEVOPS-02-004-P · Primer — Deployments & ReplicaSets

The idea in one sentence

A Deployment states the desired pod count and image; the ReplicaSet it owns runs a tight control loop that keeps creating or deleting pods until reality matches that desired state, and the Deployment layer versions ReplicaSets so it can roll that state forward safely.

Why it exists

It's the thermostat pattern: you don't manually flip the furnace on and off to hit 68°F, you tell the thermostat 68°F and it corrects continuously. Kubernetes needs the same thing because pods die for reasons no human is watching for — a node loses power, the kubelet OOM-kills a container, a spot instance gets reclaimed. Without a controller, "5 replicas" would be a one-time instruction that decays the instant the first pod disappears, and nobody would notice until a customer hit a 502. The ReplicaSet exists so "5" is not a command you gave once but an invariant something enforces forever; the Deployment exists on top of that so when you change the pod template (a new image tag), the same reconcile machinery can migrate you from the old invariant to the new one pod-by-pod instead of all at once.

How it works

A Deployment object never touches pods directly — it creates a ReplicaSet, and the ReplicaSet's own controller does the work. That controller is a control loop: it lists pods matching its label selector (via a watch-fed informer cache, not a fresh API call each tick), counts them, and diffs that count against spec.replicas. If the count is short, it calls the API server to create the missing pods from the pod template; if there's a surplus, it deletes the extras. This is pure reconciliation — the loop doesn't care why the numbers diverged (crash, manual kubectl delete pod, node drain), it only cares about closing the gap, and it re-runs on every relevant watch event plus a periodic resync, so drift never lasts long.

Reconciling the replica count is the easy half; a Deployment's harder job is reconciling the pod template without an outage, which is where maxSurge/maxUnavailable math comes in. Say a Deployment is running 10 replicas with the default rolling-update strategy of \(\text{maxSurge}=25\%\) and \(\text{maxUnavailable}=25\%\). Kubernetes computes \(\text{maxSurge} = \lceil 10 \times 0.25 \rceil = \lceil 2.5 \rceil = 3\) extra pods it's allowed to create above the target, and \(\text{maxUnavailable} = \lfloor 10 \times 0.25 \rfloor = \lfloor 2.5 \rfloor = 2\) pods it's allowed to take offline at once. That bounds the whole rollout: total pod count across old + new ReplicaSets never exceeds \(replicas + \text{maxSurge} = 10 + 3 = 13\), and ready pod count never drops below \(replicas - \text{maxUnavailable} = 10 - 2 = 8\). The Deployment controller walks this by scaling the new ReplicaSet up in small batches and the old one down to match, re-running the same create/delete reconcile loop on each ReplicaSet at every step, until the new ReplicaSet holds all 10 and the old one holds 0 (kept at revision history, not deleted, so kubectl rollout undo can reverse it).

The everyday case is simpler and is exactly what the lab models: a node carrying 2 of your 5 pods gets cordoned and drained. The ReplicaSet controller's next list shows 3 running pods against a desired 5, computes the gap as \(5 - 3 = 2\), and issues 2 create calls — no human paged, no ticket filed, service degraded for however many seconds scheduling and image pull take, then fully healed.

See it — The reconcile loop: compare desired to actual, create or delete the difference, repeat.

flowchart TD
  A["Deployment spec:<br/>replicas = 5"] --> B["ReplicaSet controller<br/>lists matching pods"]
  B --> C{"actual count<br/>vs desired?"}
  C -->|"equal"| D["No action,<br/>wait for next event"]
  C -->|"too few"| E["Create pods<br/>(diff is positive)"]
  C -->|"too many"| F["Delete pods<br/>(diff is negative)"]
  E --> B
  F --> B

The one gotcha

Deleting or evicting a Deployment-managed pod isn't "removing the workload" — the ReplicaSet controller notices count < desired on its next reconcile (sub-second) and creates a replacement, so pod identity is disposable but pod count is enforced. The trap runs the other way too: kubectl scale rs/<name> --replicas=N on a ReplicaSet owned by a Deployment gets silently reverted, because the Deployment object's spec.replicas is the real source of truth and the next sync overwrites whatever you set on the child.

The one-line takeaway

Controller loop keeps recreating pods until counts match.

Where you'll meet it

kubectl get rs,deploy, kubectl describe deployment, kubectl rollout status/history/undo, kube-controller-manager (runs the ReplicaSet control loop), Helm charts (author Deployments), k9s (watch RS churn live during a rollout), and every managed control plane — EKS, GKE, AKS — since this loop is core Kubernetes, not a cloud add-on.

Named concepts: replica reconcile