Kubernetes basics · Lesson 5 of 12 · about 30 minutes

Deployments: keeping copies running

How a Deployment keeps your app running, scales it, updates it without downtime, and rolls it back when a release goes wrong. Plus the other ways to run pods, and when to use each.

By the end you'll know

  • What a Deployment actually manages, and the ReplicaSet hiding underneath it
  • How to scale an app, and ship a new version without taking it down
  • What happens when a release is broken, and how to roll it back in one command
  • Which of the other workload types to use for databases, per-node agents and scheduled tasks

What a Deployment does

In Lesson 3 you saw that a bare pod doesn't come back once it's deleted. A Deployment fixes that. You tell it what you want, "3 copies of this app, running this image", and it keeps that true: it replaces pods that disappear, adds or removes pods when you change the count, and swaps in a new version a few pods at a time when you change the image.

It doesn't manage pods directly, though. There's a middle layer:

A Deployment during a rolling updateA Deployment asks for 3 copies of nginx 1.28. It owns two ReplicaSets. The old ReplicaSet, for nginx 1.27, is shrinking and has 1 pod left. The new ReplicaSet, for nginx 1.28, is growing and has 3 pods, one of them still starting. When the update finishes, the old ReplicaSet has 0 pods and is kept so you can roll back.Deployment: hellowant: 3 copies of nginx:1.28Old ReplicaSetnginx:1.27 · shrinking: 1 pod leftPodnginx:1.27removedremovedNew ReplicaSetnginx:1.28 · growing: 3 podsPodnginx:1.28Podnginx:1.28PodstartingNew pods start before old ones stop, so the app keeps serving throughout the update.
A rolling update in progress. The Deployment creates a new ReplicaSet for the new version and moves pods across a few at a time. The old ReplicaSet stays behind with 0 pods, which is what makes a rollback instant.
  • The Deployment holds your intent: the image, the number of copies, and how to update.
  • A ReplicaSet keeps an exact number of identical pods running. The Deployment creates one ReplicaSet per version of your app.
  • The pods are the actual running copies.

You almost never create a ReplicaSet yourself. The Deployment does it for you, which is why you'll see them in listings without having written one.

Try it: create a Deployment

Start a cluster if you don't have one running:

BASH
kind create cluster --name learn

Save this as deploy.yaml. It's the same shape you read in Lesson 4, with three copies:

YAML
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello
spec:
  replicas: 3
  selector:
    matchLabels:
      app: hello
  template:
    metadata:
      labels:
        app: hello
    spec:
      containers:
        - name: hello
          image: nginx:1.27

Apply it and look at all three layers at once:

BASH
kubectl apply -f deploy.yaml
kubectl get deployments,replicasets,pods
TEXT
NAME                    READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/hello   3/3     3            3           20s

NAME                               DESIRED   CURRENT   READY   AGE
replicaset.apps/hello-6d5f9c7b8d   3         3         3       20s

NAME                         READY   STATUS    RESTARTS   AGE
pod/hello-6d5f9c7b8d-4kqzt   1/1     Running   0          20s
pod/hello-6d5f9c7b8d-b7m2x   1/1     Running   0          20s
pod/hello-6d5f9c7b8d-tx9wl   1/1     Running   0          20s

Look at the names: the ReplicaSet is hello plus a code, and each pod is the ReplicaSet's name plus another code. You can tell which version every pod belongs to just from its name. Your codes will be different.

Scale it

Change replicas: 3 to replicas: 5 in deploy.yaml and apply again:

BASH
kubectl apply -f deploy.yaml
kubectl get pods

Two new pods appear. Change it back to 3 and apply, and two are removed. There's also a shortcut, kubectl scale deployment hello --replicas=5, but it changes the cluster without changing your file, so the two drift apart. Editing the file and applying it keeps your file as the record of what's running, which matters a lot once your files live in Git.

Ship a new version

Change the image in deploy.yaml from nginx:1.27 to nginx:1.28, apply, and watch:

BASH
kubectl apply -f deploy.yaml
kubectl rollout status deployment/hello
TEXT
Waiting for deployment "hello" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "hello" rollout to finish: 2 out of 3 new replicas have been updated...
deployment "hello" successfully rolled out

That's a rolling update. The Deployment created a new ReplicaSet for nginx:1.28 and moved pods over gradually, starting new ones before removing old ones, so there were always pods serving. Now look at the ReplicaSets:

BASH
kubectl get replicasets
TEXT
NAME               DESIRED   CURRENT   READY   AGE
hello-6d5f9c7b8d   0         0         0       5m
hello-7b9c4f6d5c   3         3         3       40s

