Deployment: rollout and rollback
Throughout Part III we created bare Pods by hand to dissect their lifecycle. But no one runs production like that: a bare Pod doesn't resurrect itself when a node dies, doesn't update versions cleanly, doesn't scale. That work is handed to a controller, and the most common one is the Deployment. Part IV opens here because the Deployment is what we touch every day, but the thing worth learning isn't the YAML syntax — it's the mechanism behind it: a Deployment doesn't manage Pods directly, it manages them through an intermediate layer called a ReplicaSet, and it's precisely this middle layer that makes rollout/rollback work.
Deployment → ReplicaSet → Pod
The docs describe the Deployment's role in one declarative sentence: "A Deployment provides declarative updates for Pods and ReplicaSets. You describe a desired state ... and the Deployment Controller changes the actual state to the desired state at a controlled rate." The word "ReplicaSets" there is no accident: it's the layer the Deployment uses to keep the right number of replicas. Create a 3-replica Deployment and look at what sprouts from it:
apiVersion: apps/v1
kind: Deployment
metadata: {name: rollout-demo}
spec:
replicas: 3
selector: {matchLabels: {app: rollout-demo}}
template:
metadata: {labels: {app: rollout-demo}}
spec:
containers:
- name: app
image: busybox:1.36
command: ["sleep","3600"]
kubectl get deploy rollout-demo
kubectl get rs,pods -l app=rollout-demo
NAME READY UP-TO-DATE AVAILABLE AGE
rollout-demo 3/3 3 3 8s
NAME DESIRED CURRENT READY AGE
replicaset.apps/rollout-demo-7545d5669f 3 3 3 8s
NAME READY STATUS RESTARTS AGE
pod/rollout-demo-7545d5669f-8nh2r 1/1 Running 0 8s
pod/rollout-demo-7545d5669f-s6pps 1/1 Running 0 8s
pod/rollout-demo-7545d5669f-sv2fv 1/1 Running 0 8s
We declared one Deployment, but there are three layers of objects: the Deployment rollout-demo, one ReplicaSet rollout-demo-7545d5669f, and three Pods whose names start with exactly that ReplicaSet name. The suffix 7545d5669f isn't random — it's the pod-template-hash, a hash of the pod template. Follow the ownerReferences (the ownership/GC mechanism from Article 17) to see the chain:
POD=$(kubectl get pods -l app=rollout-demo -o jsonpath='{.items[0].metadata.name}')
kubectl get pod $POD -o jsonpath='pod={.metadata.name} ownerRS={.metadata.ownerReferences[0].name}{"\n"}'
kubectl get rs rollout-demo-7545d5669f -o jsonpath='rs={.metadata.name} ownerDeploy={.metadata.ownerReferences[0].name} hash={.metadata.labels.pod-template-hash}{"\n"}'
pod=rollout-demo-7545d5669f-8nh2r ownerRS=rollout-demo-7545d5669f
rs=rollout-demo-7545d5669f ownerDeploy=rollout-demo hash=7545d5669f
The Pod belongs to the ReplicaSet, the ReplicaSet belongs to the Deployment. The roles are cleanly split: the ReplicaSet does exactly one thing — keep the right number of Pods for one version of the template (it's the very thing that recreated the replacement Pod in Article 23). The Deployment handles the higher-level job: orchestrating multiple ReplicaSets as the template changes. Once you understand this division of roles, rollout/rollback becomes obvious.
Changing the template spawns a new ReplicaSet
The key question: when does a Deployment do what? The docs answer precisely:
"A Deployment's rollout is triggered if and only if the Deployment's Pod template (that is,
.spec.template) is changed, for example if the labels or container images of the template are updated. Other updates, such as scaling the Deployment, do not trigger a rollout."
"if and only if" — only when .spec.template changes. Changing the image is one such case. And how the Deployment handles it, again per the docs: "A new ReplicaSet is created, and the Deployment gradually scales it up while scaling down the old ReplicaSet, ensuring Pods are replaced at a controlled rate." Change the image and watch:
kubectl set image deployment/rollout-demo app=busybox:1.37
kubectl rollout status deployment/rollout-demo
deployment.apps/rollout-demo image updated
Waiting for deployment "rollout-demo" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "rollout-demo" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "rollout-demo" rollout to finish: 1 old replicas are pending termination...
deployment "rollout-demo" successfully rolled out
rollout status shows that very "controlled rate": the new version comes up gradually 1 → 2 → 3, the old one steps down. This pace is governed by two fields in .spec.strategy.rollingUpdate, both defaulting to 25%: maxUnavailable (the maximum number of replicas allowed to be missing during the update) and maxSurge (the maximum number of extra replicas, above the desired count, that may be created). With 3 replicas, 25% rounds up to 1 — so during the rollout there are moments running up to 4 pods (3 + 1 surge) and it never drops below 2 available. (RollingUpdate is the default strategy; Recreate instead kills all the old pods and only then stands up the new ones — accepting downtime.) Look at the two ReplicaSets once it's done:
kubectl get rs -l app=rollout-demo -o 'custom-columns=NAME:.metadata.name,DESIRED:.spec.replicas,READY:.status.readyReplicas,IMAGE:.spec.template.spec.containers[0].image'
NAME DESIRED READY IMAGE
rollout-demo-6bbbc96696 3 3 busybox:1.37
rollout-demo-7545d5669f 0 <none> busybox:1.36
Now there are two ReplicaSets: the new one 6bbbc96696 (busybox:1.37) holding 3 Pods, and the old one 7545d5669f (busybox:1.36) scaled down to 0 — but not deleted. This is the crux of rollback: the old ReplicaSet is kept at 0 replicas as a "snapshot" of the previous version, ready to be scaled back up. kubectl rollout history shows those revisions:
kubectl rollout history deployment/rollout-demo
REVISION CHANGE-CAUSE
1 <none>
2 <none>
Each revision corresponds to one ReplicaSet. How many old ReplicaSets are kept is governed by .spec.revisionHistoryLimit, defaulting to 10 — beyond that, the oldest ReplicaSet is cleaned up.
Rollback in one command
Say busybox:1.37 is broken and you need to revert urgently. Since the old ReplicaSet is still intact at 0, rollback is just scaling it back up — kubectl rollout undo does exactly that:
kubectl rollout undo deployment/rollout-demo
kubectl rollout status deployment/rollout-demo
kubectl get deployment rollout-demo -o jsonpath='image={.spec.template.spec.containers[0].image}{"\n"}'
kubectl get rs -l app=rollout-demo -o 'custom-columns=NAME:.metadata.name,DESIRED:.spec.replicas,IMAGE:.spec.template.spec.containers[0].image'
deployment "rollout-demo" successfully rolled out
image=busybox:1.36
NAME DESIRED IMAGE
rollout-demo-6bbbc96696 0 busybox:1.37
rollout-demo-7545d5669f 3 busybox:1.36
The Deployment's image goes back to busybox:1.36, and note: it does not create a third ReplicaSet — it reuses the very old RS 7545d5669f, scaling it from 0 up to 3 and the 1.37 one back down to 0. A rollback is just a rolling update in reverse, reusing an existing snapshot. That's why it's fast and reliable: no rebuild, no new image pull. (rollout undo --to-revision=N lets you revert to a specific revision.)
A practical warning shows up when you run this on a Deployment that was created with kubectl apply:
Warning: resource deployments/rollout-demo was previously managed with 'kubectl apply'.
Rolling back will not update the kubectl.kubernetes.io/last-applied-configuration annotation ...
In other words, rollout undo changes the cluster but does not fix your YAML file / the last-applied annotation, so the next kubectl apply (from the old file containing 1.37) will inadvertently "roll forward" again. The GitOps lesson: a rollback should come with a fix to the source of configuration — don't let the cluster and Git drift apart.
Scaling is not a rollout
The docs say scaling does not trigger a rollout — let's verify. Change the replica count without touching the template:
kubectl scale deployment/rollout-demo --replicas=4
kubectl get rs -l app=rollout-demo -o 'custom-columns=NAME:.metadata.name,DESIRED:.spec.replicas,IMAGE:.spec.template.spec.containers[0].image'
kubectl rollout history deployment/rollout-demo
NAME DESIRED IMAGE
rollout-demo-6bbbc96696 0 busybox:1.37
rollout-demo-7545d5669f 4 busybox:1.36
REVISION CHANGE-CAUSE
2 <none>
3 <none>
The active ReplicaSet 7545d5669f simply goes up to 4 — no new ReplicaSet, no new revision (history is still 2 and 3). Exactly as the docs say: scaling adjusts .spec.replicas, not .spec.template, so the Deployment treats it as the same version with just a different count. This is why you can scale freely without "burning" rollout history.
🧹 Cleanup
kubectl delete deployment rollout-demo
Deleting the Deployment takes the ReplicaSets (including the one at 0) and all Pods with it through the ownerReferences chain — this is the garbage collection from Article 17 at work. The cluster returns to two CoreDNS pods. Manifests at github.com/nghiadaulau/kubernetes-from-scratch, directory 24-deployment.
Wrap-up
A Deployment doesn't manage Pods directly but through a ReplicaSet: each version of the pod template maps to one ReplicaSet (tagged by pod-template-hash), the ReplicaSet keeps the right number of Pods, and the Deployment orchestrates multiple ReplicaSets. A rollout is triggered only when .spec.template changes (new image, label...) — at which point the Deployment creates a new ReplicaSet and a rolling update scales it up / scales the old one down per maxSurge/maxUnavailable (default 25%), keeping the old ReplicaSet at 0 as a snapshot. Rollback (kubectl rollout undo) reuses that old ReplicaSet, scaling it back up — fast because there's no rebuild/pull (remember to re-sync the configuration source or a later apply will roll back the rollback). Scaling only changes .spec.replicas, doesn't touch the template, so it creates no revision. The number of revisions kept is governed by revisionHistoryLimit (default 10).
Article 25 moves to the StatefulSet — the controller for stateful applications: pods with stable identity (fixed names, a defined startup/shutdown order) where each pod keeps its own volume, quite different from the anonymous "school of fish" Pods of a Deployment that replace one another.