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

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

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

RollingUpdate (default)

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 maxSurge and maxUnavailable, below.

Recreate

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

minReadySeconds, progressDeadlineSeconds and revisionHistoryLimit

Field Meaning

minReadySeconds

How long a newly created Pod must stay Ready (readiness probe passing, see Health Probes and Container Lifecycle) before it counts as available and the rollout is allowed to continue replacing the next batch. Defaults to 0, meaning a Pod is considered available the instant it becomes ready — too eager to catch a container that starts cleanly, passes its probe once, and crashes seconds later.

progressDeadlineSeconds

How long the Deployment controller waits, after the last time it observed forward progress, before reporting Progressing=False (a stuck rollout) in status.conditions. Defaults to 600. It must be strictly greater than minReadySeconds; it never rolls anything back by itself, it only surfaces the condition that kubectl rollout status (below) waits on and that alerting can watch for.

revisionHistoryLimit

How many old, scaled-to-zero ReplicaSets a Deployment keeps around after a successful rollout, purely so kubectl rollout undo --to-revision (below) has something to roll back to. Defaults to 10. Setting it to 0 still lets the current rollout create a new ReplicaSet before the previous one is cleaned up — it only removes the ability to undo past this rollout, it does not skip creating a ReplicaSet for the next one.

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.

sequenceDiagram participant D as Deployment/catalog-api participant RSold as ReplicaSet 1.0.0 (old) participant RSnew as ReplicaSet 1.1.0 (new) Note over D,RSnew: Start: RSold=6, RSnew=0 D->>RSnew: create (scale 0 -> 2) Note over RSnew: 2 new Pods created, not yet ready RSnew-->>D: 2 Pods become Ready (minReadySeconds elapsed) D->>RSold: scale down 6 -> 4 Note over D,RSnew: RSold=4, RSnew=2, total=6 D->>RSnew: scale up 2 -> 4 RSnew-->>D: 2 more Pods become Ready D->>RSold: scale down 4 -> 2 Note over D,RSnew: RSold=2, RSnew=4, total=6 D->>RSnew: scale up 4 -> 6 RSnew-->>D: final Pods become Ready D->>RSold: scale down 2 -> 0 Note over D,RSnew: Done: RSold=0, RSnew=6

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.