The old one is still there, scaled to 0. Keep that in mind for the next section.

How gradual is "gradual"? Two settings control it. maxSurge is how many extra pods can exist above your count during the update, and maxUnavailable is how many of your count can be missing. Both default to 25%, rounded so that at least one pod can move at a time. With 3 replicas, that works out to "start 1 new pod, then remove 1 old one", over and over.

Break it on purpose

Now ship a version that doesn't exist. Change the image to nginx:9.99 and apply:

BASH
kubectl apply -f deploy.yaml
kubectl get pods
TEXT
NAME                     READY   STATUS             RESTARTS   AGE
hello-5c8f7d9b6a-q2wrt   0/1     ImagePullBackOff   0          30s
hello-7b9c4f6d5c-8hn4v   1/1     Running            0          6m
hello-7b9c4f6d5c-kd7sp   1/1     Running            0          6m
hello-7b9c4f6d5c-x5vzm   1/1     Running            0          6m

One new pod is stuck in ImagePullBackOff, the failure you met in Lesson 3. But look at the rest: all three old pods are still running. The rolling update only removes an old pod after a new one is ready, and the new one never becomes ready, so the update stalls and your app keeps working on the old version. A bad release doesn't take the site down. It just never finishes.

Roll it back:

BASH
kubectl rollout undo deployment/hello
kubectl get pods

The broken pod disappears, and you're back on nginx:1.28. The rollback was instant because the previous ReplicaSet was still there, scaled to 0, ready to grow again. You can see every version the Deployment remembers with:

BASH
kubectl rollout history deployment/hello

One catch: rollout undo changed the cluster, but your deploy.yaml still says nginx:9.99. Fix the file too, or the next apply will ship the broken version again. In a team that keeps its files in Git, the usual way to roll back is to revert the change in Git and let it apply, so the file and the cluster always agree.

In my real cluster

CivicGrid's API runs as a Deployment, and shipping a new version is a one-line change in Git: I update the image tag in my GitOps (Git-driven deployment) repository, and Argo CD applies it, which starts exactly this kind of rolling update. Because a new pod starts before the old one stops, every update briefly needs room for one extra pod. On my single, fairly full node, that headroom is one of the reasons I freed up memory by turning Grafana off. If there were no room, the new pod would wait in Pending, and the old one would keep serving until it fit.

Other ways to run pods

A Deployment fits most apps, but not all. Kubernetes has a few other workload types, each managing pods in a different way:

TypeUse it forReal example
DeploymentApps where any copy is as good as another, and copies come and go freelyA web app or API
StatefulSetApps where each copy needs a stable name and its own storage that survives restartsDatabases, Prometheus
DaemonSetExactly one copy on every node, including new nodes as they joinLog collectors, monitoring agents, network plugins
JobA task that runs until it finishes, then stopsA data import or a database migration
CronJobA Job on a scheduleA nightly backup or a weekly report

The question to ask is: does it matter which copy is which? If not, use a Deployment. If each copy has its own identity or its own data, use a StatefulSet. If it's about the nodes rather than the app, use a DaemonSet. If it should finish, use a Job, or a CronJob if it should repeat.

See them in your own cluster:

BASH
kubectl get deployments,statefulsets,daemonsets,jobs,cronjobs -A

Even an empty kind cluster has some: coredns runs as a Deployment, and kube-proxy and kindnet run as DaemonSets, one pod per node, because every node needs its own networking.

In my real cluster

My production cluster uses most of these. The CivicGrid API is a Deployment. Prometheus and Alertmanager run as StatefulSets, because each keeps its own data. The Cilium network plugin runs as a DaemonSet, one per node. And CivicGrid's scheduled tasks run as CronJobs.

Clean up

BASH
kind delete cluster --name learn

Check yourself

  1. What does a Deployment create to keep the right number of pods running?

  2. You change a Deployment's image and apply. What happens to the running pods?

  3. A new release has a typo in the image name. What happens to your app?

  4. Which command takes a Deployment back to its previous version?

  5. You need one copy of a log collector on every node. Which workload type fits?

  6. Why is editing replicas in your file and applying it better than kubectl scale?

Key terms

  • Deployment: keeps a set number of identical pods running, and updates them gradually.
  • ReplicaSet: the layer a Deployment uses to keep an exact number of pods for one version.
  • Rolling update: replacing pods with a new version a few at a time, so the app keeps running.
  • Rollback: returning to a previous version, with kubectl rollout undo.
  • StatefulSet, DaemonSet, Job, CronJob: the other workload types, for stateful apps, per-node agents, finite tasks and scheduled tasks.

Go deeper