Kubernetes workload types
In this series (14 parts)
- Why Kubernetes exists
- Kubernetes architecture
- Core Kubernetes objects
- Kubernetes networking
- Storage in Kubernetes
- Kubernetes configuration and secrets
- Resource management and autoscaling
- Kubernetes workload types
- Kubernetes observability
- Kubernetes security
- Helm and package management
- GitOps with ArgoCD
- Kubernetes cluster operations
- Service mesh concepts
Every pod in Kubernetes is ephemeral. It can be killed, rescheduled, or replaced at any moment. Raw pods are not useful in production. Controllers are what give pods their operational behavior: how many replicas run, how updates roll out, whether pods need stable identities, and whether they run once or on a schedule.
Kubernetes ships five workload controllers. Each one solves a distinct scheduling problem. Picking the wrong one means fighting the platform instead of using it.
Deployment: stateless applications
A Deployment manages a set of identical, interchangeable pods. It is the default choice for stateless services like API servers, web frontends, and worker processes that do not need persistent local state. The Deployment controller ensures the desired number of replicas are running and handles rolling updates when you change the pod spec.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
labels:
app: api-server
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
spec:
containers:
- name: api
image: myregistry/api-server:1.4.0
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
Setting maxUnavailable: 0 ensures that no existing pod is terminated until its replacement passes its readiness probe. Setting maxSurge: 1 limits the extra capacity created during rollout. This combination gives you zero-downtime deploys at the cost of needing one extra pod worth of resources during transitions.
stateDiagram-v2 [*] --> Complete Complete --> Progressing: spec update Progressing --> Progressing: pods rolling Progressing --> Complete: all replicas ready Progressing --> Failed: deadline exceeded Failed --> Progressing: spec update or rollback
Deployment rollout states. A spec change moves the Deployment to Progressing. It returns to Complete once all new replicas are ready.
If a rollout stalls, kubectl rollout undo deployment/api-server reverts to the previous ReplicaSet. Kubernetes keeps a history of ReplicaSets (controlled by revisionHistoryLimit) so you can roll back quickly.
StatefulSet: stable identity and persistent storage
Some workloads cannot treat pods as interchangeable. Databases, message brokers, and consensus systems need stable network identities and persistent storage that survives rescheduling. StatefulSets provide both.
Each pod in a StatefulSet gets an ordinal index. Pod names follow the pattern <statefulset-name>-<ordinal>, so postgres-0, postgres-1, postgres-2. Combined with a headless Service, each pod gets a stable DNS name like postgres-0.postgres-headless.default.svc.cluster.local. This identity persists across restarts.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres-headless
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
ports:
- containerPort: 5432
volumeMounts:
- name: pgdata
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: pgdata
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 50Gi
The volumeClaimTemplates field creates a unique PersistentVolumeClaim for each pod. When postgres-1 is rescheduled, Kubernetes reattaches the same PVC. Data is not lost.
Pods are created in order (0, then 1, then 2) and terminated in reverse. This ordered startup matters for workloads like ZooKeeper where a quorum must form before followers join. If you do not need ordered operations, set podManagementPolicy: Parallel to speed things up.
DaemonSet: one pod per node
DaemonSets ensure exactly one copy of a pod runs on every node in the cluster (or a subset selected by node selectors or tolerations). They are the right choice for node-level infrastructure: log collectors, monitoring agents, network plugins, and storage drivers.
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: log-collector
spec:
selector:
matchLabels:
app: log-collector
template:
metadata:
labels:
app: log-collector
spec:
containers:
- name: fluentbit
image: fluent/fluent-bit:3.0
volumeMounts:
- name: varlog
mountPath: /var/log
readOnly: true
volumes:
- name: varlog
hostPath:
path: /var/log
When a new node joins the cluster, the DaemonSet controller automatically schedules a pod on it. When a node is removed, that pod is garbage collected. You do not manage replica counts. The cluster topology is the replica count.
DaemonSets support rolling updates. By default they use OnDelete, which only replaces pods when you manually delete them. Switch to RollingUpdate with maxUnavailable: 1 for automated rollouts.
Job: one-off batch work
A Job creates one or more pods and ensures they run to successful completion. Use Jobs for data migrations, batch processing, report generation, or any task that should run once and then stop.
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration
spec:
backoffLimit: 3
activeDeadlineSeconds: 600
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: myregistry/db-migrate:2.1.0
command: ["./migrate", "--target", "v42"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: url
backoffLimit: 3 means Kubernetes retries up to three times on failure. activeDeadlineSeconds: 600 kills the Job if it has not completed within 10 minutes. restartPolicy: Never ensures failed pods are not restarted in place, so the Job controller creates new pods for retries.
For parallel batch work, set completions and parallelism:
spec:
completions: 10
parallelism: 3
This runs 10 tasks total, 3 at a time. Each pod picks up one unit of work from a queue.
CronJob: scheduled batch work
A CronJob wraps a Job spec with a cron schedule. It is for recurring tasks like nightly backups, hourly aggregations, or periodic cleanup.
apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-backup
spec:
schedule: "0 2 * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
backoffLimit: 2
template:
spec:
restartPolicy: OnFailure
containers:
- name: backup
image: myregistry/pg-backup:1.0.0
command: ["./backup.sh"]
env:
- name: S3_BUCKET
value: "my-backups"
concurrencyPolicy: Forbid prevents a new Job from starting if the previous run is still active. Other options are Allow (the default, which lets Jobs overlap) and Replace (which kills the running Job before starting a new one).
The schedule field uses standard cron syntax. 0 2 * * * runs at 2:00 AM UTC every day. Keep in mind that CronJobs depend on the kube-controller-manager clock, so skew or downtime can cause missed runs.
Choosing the right workload type
The decision comes down to two questions. Is your workload long-running or batch? Does it need stable identity?
| Requirement | Controller |
|---|---|
| Stateless, long-running, horizontally scalable | Deployment |
| Stable network identity, persistent per-pod storage | StatefulSet |
| Exactly one pod per node | DaemonSet |
| Run to completion, then stop | Job |
| Run to completion on a schedule | CronJob |
Start with a Deployment. Most services are stateless. Only reach for a StatefulSet when you have a concrete requirement for ordered startup, stable DNS names, or per-pod persistent volumes. If you find yourself attaching PVCs to a Deployment and writing logic to route traffic to specific pods, you need a StatefulSet.
DaemonSets are infrastructure. If your team manages the cluster itself (logging, networking, security agents), you will use DaemonSets. Application teams rarely create them directly.
Jobs and CronJobs handle batch work. If you are running a one-off migration, use a Job. If that migration needs to happen every night, wrap it in a CronJob.
What comes next
Workload controllers keep pods running, but they do not tell you whether those pods are healthy, performing well, or about to fall over. The next post covers observability, where you will set up metrics, logs, and traces so you can actually see what your workloads are doing in production.