Skip to main content

Kubernetes Deployments: Rolling Updates, Rollbacks and Scaling

Run a Kubernetes Deployment: scale it, roll out a new version, watch a broken rollout stop safely, roll back, and run Jobs, CronJobs and DaemonSets.

TLDR: A Deployment keeps a number of identical pods running and replaces them gradually when you change the pod template. It does this through ReplicaSets: one per version of the template. kubectl rollout status, history and undo follow and reverse a rollout, and maxSurge, maxUnavailable and readiness probes decide how safe it is. For work that finishes, use a Job or CronJob; for one pod per node, a DaemonSet.

In this part you run three copies of podinfo, delete one to watch it come back, scale it, update it to a new version, push a broken version and watch Kubernetes refuse to take the app down, and roll back. Then you run a Job with a native sidecar and a CronJob.

Prerequisites

  • The kind cluster from Part 2 with the demo namespace selected.
  • The pods from Part 3 deleted (the last command of that part).

Deployments, ReplicaSets and Pods

You create the Deployment. The Deployment creates a ReplicaSet for the current pod template, and the ReplicaSet creates the pods. When you change the template, for example the image tag, the Deployment creates a second ReplicaSet and moves pods from the old one to the new one step by step.

You almost never touch ReplicaSets yourself. They matter because they are what kubectl rollout undo goes back to, and because they appear in pod names: podinfo-77cfdf847c-9t6dz is a pod of ReplicaSet podinfo-77cfdf847c, where 77cfdf847c is a hash of the pod template.

Create the Deployment

The Deployment runs three replicas of podinfo 6.14.1, the previous release, so that you can update it in a moment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: podinfo
  labels:
    app: podinfo
spec:
  replicas: 3
  # Keep five old ReplicaSets for rollbacks
  revisionHistoryLimit: 5
  # Mark the rollout as failed if it makes no progress for 2 minutes
  progressDeadlineSeconds: 120
  # A new pod must stay ready for 5 seconds before it counts as available
  minReadySeconds: 5
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1 # at most one pod above the desired count
      maxUnavailable: 0 # never go below the desired count of available pods
  selector:
    matchLabels:
      app: podinfo
  template:
    metadata:
      labels:
        app: podinfo
    spec:
      containers:
        - name: podinfo
          image: ghcr.io/stefanprodan/podinfo:6.14.1
          ports:
            - name: http
              containerPort: 9898
          readinessProbe:
            httpGet:
              path: /readyz
              port: http
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /healthz
              port: http
            periodSeconds: 10
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              memory: 128Mi

The important fields:

  • selector.matchLabels must match the labels in template.metadata.labels. This is how the Deployment finds its pods, and you cannot change it later.
  • strategy.rollingUpdate with maxSurge: 1 and maxUnavailable: 0 means: during an update, the controller creates at most one pod above the desired count and removes an old pod only when a new one is available. A new version that never becomes ready therefore cannot reduce the number of available pods. Old pods that are shutting down still count against node resources for their grace period, and a version that passes its readiness probe and fails later is not caught; that is what monitoring and rollbacks are for.
  • minReadySeconds: 5 makes a new pod count as available only after it has been ready for 5 seconds, which catches pods that crash right after starting.
  • progressDeadlineSeconds: 120 marks a rollout as failed if it makes no progress for two minutes.
  • resources reserves CPU and memory for each pod and caps its memory (Part 8 explains these numbers).

In kubectl get deployment, READY is ready pods out of desired, UP-TO-DATE counts pods that run the current template, and AVAILABLE counts pods that have been ready for minReadySeconds. The NODE column shows where the scheduler put each pod. It tends to spread copies of an app over nodes but does not promise to; Part 8 shows how to ask for it.

Self-Healing and Scaling

Delete one pod and the ReplicaSet notices that it has two of three, and creates a replacement:

The new pod has a new name and a younger AGE. This is also why a Deployment's pods are not the place for anything you want to keep.

Scaling changes the replica count of the current ReplicaSet:

kubectl scale is handy, but the YAML file still says replicas: 3, and the next kubectl apply of the file sets it back. For a lasting change, edit the file. When an autoscaler manages the count (Part 8), remove replicas from the file so the two do not fight.

Roll Out a New Version

Update the image to 6.15.0, then record why in the kubernetes.io/change-cause annotation, which kubectl rollout history shows. (The old --record flag that set it automatically is deprecated.) Then follow the rollout:

The new ReplicaSet grew one pod at a time (maxSurge: 1), and the old one shrank only after each new pod had stayed ready for 5 seconds and become available. The old ReplicaSet is still there with 0 replicas; revisionHistoryLimit: 5 keeps five of them for rollbacks.

A Broken Rollout

Now push a version that does not exist, as a typo in a tag would:

This is the rolling update doing its job. The one new pod cannot pull its image, so it never becomes ready, and because maxUnavailable is 0 the Deployment does not remove a single old pod: all five replicas of 6.15.0 still serve traffic. Without a readiness probe, or with a larger maxUnavailable, a broken version can replace working pods.

After progressDeadlineSeconds without progress, the Deployment marks the rollout as failed. It does not roll back by itself, but the failure is visible to kubectl rollout status, to your CI pipeline and to monitoring:

Roll Back

kubectl rollout undo switches the Deployment back to the previous ReplicaSet's template:

Revision 2 became revision 4: a rollback reuses the old ReplicaSet and gives it a new revision number. To go back further, use --to-revision=<n>, and kubectl rollout history deployment/podinfo --revision=4 shows the full template of one revision. Set the replica count back to the file's value:

