arundhaj

all that is technology

Kubernetes Deployments and ReplicaSets Explained with Examples

 

In the previous post on Pods we saw that a Pod you create by hand, once it is deleted or its node fails. Nothing brings it back. That's fine for a quick test, but not for a real application.

This is where ReplicaSets and Deployments come in. A ReplicaSet keeps a fixed number of Pods running. A Deployment manages ReplicaSets for you and adds rolling updates and rollbacks on top. In this post we'll build both from scratch, break things on purpose, and see how Kubernetes fixes them.

The big picture

The three objects stack on top of each other:

Deployment  →  manages  →  ReplicaSet  →  manages  →  Pods
  • A Pod runs your containers.
  • A ReplicaSet makes sure N copies of a Pod are always running.
  • A Deployment owns ReplicaSets and handles changing from one version of your app to the next.

In day-to-day work you create Deployments and let them create everything else. But it is easier to understand a Deployment once you've seen a ReplicaSet on its own, so let's start there.

What is a ReplicaSet?

A ReplicaSet has one job: keep the desired number of Pod replicas running at all times. It runs a simple loop:

  1. Count the Pods that match my label selector.
  2. Fewer than desired? Create more from my Pod template.
  3. More than desired? Delete the extras.

Here is replicaset.yaml:

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: web-rs
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web-rs
  template:
    metadata:
      labels:
        app: web-rs
    spec:
      containers:
        - name: nginx
          image: nginx:1.26
          ports:
            - containerPort: 80

The spec has three parts:

  • replicas — how many Pods you want.
  • selector — how the ReplicaSet finds its Pods. It matches on labels.
  • template — the Pod to create when more are needed. This is the same Pod spec you wrote in the Pods post, just nested inside.

Important: the labels in template.metadata.labels must match selector.matchLabels. If they don't, Kubernetes rejects the manifest, because the ReplicaSet would create Pods it can never find.

Apply it and look at what was created:

$ kubectl apply -f replicaset.yaml
replicaset.apps/web-rs created

$ kubectl get rs
NAME     DESIRED   CURRENT   READY   AGE
web-rs   3         3         3       10s

$ kubectl get pods
NAME           READY   STATUS    RESTARTS   AGE
web-rs-4kx9p   1/1     Running   0          10s
web-rs-m2tqz   1/1     Running   0          10s
web-rs-zr8vd   1/1     Running   0          10s

One manifest, three Pods. Each Pod name is the ReplicaSet name plus a random suffix.

Self-healing in action

Now delete one of the Pods and watch what happens:

$ kubectl delete pod web-rs-4kx9p
pod "web-rs-4kx9p" deleted

$ kubectl get pods
NAME           READY   STATUS    RESTARTS   AGE
web-rs-m2tqz   1/1     Running   0          2m
web-rs-x7lwc   1/1     Running   0          3s
web-rs-zr8vd   1/1     Running   0          2m

The ReplicaSet noticed it had only two Pods and immediately created web-rs-x7lwc to get back to three. The same happens when a node dies: the Pods on it are replaced on healthy nodes.

kubectl describe shows this in the Events:

$ kubectl describe rs web-rs
...
Events:
  Type    Reason            Age   From                   Message
  ----    ------            ----  ----                   -------
  Normal  SuccessfulCreate  2m    replicaset-controller  Created pod: web-rs-4kx9p
  Normal  SuccessfulCreate  2m    replicaset-controller  Created pod: web-rs-m2tqz
  Normal  SuccessfulCreate  2m    replicaset-controller  Created pod: web-rs-zr8vd
  Normal  SuccessfulCreate  3s    replicaset-controller  Created pod: web-rs-x7lwc

Scaling a ReplicaSet

Change the replica count and the ReplicaSet adds or removes Pods to match:

$ kubectl scale rs web-rs --replicas=5
replicaset.apps/web-rs scaled

$ kubectl get rs web-rs
NAME     DESIRED   CURRENT   READY   AGE
web-rs   5         5         5       4m

The problem with ReplicaSets

Let's say we want to upgrade nginx from 1.26 to 1.27. Edit the image in replicaset.yaml and apply it again:

$ kubectl apply -f replicaset.yaml
replicaset.apps/web-rs configured

Now check which image the Pods are running:

$ kubectl get pods -l app=web-rs -o jsonpath='{.items[*].spec.containers[0].image}'
nginx:1.26 nginx:1.26 nginx:1.26 nginx:1.26 nginx:1.26

Still 1.26. A ReplicaSet only uses its template when it creates a Pod. It already has enough Pods, so it does nothing. To get the new version you'd have to delete the old Pods yourself, one at a time, and hope nothing breaks in between. There's also no history and no easy way to go back.

