Skip to the document
Madhuopen lab
The Kubernetes Ninja PathTrack 1 — Kubernetes from the ground up

Chapter 4

ReplicaSets & Deployments — Self-Healing, Scaling Apps

6 min read read1,651 wordsBeginner6 recall cards

Before you read, guess

How does a ReplicaSet ensure the actual Pod count matches the desired count?

Take ten seconds and guess — even a wrong guess makes the answer stick. Tap to see where the chapter lands, or just read on.

A ReplicaSet has one job: keep the actual Pod count equal to the desired count. It finds "its" Pods by label selector, not by name, using the same watch-compare-act reconciliation loop from Post 1.

You already know a Pod can die and nobody will notice until your users do. Time to fix that with the two objects you'll actually use every single day in Kubernetes.

The problem with bare Pods

In Post 3, you created a Pod directly with kubectl run or a Pod YAML, and it worked fine. Now run a quick thought experiment: that Pod is running on Node 3, and at 2 AM, Node 3's kernel panics and the node goes down. What happens to your Pod?

Nothing happens. Nobody brings it back. A Pod cannot heal itself — it's just a request to run some containers somewhere, and once it's gone, it's gone. The kubelet that was watching it is gone too, because it lived on the node that just died. There's no controller saying "wait, that Pod is supposed to exist, let me recreate it." You'd have to notice the outage yourself and run kubectl apply again like a human on-call engineer. That's not how you run production software.

This is exactly the gap ReplicaSets and Deployments close.

ReplicaSet: the count-keeper

A ReplicaSet does one job: keep a fixed number of Pods running, all the time, no matter what. You tell it "I want 3 replicas of this Pod template," and it becomes obsessive about that number — never more, never less.

Here's the mechanism, and it's the same reconciliation loop from Post 1, just applied for real this time. The ReplicaSet controller runs this loop forever:

  • Watch actual state: count how many Pods currently exist that match its label selector.
  • Compare to desired state: check that count against spec.replicas.
  • Act on the gap: too few Pods? Create new ones from the Pod template. Too many (say someone manually created an extra Pod with matching labels)? Delete the excess.

One detail matters more than it looks: the ReplicaSet doesn't track Pods by name or remember "the ones I personally created." It tracks Pods purely by label. Any Pod in the cluster wearing the right labels counts toward its total, whether the ReplicaSet made it or not. That's what "owns" means here — a loose connection through labels, not a hard-coded list of names.

Deployment: what you actually use

Here's the part that trips people up: in real use, you almost never create a ReplicaSet by hand. You create a Deployment, and the Deployment creates and manages a ReplicaSet for you.

Why the extra layer? Because a ReplicaSet is dumb in one specific way: it doesn't know how to change the Pod template safely. Edit a ReplicaSet's image version directly, and it won't gracefully swap old Pods for new ones — that's not its job. A Deployment adds exactly that capability: rolling updates, plus a rollback history if the update goes wrong. Think of it as a controller that manages a controller.

The bakery window

Picture a bakery window that must always display exactly 5 loaves of sourdough.

The ReplicaSet is the automatic restocker. Its only job: count the loaves in the window, compare that count to the number 5, and fix the gap. A loaf gets sold (a Pod dies)? Bake a replacement. Someone slips an extra loaf into the window (an unexpected Pod with matching labels)? Pull it out. The restocker doesn't care about recipes, ingredients, or quality — it only cares that the number in the window is 5.

The Deployment is the shift manager standing behind the restocker. When head office says "switch to the new sourdough recipe," the manager doesn't dump all 5 loaves at once and leave the window empty — that would lose every customer who walks by. Instead, they swap loaves one at a time: pull one old loaf, put in one new-recipe loaf, wait, repeat, until all 5 are the new recipe. That's a rolling update. If customers hate the new recipe, the manager reverts just as gradually — a rollback — and keeps a notebook of every recipe change, so they know exactly what to go back to. That notebook is the rollout history.

Let's build one

Create a Deployment the same way you'd create anything else — imperatively for now, YAML comes in a later post.

$ kubectl create deployment nginx --image=nginx --replicas=3
deployment.apps/nginx created

Check what got created. Notice there are three objects stacked on top of each other now: the Deployment, its ReplicaSet, and the Pods.

$ kubectl get deployments
NAME    READY   UP-TO-DATE   AVAILABLE   AGE
nginx   3/3     3            3           12s

$ kubectl get rs
NAME               DESIRED   CURRENT   READY   AGE
nginx-7d6ccb8d9f    3         3         3       12s

$ kubectl get pods
NAME                     READY   STATUS    RESTARTS   AGE
nginx-7d6ccb8d9f-4kxlp   1/1     Running   0          12s
nginx-7d6ccb8d9f-9wqzr   1/1     Running   0          12s
nginx-7d6ccb8d9f-h2v6c   1/1     Running   0          12s

See the naming pattern? Deployment name → ReplicaSet name (Deployment name + a hash of the Pod template) → Pod name (ReplicaSet name + a random suffix). That hash matters: every time the Pod template changes, a new hash means a brand new ReplicaSet.

Scaling is just changing a number

$ kubectl scale deployment nginx --replicas=5
deployment.apps/nginx scaled