kubectl scale deployment podinfo --replicas=3
deployment.apps/podinfo scaled

Read the warning that rollout undo printed. The cluster now runs 6.15.0, but 04-deployments/deployment.yaml still says 6.14.1, so the next kubectl apply of that file would quietly downgrade the app. kubectl diff shows the drift before you apply:

kubectl diff -f 04-deployments/deployment.yaml | grep '^[-+] '
-  generation: 9
+  generation: 10
-      - image: ghcr.io/stefanprodan/podinfo:6.15.0
+      - image: ghcr.io/stefanprodan/podinfo:6.14.1

That is why teams treat set image, scale and rollout undo as emergency tools and make the lasting change in Git. The later parts of this guide use their own manifests with 6.15.0.

Restart Pods Without a Change

kubectl rollout restart replaces all pods with the same rolling strategy, for example to pick up a changed ConfigMap (Part 6) or a rotated credential:

It works by adding a timestamp annotation to the pod template, which counts as a change.

Update Strategies

  • RollingUpdate (the default) replaces pods gradually, within the limits of maxSurge and maxUnavailable. Both accept a number or a percentage of replicas.
  • Recreate deletes all old pods before it creates new ones. There is downtime, but two versions never run at the same time, which some apps with schema changes or exclusive locks need.

Blue-green and canary releases are built from the same pieces: two Deployments with different labels, and a Service or route that decides which one gets traffic. Part 5 sends 10 percent of requests to a canary with Gateway API. Tools such as Argo Rollouts and Flagger automate those steps and roll back on bad metrics. The deployment strategies guide and the deployment strategies simulator compare the options.

Other Workload Controllers

Deployments are for stateless, long-running apps. The other controllers follow the same pattern for other kinds of work:

Controller Use it for
StatefulSet Apps that need a stable name and their own volume per pod, such as databases (Part 7)
DaemonSet One pod per node: log collectors, monitoring agents, network plugins
Job Work that runs to completion, with retries
CronJob A Job on a schedule

The cluster already runs two DaemonSets, the network plugin and kube-proxy:

kubectl get daemonsets -n kube-system
NAME         DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
kindnet      3         3         3       3            3           kubernetes.io/os=linux   4m47s
kube-proxy   3         3         3       3            3           kubernetes.io/os=linux   4m48s

A Job with a Native Sidecar

This Job processes three batches and writes a line for each to a log file. A native sidecar ships that file, here by printing it to its own output. Before sidecars were a Kubernetes feature, a sidecar like this kept running after the main container finished and the Job never completed.

apiVersion: batch/v1
kind: Job
metadata:
  name: report
spec:
  backoffLimit: 2 # retry a failed pod at most twice
  ttlSecondsAfterFinished: 600 # delete the Job and its pod 10 minutes after it ends
  template:
    spec:
      restartPolicy: Never
      volumes:
        - name: logs
          emptyDir: {}
      initContainers:
        # A native sidecar: it does not keep the Job from completing
        - name: log-shipper
          image: busybox:1.38.0
          restartPolicy: Always
          command: ['sh', '-c', 'touch /logs/report.log && tail -f /logs/report.log']
          volumeMounts:
            - name: logs
              mountPath: /logs
      containers:
        - name: report
          image: busybox:1.38.0
          command:
            - sh
            - -c
            - for i in 1 2 3; do echo "processed batch $i" >> /logs/report.log; sleep 2; done
          volumeMounts:
            - name: logs
              mountPath: /logs

The Job completed, and Kubernetes stopped the sidecar after the main container exited. backoffLimit caps the retries of a failing Job, and ttlSecondsAfterFinished deletes the finished Job and its pod after 10 minutes so they do not pile up.

A CronJob

A CronJob creates a Job on a schedule. timeZone makes the schedule independent of the control plane's clock, and concurrencyPolicy: Forbid skips a run while the previous one is still running:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: cleanup
spec:
  schedule: '0 3 * * *' # 03:00 every day
  timeZone: Etc/UTC
  concurrencyPolicy: Forbid # skip a run while the previous one is still running
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      backoffLimit: 1
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: cleanup
              image: busybox:1.38.0
              command: ['sh', '-c', 'echo "cleanup ran at $(date -u)"']

To test it without waiting until 03:00, create a Job from its template:

Clean up. The manual Job belongs to the CronJob through an owner reference, so it goes with it:

Keep the podinfo Deployment: the next part puts a Service in front of it.

Common Mistakes

  • No readiness probe. Kubernetes then considers a pod ready as soon as its container starts, and a rolling update can replace working pods with broken ones.
  • Changing the selector. spec.selector is immutable. Plan your labels before the first apply.
  • Editing live objects and forgetting the file. set image, scale and edit drift from Git; kubectl diff shows the difference.
  • Expecting an automatic rollback. A failed rollout stops and reports ProgressDeadlineExceeded; your pipeline or a tool such as Argo Rollouts decides what to do next.
  • Jobs that never get cleaned up. Set ttlSecondsAfterFinished and the CronJob history limits.

Summary

  • A Deployment manages ReplicaSets, one per version of the pod template, and the ReplicaSets manage the pods.
  • maxSurge, maxUnavailable, readiness probes and minReadySeconds decide how safe a rolling update is.
  • kubectl rollout status, history, undo and restart follow, list, reverse and restart rollouts.
  • Jobs run to completion, CronJobs run Jobs on a schedule, DaemonSets run one pod per node, and native sidecars do not block Jobs.

Keep Learning

Next, in Part 5, you give these pods one stable address and expose them outside the cluster.

Updated:

Found an issue?