Deployment and ReplicaSet: Keeping Your App Alive

K
Kai··6 min read

Article 3 ended on a problem: a bare pod, once deleted, is gone for good — no self-healing. The Deployment solves that completely and then some. This is the object you'll declare most in your Kubernetes career, so we dedicate a whole article to dissecting it and proving each of its promises.

Three layers: Deployment → ReplicaSet → Pod

First, understand the relationship. You declare a Deployment; it creates and manages a ReplicaSet; the ReplicaSet ensures the right number of Pods are running.

   Deployment "web"            (you manage this one)
        │  manages
        ▼
   ReplicaSet "web-5687..."    (ensures: always 3 pods)
        │  creates & keeps
        ▼
   Pod   Pod   Pod             (3 copies actually running)
  • ReplicaSet is a controller with one single job: "keep exactly N pods matching this label running". A pod dies → it builds a new pod. This is the self-healing mechanism.
  • Deployment sits above the ReplicaSet, adding the ability to update versions (rolling update) and rollback. When you change the image, the Deployment creates a new ReplicaSet and shifts the pods over gradually.

In practice you almost only work with the Deployment; the ReplicaSet is managed by it automatically. But knowing these three layers means kubectl get won't catch you off guard.

Declare a Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
spec:
  replicas: 3                 # desired state: 3 copies
  selector:
    matchLabels:
      app: web                # which pods does the Deployment manage? pods with label app=web
  template:                   # the "mold" for stamping out pods
    metadata:
      labels:
        app: web              # label stamped on each pod (must match the selector)
    spec:
      containers:
        - name: nginx
          image: nginx:1.27-alpine
          ports:
            - containerPort: 80

Three notable parts in the spec:

  • replicas: 3 — the desired state. This is the number the control loop will always keep.
  • selector.matchLabels — the Deployment uses labels to know which pods belong to it. This is how Kubernetes "binds" a controller to pods (label details in Article 6).
  • template — the mold for stamping out pods. Notice the part under template.spec looks exactly like a Pod's spec from Article 3 — because that's exactly what it is: the Deployment stamps out pods from this mold. The labels in template must match the selector, or Kubernetes reports an error.

apply and observe the three layers

kubectl apply -f deployment.yaml
kubectl rollout status deployment/web
deployment.apps/web created
Waiting for deployment "web" rollout to finish: 2 of 3 updated replicas are available...
deployment "web" successfully rolled out

Now look at all three layers at once:

kubectl get deploy,rs,pods -l app=web
NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/web   3/3     3            3           1s

NAME                             DESIRED   CURRENT   READY   AGE
replicaset.apps/web-5687994c96   3         3         3       1s

NAME                       READY   STATUS    RESTARTS   AGE
pod/web-5687994c96-9n9j7   1/1     Running   0          1s
pod/web-5687994c96-m6glb   1/1     Running   0          1s
pod/web-5687994c96-wtt7h   1/1     Running   0          1s

Reading the pod names shows the relationship right away: web (Deployment) → web-5687994c96 (ReplicaSet, hash suffix) → web-5687994c96-9n9j7 (Pod, plus a random suffix). Pod names are generated automatically and not predictable — never hard-code a pod name.

Self-healing: delete a pod, watch the magic

This is the biggest promise. Delete a pod outright:

kubectl delete pod web-5687994c96-9n9j7
kubectl get pods -l app=web
pod "web-5687994c96-9n9j7" deleted from default namespace

NAME                   READY   STATUS    RESTARTS   AGE
web-5687994c96-m6glb   1/1     Running   0          11s
web-5687994c96-qsk6p   1/1     Running   0          5s     ← NEW pod, just born
web-5687994c96-wtt7h   1/1     Running   0          11s

Still exactly 3 pods. The deleted one is gone, but a new pod (qsk6p, AGE only 5s) was built immediately to compensate. The ReplicaSet sees "want 3, have 2" and instantly pulls it back to 3 — exactly the control loop from Article 0. Compared to the bare pod in Article 3 (delete it and it's gone for good), the difference is night and day. This is the reason for existence of the Deployment.

Scale: up and down with one command

kubectl scale deployment/web --replicas=5
deployment.apps/web scaled

After a few seconds, count again: 5 pods. Changing replicas then re-running apply on the YAML gives the same result (and you should prefer this way so Git is the source of truth). Scale back down to 3 and the 2 extra pods are deleted. Scaling in Kubernetes is genuinely as light as a single number.

Rolling update: updating with no downtime

Now the part where the Deployment shines. Update the image to a new version:

kubectl set image deployment/web nginx=nginx:1.28-alpine
kubectl rollout status deployment/web
deployment.apps/web image updated
Waiting for deployment "web" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "web" rollout to finish: 1 old replicas are pending termination...
deployment "web" successfully rolled out

Notice how it plays out: Kubernetes does not kill all 3 old pods and then build the new ones (that would mean downtime). It replaces them gradually — builds a new pod, waits for it to be ready, only then deletes one old pod, and repeats. There's always a pod serving throughout the process. Looking at the ReplicaSets makes the mechanism clear:

kubectl get rs -l app=web
NAME             DESIRED   CURRENT   READY   AGE
web-55588d64c9   3         3         3       14s    ← new RS (1.28): 3 pods
web-5687994c96   0         0         0       39s    ← old RS (1.27): drained to 0

The Deployment keeps both ReplicaSets: the new one scaled up to 3, the old one scaled down to 0 (but not deleted). Keeping the old RS around is exactly what makes rollback possible.

Rollback: backing out when it breaks

The old ReplicaSet is still there, so rolling back to the previous version is just one command. View the history then undo:

kubectl rollout history deployment/web
kubectl rollout undo deployment/web
REVISION  CHANGE-CAUSE
1         <none>
2         <none>

deployment.apps/web rolled back

Check the image after rollback:

kubectl get deployment web -o jsonpath='{.spec.template.spec.containers[0].image}'
nginx:1.27-alpine

Back to 1.27. The mechanism: undo simply scales the old RS (1.27) back up and the new RS (1.28) down to 0 — also a rolling update, in reverse. When you deploy a broken build at midnight, a single rollout undo saves you. (Tip: add --record or set the annotation kubernetes.io/change-cause so the CHANGE-CAUSE column has content and the history is easier to read.)

Wrap-up

The Deployment is the standard way to run stateless applications on Kubernetes. It manages a ReplicaSet, and the ReplicaSet keeps exactly replicas pods alive — a dead pod is rebuilt automatically (self-healing, completely unlike a bare pod). Scaling is just changing a number. Updating the image triggers a rolling update: replacing pods gradually, with no downtime, by building a new RS and draining the old RS to 0 — and because the old RS is kept, rollout undo reverts to the previous version in one command. Always keep the YAML in Git as the source of truth.

We've got 3 healthy pods running, but each pod has its own IP and the IP changes on every rebuild. How do we reach them through one stable address, and balance the load across them? Article 5: the Service.