$ kubectl get pods
NAME                     READY   STATUS    RESTARTS   AGE
nginx-7d6ccb8d9f-4kxlp   1/1     Running   0          2m
nginx-7d6ccb8d9f-9wqzr   1/1     Running   0          2m
nginx-7d6ccb8d9f-h2v6c   1/1     Running   0          2m
nginx-7d6ccb8d9f-p8t2m   1/1     Running   0          8s
nginx-7d6ccb8d9f-qz7wn   1/1     Running   0          8s

Two new loaves appeared in the window. Same ReplicaSet, same recipe, just a new target count. Under the hood, the Deployment updated the ReplicaSet's replicas field to 5, and the reconciliation loop did the rest — it saw actual (3) didn't match desired (5), and created two more Pods.

Watch it heal itself

This is the moment that makes it click. Remember the reconciliation loop from Post 1? You're about to watch it run with your own eyes. Kill one of the Pods on purpose.

$ kubectl delete pod nginx-7d6ccb8d9f-4kxlp
pod "nginx-7d6ccb8d9f-4kxlp" deleted

$ kubectl get pods
NAME                     READY   STATUS    RESTARTS   AGE
nginx-7d6ccb8d9f-9wqzr   1/1     Running   0          3m
nginx-7d6ccb8d9f-h2v6c   1/1     Running   0          3m
nginx-7d6ccb8d9f-p8t2m   1/1     Running   0          40s
nginx-7d6ccb8d9f-qz7wn   1/1     Running   0          40s
nginx-7d6ccb8d9f-r4dxj   1/1     Running   0          3s

The Pod you deleted, nginx-7d6ccb8d9f-4kxlp, is gone for good — it never comes back. A brand new Pod, nginx-7d6ccb8d9f-r4dxj, took its place: new name, new IP, same job. Here's exactly what happened: the ReplicaSet controller watched actual state, saw the count drop from 5 to 4, compared that to the desired count of 5, found a gap of 1, and acted — it created a replacement immediately. Nobody paged anyone. Nobody typed a command. That watch-compare-act loop is the entire value of controllers in Kubernetes, and you just watched it happen in about three seconds.

Try it yourself right now: create a 3-replica Deployment, run kubectl get pods -w in one terminal to watch live, and in another terminal delete a Pod. Watch the new Pod appear in real time in the -w output. Then try deleting two Pods at once and see the controller replace both.

Rolling updates and rollback

Now let's change the recipe. Update the image version:

$ kubectl set image deployment/nginx nginx=nginx:1.25
deployment.apps/nginx image updated

$ kubectl rollout status deployment/nginx
Waiting for deployment "nginx" rollout to finish: 2 out of 5 new replicas have been updated...
Waiting for deployment "nginx" rollout to finish: 4 out of 5 new replicas have been updated...
deployment "nginx" successfully rolled out

Behind the scenes: the Deployment created a second ReplicaSet running nginx:1.25. It scaled that new ReplicaSet up one Pod at a time, and scaled the old ReplicaSet down by one Pod at a time, in lockstep. The window is never empty, never fully old, never fully new, mid-rollout. That's the rolling update.

Every rollout gets recorded, like the manager's notebook of recipe changes:

$ kubectl rollout history deployment/nginx
deployment.apps/nginx
REVISION  CHANGE-CAUSE
1         <none>
2         <none>

If nginx:1.25 turns out to be broken, revert instantly:

$ kubectl rollout undo deployment/nginx
deployment.apps/nginx rolled back

That triggers another rolling update, just running in reverse: the old ReplicaSet (now current again) scales back up, and the bad one scales back down, loaf by loaf. The window is never empty.

Exam trap: kubectl rollout restart deployment/nginx and kubectl rollout undo deployment/nginx sound alike but do different things, and the CKA loves testing the difference. restart recreates every Pod on the same image and spec — useful when a mounted ConfigMap or Secret changed, since Kubernetes never restarts Pods on its own for that. undo reverts to a previous ReplicaSet revision, changing the spec back. Also know rollout status (watch a rollout in progress) and rollout history (list revisions, or add --revision=2 for one revision's details) cold — they show up constantly.

Key Takeaways

  • A bare Pod cannot heal itself. If it dies, or its node fails, nothing recreates it — you'd have to notice and fix it by hand.
  • A ReplicaSet has one job: keep the actual Pod count equal to the desired count. It finds "its" Pods by label selector, not by name, using the same watch-compare-act reconciliation loop from Post 1.
  • A Deployment manages a ReplicaSet underneath it and adds two things a ReplicaSet can't do: rolling updates and rollback history. In practice, you create Deployments, not ReplicaSets, directly.
  • Changing the Pod template (like the image) creates a brand new ReplicaSet. The Deployment shifts Pods from old to new one at a time, so the app is never fully down.
  • kubectl rollout restart recreates Pods on the same spec (for picking up a ConfigMap or Secret change). kubectl rollout undo reverts to a previous spec revision. Different commands, different jobs — don't mix them up.
  • Delete a Pod that belongs to a ReplicaSet, and it's corrected instantly — a fresh Pod with a new name appears to restore the desired count.

Next up: Services — how Pods that come and go with new names and new IPs still get a stable way to talk to each other.

Before you go

In one sentence, what was this chapter about?

From memory, without scrolling up. Writing it is what makes it yours; the grade is only to show you what you had.

How sure?