That's exactly the gap a Deployment fills. Clean up before moving on:

$ kubectl delete rs web-rs
replicaset.apps "web-rs" deleted

Deleting a ReplicaSet also deletes the Pods it owns.

What is a Deployment?

A Deployment describes the desired state of your app: which image, how many replicas, and how to move from one version to the next. When you change the Pod template, the Deployment creates a new ReplicaSet for the new version and gradually shifts Pods from the old one to the new one. This is a rolling update, and it means no downtime.

Here is deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: nginx
          image: nginx:1.26
          ports:
            - containerPort: 80

Look familiar? Apart from kind: Deployment, it is the same as the ReplicaSet manifest. That's the nice thing: if you understand ReplicaSets, you already know how to write a Deployment.

Tip: Let kubectl generate the manifest for you:

$ kubectl create deployment web --image=nginx:1.26 --replicas=3 --dry-run=client -o yaml > deployment.yaml

Apply it:

$ kubectl apply -f deployment.yaml
deployment.apps/web created

$ kubectl get deployments
NAME   READY   UP-TO-DATE   AVAILABLE   AGE
web    3/3     3            3           15s

The columns mean:

  • READY — Pods ready out of the desired count.
  • UP-TO-DATE — Pods running the latest Pod template.
  • AVAILABLE — Pods available to serve traffic.

Deployment, ReplicaSet and Pods together

You only created a Deployment, but look at what exists now:

$ kubectl get deploy,rs,pods
NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/web   3/3     3            3           1m

NAME                             DESIRED   CURRENT   READY   AGE
replicaset.apps/web-6d8f7b9c4d   3         3         3       1m

NAME                       READY   STATUS    RESTARTS   AGE
pod/web-6d8f7b9c4d-2hjxk   1/1     Running   0          1m
pod/web-6d8f7b9c4d-9qmnv   1/1     Running   0          1m
pod/web-6d8f7b9c4d-tw4rz   1/1     Running   0          1m

The naming tells the whole story:

  • Deployment: web
  • ReplicaSet: web-6d8f7b9c4d (Deployment name + a hash of the Pod template)
  • Pods: web-6d8f7b9c4d-2hjxk (ReplicaSet name + random suffix)

That hash is stored as a pod-template-hash label on the ReplicaSet and its Pods. Every time the Pod template changes, the hash changes, and that's how the Deployment tells old Pods from new ones.

$ kubectl get pods --show-labels
NAME                   READY   STATUS    RESTARTS   AGE   LABELS
web-6d8f7b9c4d-2hjxk   1/1     Running   0          2m    app=web,pod-template-hash=6d8f7b9c4d
...

Self-healing still works because the Deployment's ReplicaSet is doing the same job as before. Delete a Pod and a new one appears.

Rolling update to a new version

Now let's do the upgrade that the ReplicaSet couldn't. You can change the image in deployment.yaml and run kubectl apply, or use kubectl set image for a quick change:

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

Here nginx= is the container name from the manifest, not the image name.

Watch the rollout as it happens:

$ kubectl rollout status deployment/web
Waiting for deployment "web" rollout to finish: 1 out of 3 new replicas have been 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

Now check the ReplicaSets:

$ kubectl get rs
NAME             DESIRED   CURRENT   READY   AGE
web-6d8f7b9c4d   0         0         0       5m
web-7f9c6b8d5e   3         3         3       30s

There are two. The Deployment created a new ReplicaSet web-7f9c6b8d5e for nginx:1.27, scaled it up step by step, and scaled the old one down to zero. The old ReplicaSet is kept around on purpose, so you can roll back to it.

kubectl describe deployment web shows each step in the Events:

$ kubectl describe deployment web
...
Events:
  Type    Reason             Age   From                   Message
  ----    ------             ----  ----                   -------
  Normal  ScalingReplicaSet  5m    deployment-controller  Scaled up replica set web-6d8f7b9c4d to 3
  Normal  ScalingReplicaSet  40s   deployment-controller  Scaled up replica set web-7f9c6b8d5e to 1
  Normal  ScalingReplicaSet  35s   deployment-controller  Scaled down replica set web-6d8f7b9c4d to 2 from 3
  Normal  ScalingReplicaSet  35s   deployment-controller  Scaled up replica set web-7f9c6b8d5e to 2 from 1
  Normal  ScalingReplicaSet  30s   deployment-controller  Scaled down replica set web-6d8f7b9c4d to 1 from 2
  Normal  ScalingReplicaSet  30s   deployment-controller  Scaled up replica set web-7f9c6b8d5e to 3 from 2
  Normal  ScalingReplicaSet  25s   deployment-controller  Scaled down replica set web-6d8f7b9c4d to 0 from 1

