ReplicaSets and Deployments
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
A bare Pod (Pods) has no one to replace it if it dies, and no one to create more
copies of it under load. ReplicaSets add exactly that: a controller that keeps a fixed number of Pods matching a
label selector running at all times. Deployments then add a second controller on top of ReplicaSets, so that
changing a Pod template produces a controlled, resumable, rollback-able rollout instead of an in-place edit. This
page describes both controllers as they stand in Kubernetes 1.37, and the history that got there — Book 1’s
ReplicationController and Book 2’s extensions/v1beta1 Deployment are both obsolete, and every live example below
targets apps/v1.
From ReplicationController to ReplicaSet to Deployment
Rensin’s Kubernetes: Scheduling the Future at Cloud Scale (2015), ch. 3, "Organize, Grow, and Go", describes the
only self-healing controller Kubernetes had at the time: the ReplicationController (kind: ReplicationController,
core v1 API, field spec.replicas with a spec.template and a plain equality-based spec.selector). It already
had the two ideas that survive unchanged today — a desired replica count reconciled continuously against reality,
and loose coupling: the controller does not track "its" Pods by name or by having created them, it finds them by
matching a label selector against whatever Pods currently exist in the namespace. Deleting a ReplicationController
without deleting its Pods, and then later creating a fresh one with the same selector, was already the book’s
worked example of that same loose coupling.
ReplicaSet (apps/v1beta2 when Kubernetes: Up and Running’s 2017 sampler was written, later graduating to
apps/v1) is the ReplicationController’s direct successor: an identical replicas / selector / template shape,
but with a selector that also accepts the set-based label expressions covered in
Objects and the Kubernetes API (matchLabels and
matchExpressions), where a ReplicationController’s selector could only be a flat equality map. The
extensions/v1beta1 API group the 2017 sampler shows Deployments and DaemonSets served under (and that the
ReplicaSet examples of that era assumed alongside them) was removed from the API server entirely in Kubernetes 1.16 — any manifest still written against extensions/v1beta1 or apps/v1beta1/apps/v1beta2 must be rewritten
against apps/v1 before it will apply to a 1.37 cluster, exactly as the deprecation guide in
Objects and the Kubernetes API describes. ReplicationController
itself was never removed — it is still served under the core v1 group — but the official documentation has
recommended a Deployment-managed ReplicaSet over a bare ReplicationController for years, and no Bookshelf manifest
in this section uses kind: ReplicationController.
Neither a ReplicationController nor a bare ReplicaSet, on its own, knows how to change its Pod template safely: edit
spec.template.spec.containers[0].image on a running ReplicaSet and every existing Pod is left untouched, because
the selector still matches them and the controller only creates new Pods to reach the replica count, it never
touches existing ones. That gap is what a Deployment (apps/v1, since Kubernetes 1.9) fills: a Deployment owns
one or more ReplicaSets (see the owner-reference chain in
Objects and the Kubernetes API) and, whenever its own Pod template
changes, creates a new ReplicaSet with the new template and drives traffic from the old ReplicaSet to the new one
according to a strategy, covered below. In practice almost no Bookshelf manifest creates a ReplicaSet directly;
it is written as a Deployment, and the Deployment controller manages the ReplicaSet underneath it.
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: catalog-api-manual-example
labels:
app.kubernetes.io/name: catalog-api
spec:
replicas: 3
selector:
matchLabels:
app.kubernetes.io/name: catalog-api
template:
metadata:
labels:
app.kubernetes.io/name: catalog-api
spec:
containers:
- name: catalog-api
image: ghcr.io/example/bookshelf-catalog-api:1.0.0
ports:
- containerPort: 8080
Source: Kubernetes Docs — ReplicaSet.
Selectors and templates, and why the coupling stays loose
A ReplicaSet’s spec.selector and spec.template.metadata.labels are two independent fields that must simply agree
often enough to work: the selector decides which Pods the ReplicaSet counts and manages, and the template decides
what label set a new Pod is created with. Nothing stops another controller, or a person with kubectl create pod,
from creating a Pod with matching labels by hand — that Pod is immediately "acquired" by the ReplicaSet and counted
towards replicas, with no record that a human, not the ReplicaSet, created it. This is the same loose coupling
Rensin’s ch. 3 built the whole design around: the controller and its Pods never reference each other by name, only
by label, so any of several ReplicaSets can pick up a Pod that happens to match, and a ReplicaSet survives being
deleted independently of the Pods it created (kubectl delete replicaset --cascade=orphan, see
Objects and the Kubernetes API).
That same loose coupling is what makes relabelling a Pod out of a ReplicaSet work as a debugging technique. Give
one running Pod a label that no longer matches its ReplicaSet’s selector, and two things happen at once: the
ReplicaSet controller notices it is one Pod short and immediately creates a fresh replacement to restore
replicas, while the relabelled Pod itself keeps running completely untouched, now invisible to the controller and
free to be poked at — kubectl exec, kubectl logs -f, a port-forward — without the controller fighting to
reconcile it away or a user-facing Service still routing live traffic to it if the same label also drove that
Service’s own selector.
kubectl get pods -l app.kubernetes.io/name=catalog-api
kubectl label pod catalog-api-7d4b6c9f9c-x2p7q app.kubernetes.io/name- # remove the matching label
kubectl label pod catalog-api-7d4b6c9f9c-x2p7q debug=true # optional: tag it for easy lookup
kubectl get pods -l app.kubernetes.io/name=catalog-api # one fewer -- a new Pod already replaced it
kubectl get pods -l debug=true # the isolated Pod, still running
NAME READY STATUS RESTARTS AGE
catalog-api-7d4b6c9f9c-k9m2r 1/1 Running 0 14m
catalog-api-7d4b6c9f9c-q4t8w 1/1 Running 0 14m
catalog-api-7d4b6c9f9c-z8n3p 1/1 Running 0 8s
Source: Kubernetes Docs — ReplicaSet.
The Deployment spec
A Deployment adds one field on top of everything a ReplicaSet already has: spec.strategy, which controls how the
old ReplicaSet is scaled down and the new one scaled up whenever spec.template changes.
A rolling update is what delivers zero-downtime releases: with maxUnavailable: 0 and a readiness probe, capacity
never drops below the desired replica count during a rollout. Combined with self-healing replacement of failed Pods,
this is the mechanism behind "five nines" (99.999%) availability targets, which allow only about five minutes of
downtime a year; a Deployment alone is necessary but not sufficient, since probes, PodDisruptionBudgets and
multiple nodes all contribute.
RollingUpdate vs Recreate
strategy.type |
Behaviour |
|---|---|
|
New Pods are created and old Pods are terminated incrementally, so the application stays available throughout
the rollout provided enough replicas stay ready. Controlled by |
|
Every existing Pod is terminated before any new one is created. There is a guaranteed gap with zero running Pods, but also a guarantee that old and new Pod versions never run side by side — the right choice when two versions cannot safely share a database schema, a cache format, or an exclusive lock, and downtime during the switch is acceptable. |
Under RollingUpdate, maxSurge and maxUnavailable (both percentages or absolute numbers, defaulting to 25%
each) bound how far the rollout is allowed to move away from the desired replica count in either direction: at any
point during the rollout the total replica count stays between replicas - maxUnavailable and
replicas + maxSurge. A larger maxSurge finishes the rollout faster at the cost of temporarily using more
cluster resources; a larger maxUnavailable finishes faster at the cost of temporarily serving fewer replicas.
Setting maxUnavailable: 0 guarantees full capacity is never lost, at the cost of requiring at least one extra
surge Pod’s worth of headroom (maxSurge cannot also be 0).
apiVersion: apps/v1
kind: Deployment
metadata:
name: catalog-api
namespace: bookshelf
labels:
app.kubernetes.io/name: catalog-api
annotations:
kubernetes.io/change-cause: "roll out catalog-api 1.1.0"
spec:
replicas: 6
minReadySeconds: 10
progressDeadlineSeconds: 300
revisionHistoryLimit: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 2
maxUnavailable: 0
selector:
matchLabels:
app.kubernetes.io/name: catalog-api
template:
metadata:
labels:
app.kubernetes.io/name: catalog-api
app.kubernetes.io/version: "1.1.0"
spec:
containers:
- name: catalog-api
image: ghcr.io/example/bookshelf-catalog-api:1.1.0
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /healthz/ready
port: 8080
Source: Kubernetes Docs — Deployment.
minReadySeconds, progressDeadlineSeconds and revisionHistoryLimit
| Field | Meaning |
|---|---|
|
How long a newly created Pod must stay |
|
How long the Deployment controller waits, after the last time it observed forward progress, before reporting
|
|
How many old, scaled-to-zero ReplicaSets a Deployment keeps around after a successful rollout, purely so
|
Source: Kubernetes Docs — Deployment.
Proportional scaling
Scaling a Deployment (or editing its Pod template) while a previous rollout is still in progress does not wait for
that rollout to finish first: the Deployment controller splits the change in desired replica count proportionally
across every ReplicaSet it currently owns, old and new alike, weighted by each ReplicaSet’s own current size, rather
than dumping the entire change onto whichever ReplicaSet happens to be newest. A Deployment mid-rollout at 7 old / 3
new replicas that is then scaled from 10 to 15 does not simply add 5 replicas to the new ReplicaSet — it adds
roughly 3.5 to the old one and 1.5 to the new one, still bounded by maxSurge / maxUnavailable, so capacity keeps
growing smoothly through an in-flight update instead of the scale request and the rollout fighting each other.
A rolling update step by step
The diagram below follows the Deployment YAML above (replicas: 6, maxSurge: 2, maxUnavailable: 0) through one
RollingUpdate from the catalog-api ReplicaSet running image 1.0.0 to a new one running 1.1.0: the new
ReplicaSet is scaled up in maxSurge-sized steps and the old one is scaled down only as each new Pod becomes
available, never dropping below the desired replica count because maxUnavailable is 0.
kubectl rollout status (below) is exactly what waits on this sequence: it reports "Progressing" while the two
ReplicaSets are still shifting and returns once the new ReplicaSet’s Pods are all ready and the old one is fully
scaled down. Source: Kubernetes Docs — Deployment.
Driving rollouts with kubectl rollout
kubectl rollout is the command group for everything about a Deployment’s (or DaemonSet’s, or StatefulSet’s)
history and in-flight state, alongside kubectl scale for the replica count and kubectl set image for the most
common single-field template edit.
kubectl set image deployment/catalog-api catalog-api=ghcr.io/example/bookshelf-catalog-api:1.1.0
kubectl annotate deployment/catalog-api kubernetes.io/change-cause="roll out catalog-api 1.1.0"
kubectl rollout status deployment/catalog-api # blocks until the rollout completes or times out
kubectl rollout history deployment/catalog-api # one line per retained revision, from CHANGE-CAUSE
kubectl rollout history deployment/catalog-api --revision=4
kubectl rollout pause deployment/catalog-api # stop the controller mid-rollout, e.g. to batch edits
kubectl rollout resume deployment/catalog-api
kubectl rollout undo deployment/catalog-api # back to the previous revision
kubectl rollout undo deployment/catalog-api --to-revision=3 # back to a specific retained revision
kubectl rollout restart deployment/catalog-api # re-create every Pod with an unchanged template
kubectl scale deployment/catalog-api --replicas=8
deployment "catalog-api" successfully rolled out
REVISION CHANGE-CAUSE
1 <none>
2 roll out catalog-api 1.0.0
3 fix readiness probe path
4 roll out catalog-api 1.1.0
kubectl set image triggers a rollout the same way editing spec.template by hand would. kubectl scale does
not: spec.replicas is not part of the Pod template, so scaling resizes the current ReplicaSet without creating a
new revision. The --record flag that used to fill in CHANGE-CAUSE is deprecated, so the supported way to leave a
message for kubectl rollout history is the kubernetes.io/change-cause annotation, set with kubectl annotate
right after the change (as above) or written straight into the manifest. The Deployment controller copies the
annotation onto the newest ReplicaSet, so annotating right after set image labels the revision that command
created; a later annotation overwrites the message of whichever revision is current at that time. kubectl rollout restart is the
tool for "the image tag did not change but the Pods must still be replaced" — picking up a ConfigMap/Secret
edit that has no checksum annotation wired in (see
ConfigMaps), or simply forcing a fresh set of Pods — without editing the
Pod template at all: it patches a kubectl.kubernetes.io/restartedAt annotation on the template instead, which
still goes through the same RollingUpdate/Recreate strategy as any other change. kubectl rollout pause and
kubectl rollout resume are for batching several template edits (e.g. set image plus an env var change) into one
rollout instead of triggering a rollout per edit: pause, make every edit, then resume to let one rollout apply them
all together. Source: Kubernetes Docs — kubectl rollout and Kubernetes Docs — Deployment.
kubectl rolling-update: removed in 1.18
Before Deployments existed, kubectl rolling-update performed a rolling update of a ReplicationController
entirely client-side: the command itself repeatedly created new Pods, deleted old ones and polled their status,
with no controller in the API server driving the process and nothing to resume it if the client was interrupted
midway. It never applied to Deployments or ReplicaSets, which is why Book 1 ch. 3’s rolling-update examples are
tied specifically to a ReplicationController. The command was deprecated once Deployments shipped and formally
removed from kubectl in Kubernetes 1.18; on a 1.37 cluster, every rolling update — of a Deployment, and
indirectly of the ReplicaSets it owns — goes through the server-side Deployment controller and kubectl rollout
above, never through a client-driven loop.
References
-
Kubernetes Docs — Run a Stateless Application Using a Deployment
-
Kubernetes Docs — Kubernetes Basics: Updating Your Application
-
Rensin, D. (2015). Kubernetes: Scheduling the Future at Cloud Scale. O’Reilly. Ch. 3, "Organize, Grow, and Go".
-
Hightower, K., Burns, B., Beda, J. (2017). Kubernetes: Up and Running (1st ed. sampler). O’Reilly. Ch. 2 (book ch. 2), "Creating and Running Containers".