02-workloads-health-config

Deployments, Rollouts and the Other Workload Types

How a Deployment updates without downtime, what maxSurge and maxUnavailable really control, and the workload types most tutorials skip.

September 13, 2026
kubernetesdeploymentsreplicasetsrolloutsdaemonsetjobcronjob

Three Objects, One Chain

A Deployment doesn't manage pods. It manages ReplicaSets, and those manage pods:

Understanding the middle layer explains how rollback works. When you change the pod template, the Deployment creates a new ReplicaSet and gradually shifts replicas from the old to the new. The old ReplicaSet isn't deleted — it's scaled to zero and kept.

That retained object is the rollback. kubectl rollout undo scales the previous ReplicaSet back up. It's not re-deploying from a stored manifest; the old configuration is still sitting there.

bash
kubectl get replicasets
# NAME              DESIRED   CURRENT   READY   AGE
# api-7d4f8b9c5     3         3         3       2m
# api-5c8f7a2b1     0         0         0       1h    <- previous version, kept

revisionHistoryLimit controls how many are kept; the default is 10.

💡

A change only triggers a rollout if it changes the pod template (spec.template). Editing replicas scales the existing ReplicaSet without creating a new revision — which is correct, since scaling isn't a new version.

Rolling Updates

The default strategy replaces pods gradually, governed by two fields:

yaml
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%          # extra pods allowed ABOVE the desired count
      maxUnavailable: 25%    # pods allowed to be missing BELOW it

Both default to 25%. So a 4-replica Deployment may run up to 5 pods during a rollout and must keep at least 3 available.

The two knobs trade speed against capacity:

SettingEffect
maxSurge: 0Never exceed the replica count — for tight resource budgets. Slower.
maxUnavailable: 0Never drop below full capacity. Requires maxSurge > 0. Safest.
Both highFastest rollout, largest capacity dip

maxUnavailable: 0 with maxSurge: 1 is a common choice for a service that must not lose capacity: bring up one new pod, wait for it to be ready, then retire one old pod.

🚨

Rolling updates depend entirely on readiness probes. Without one, a pod counts as available the moment its container starts — so Kubernetes cheerfully retires an old pod and replaces it with one that isn't ready to serve. Your "zero-downtime" rollout drops requests. The next guide is about exactly this.

The Recreate strategy

yaml
spec:
  strategy:
    type: Recreate

Terminate everything, then start the new version. That means downtime — chosen deliberately when two versions genuinely cannot coexist: an incompatible schema migration, or a workload holding an exclusive lock. Honest downtime beats subtle corruption.

Driving and Observing a Rollout

bash
kubectl rollout status deploy/api          # block until complete or failed
kubectl rollout history deploy/api         # revisions
kubectl rollout undo deploy/api            # back one revision
kubectl rollout undo deploy/api --to-revision=3
kubectl rollout restart deploy/api         # restart pods without changing the image

kubectl rollout restart is genuinely useful — it patches an annotation on the pod template, which creates a new ReplicaSet and rolls pods safely. It's the supported way to pick up a changed ConfigMap or Secret, which the configuration guide returns to.

If a rollout stalls, progressDeadlineSeconds is what marks it as failed rather than leaving it pending forever.

Check yourself

A Deployment with 4 replicas uses the defaults. Mid-rollout, how many pods can exist and how many must stay available?

DaemonSets

One pod per node, automatically. New node joins, it gets a pod; node leaves, the pod goes.

yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: log-collector
spec:
  selector:
    matchLabels: { app: log-collector }
  template:
    metadata:
      labels: { app: log-collector }
    spec:
      tolerations:
        - operator: Exists     # run on tainted nodes too, including control plane
      containers:
        - name: collector
          image: fluent-bit:3.1

For infrastructure that must be everywhere: log collectors, metrics agents, CNI plugins, storage drivers. Note the toleration — without it, a DaemonSet skips tainted nodes, which usually defeats the purpose for a log collector.

Jobs

Run to completion, then stop.

yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: migrate
spec:
  backoffLimit: 4
  template:
    spec:
      restartPolicy: OnFailure
      containers:
        - name: migrate
          image: myapp:1.4.0
          command: ["npm", "run", "migrate"]

backoffLimit caps retries before the Job is marked failed. completions and parallelism handle batch work — run N items, M at a time.

⚠️

A Job's pods are not deleted when it completes — that's deliberate, so you can read their logs. They accumulate. ttlSecondsAfterFinished cleans them up automatically, and it's worth setting on anything that runs regularly.

CronJobs

A Job on a schedule.

yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: nightly-report
spec:
  schedule: "0 2 * * *"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: report
              image: myapp:1.4.0
              command: ["npm", "run", "report"]

concurrencyPolicy is the field to think about, because the default surprises people:

  • Allow (default) — a new run starts even if the previous one is still going. A job that occasionally overruns its interval will pile up.
  • Forbid — skip the new run if the previous is still active.
  • Replace — kill the running one and start fresh.

Two things that catch people out. CronJob schedules use the kube-controller-manager's timezone unless you set timeZone explicitly — so "2am" may not be the 2am you meant. And if the controller is down long enough to miss a scheduled time, the run is skipped rather than backfilled.

Check yourself

A CronJob runs every 5 minutes. Under load, a run occasionally takes 12 minutes. With default settings, what happens?

Choosing a Workload Type

NeedUse
Stateless app, interchangeable replicasDeployment
Stable identity, per-replica storageStatefulSet (phase 4)
One pod on every nodeDaemonSet
Run once to completionJob
Run on a scheduleCronJob

What's Next

Rolling updates only work if Kubernetes can tell a ready pod from a running one. The next guide covers the three probes that make that distinction — and why the most common probe mistake takes down a whole fleet instead of one pod.