At every step some Pods were serving traffic. That's a zero-downtime update with one command.

Controlling the rollout with a strategy

By default a Deployment uses the RollingUpdate strategy. Two settings control how fast it goes:

spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  • maxSurge — how many extra Pods can exist above replicas during the update.
  • maxUnavailable — how many Pods can be missing below replicas during the update.

Both default to 25%. The setting above (maxSurge: 1, maxUnavailable: 0) is a safe choice: Kubernetes starts one new Pod, waits for it to be ready, and only then removes an old one. You never drop below full capacity.

The other strategy is Recreate, which deletes all old Pods before creating new ones. It causes downtime, but it is useful when two versions of your app must never run at the same time, for example during a database schema change.

Rollout history and rollback

Every change to the Pod template creates a new revision. List them with:

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

<none> isn't very helpful. Add a note to the current revision with the kubernetes.io/change-cause annotation:

$ kubectl annotate deployment/web kubernetes.io/change-cause="upgrade nginx to 1.27"
deployment.apps/web annotated

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

To see exactly what a revision contained:

$ kubectl rollout history deployment/web --revision=1

Now say version 1.27 has a bug in production. Rolling back is a single command:

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

This scales the old ReplicaSet back up and the new one down, using the same rolling update. To go back to a specific revision instead of the previous one:

$ kubectl rollout undo deployment/web --to-revision=1

This is why the Deployment keeps old ReplicaSets around. By default it keeps the last 10, controlled by spec.revisionHistoryLimit.

What happens when a rollout fails?

Let's push an image tag that doesn't exist:

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

$ kubectl get pods
NAME                   READY   STATUS             RESTARTS   AGE
web-6d8f7b9c4d-5bnqw   1/1     Running            0          3m
web-6d8f7b9c4d-kd7sm   1/1     Running            0          3m
web-6d8f7b9c4d-wl2xp   1/1     Running            0          3m
web-85b4c7d9f6-rj6tn   0/1     ImagePullBackOff   0          20s

The new Pod can't start, so the rolling update stops there. Your three old Pods are still running and still serving traffic. A broken release doesn't take your app down. Run kubectl describe pod on the failing Pod to see why, then fix it with kubectl rollout undo deployment/web.

Scaling a Deployment

Scaling works the same way as with a ReplicaSet, but you scale the Deployment, not the ReplicaSet underneath:

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

$ kubectl get deployment web
NAME   READY   UP-TO-DATE   AVAILABLE   AGE
web    5/5     5            5           10m

Scaling doesn't change the Pod template, so it doesn't create a new revision.

Note: If you manage your Deployment from a YAML file, update replicas in the file too. Otherwise the next kubectl apply will reset the count back to what the file says.

Pause and resume

If you want to make several changes and roll them out together as one revision, pause the Deployment first:

$ kubectl rollout pause deployment/web
$ kubectl set image deployment/web nginx=nginx:1.27
$ kubectl set resources deployment/web -c nginx --limits=cpu=250m,memory=128Mi
$ kubectl rollout resume deployment/web

Nothing rolls out while the Deployment is paused. When you resume, both changes go out in a single rolling update.

Restart all Pods without changing anything

Sometimes you just need fresh Pods, for example to pick up an updated ConfigMap or Secret. Instead of deleting Pods by hand, use:

$ kubectl rollout restart deployment/web
deployment.apps/web restarted

This replaces every Pod using a rolling update, so there's no downtime.

Clean up

$ kubectl delete deployment web
deployment.apps "web" deleted

This deletes the Deployment, all its ReplicaSets, and all their Pods.

Quick reference

Task Command
Create from manifest kubectl apply -f deployment.yaml
List Deployments, ReplicaSets, Pods kubectl get deploy,rs,pods
Update image kubectl set image deployment/web nginx=nginx:1.27
Watch a rollout kubectl rollout status deployment/web
See history kubectl rollout history deployment/web
Roll back kubectl rollout undo deployment/web
Scale kubectl scale deployment web --replicas=5
Restart Pods kubectl rollout restart deployment/web
Inspect kubectl describe deployment web

Should you ever use a ReplicaSet directly?

Almost never. A Deployment gives you everything a ReplicaSet does, plus rolling updates, history, and rollbacks, and the manifest is practically the same. The Kubernetes docs themselves recommend using Deployments instead of creating ReplicaSets directly.

It is still worth knowing how ReplicaSets work, because they are what's running under every Deployment. When you run kubectl get rs during a rollout and see two ReplicaSets, or when a Pod name has that extra hash in it, you now know exactly why.

Your Deployment runs inside a namespace. If you haven't read it yet, see Kubernetes Namespace Explained with Examples.

Comments