Skip to main content
Kubernetes intermediate Lesson 8 of 8

Workload Controllers

Learn how Kubernetes controllers like Deployments, StatefulSets, DaemonSets, and Jobs maintain desired state and handle reconciliation.

The Reconciliation Loop

Every Kubernetes controller runs the same basic loop:

┌─────────────────────────────────────────┐
│  Controller loop                        │
│                                         │
│  1. Observe  — read current state       │
│  2. Diff     — compare to desired state │
│  3. Act      — create/update/delete     │
│  4. Repeat   — forever                  │
└─────────────────────────────────────────┘

You never tell Kubernetes “start 3 pods”. You declare replicas: 3 and the controller figures out what to do.

Deployment (Stateless Apps)

The controller you’ll use most. Manages rolling updates and rollbacks.

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web-app
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: web-app
    spec:
      containers:
        - name: web
          image: nginx:1.25
          ports:
            - containerPort: 80
# Watch the reconciliation happen
kubectl apply -f deployment.yaml
kubectl get replicasets -w

# Manually delete a pod — Deployment recreates it immediately
kubectl delete pod <pod-name>
kubectl get pods -w   # new pod appears within seconds

ReplicaSet (Managed by Deployment)

You rarely create ReplicaSets directly, but understanding them helps.

# See the ReplicaSets a Deployment manages
kubectl get replicasets -l app=web-app

# After updating image tag, Deployment creates a NEW ReplicaSet
# Old RS scales to 0, new RS scales up
kubectl rollout history deployment/web-app
kubectl get replicasets   # you'll see two: old (0 pods) and new

StatefulSet (Stateful Apps)

For workloads that need stable pod identity and ordered startup/shutdown — databases, message queues.

# statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres-headless   # must reference a headless service
  replicas: 3
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:15
          env:
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: pg-secret
                  key: POSTGRES_PASSWORD
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:            # each pod gets its OWN PVC
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 5Gi
# Required headless service for stable DNS
apiVersion: v1
kind: Service
metadata:
  name: postgres-headless
spec:
  clusterIP: None
  selector:
    app: postgres
  ports:
    - port: 5432
kubectl apply -f statefulset.yaml

# Pods are named with ordinal index: postgres-0, postgres-1, postgres-2
kubectl get pods -l app=postgres

# Each gets a stable DNS name:
# postgres-0.postgres-headless.default.svc.cluster.local
# postgres-1.postgres-headless.default.svc.cluster.local

# Each pod gets its own PVC
kubectl get pvc

Key differences from Deployment:

DeploymentStatefulSet
Pod namesRandom suffixStable ordinal (app-0, app-1)
Startup orderParallelOrdered (0 → 1 → 2)
StorageShared or noneEach pod gets its own PVC
DNSService onlyIndividual pod DNS names

DaemonSet (One Pod per Node)

Ensures exactly one Pod runs on every node. Ideal for log collectors, monitoring agents, network plugins.

# daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: log-collector
spec:
  selector:
    matchLabels:
      app: log-collector
  template:
    metadata:
      labels:
        app: log-collector
    spec:
      containers:
        - name: fluentd
          image: fluent/fluentd:v1.16
          volumeMounts:
            - name: varlog
              mountPath: /var/log
      volumes:
        - name: varlog
          hostPath:
            path: /var/log
kubectl apply -f daemonset.yaml

# One pod per node — scales automatically when nodes are added/removed
kubectl get daemonset
kubectl get pods -l app=log-collector -o wide   # shows which node each runs on

Job (Run-to-Completion)

Runs one or more pods to successful completion — database migrations, batch processing, one-time scripts.

# job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: db-migrate
spec:
  completions: 1
  backoffLimit: 3        # retry up to 3 times on failure
  template:
    spec:
      restartPolicy: OnFailure
      containers:
        - name: migrate
          image: my-app:1.0
          command: ["python", "manage.py", "migrate"]
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: db-secret
                  key: DATABASE_URL
kubectl apply -f job.yaml
kubectl get jobs
kubectl logs job/db-migrate

CronJob (Scheduled Jobs)

# cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: nightly-backup
spec:
  schedule: "0 2 * * *"     # every day at 2 AM (cron syntax)
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: backup
              image: my-backup-tool:1.0
              command: ["./backup.sh"]
kubectl apply -f cronjob.yaml
kubectl get cronjobs

# Manually trigger the job
kubectl create job backup-now --from=cronjob/nightly-backup

# See job runs
kubectl get jobs

Controller Comparison

ControllerUse casePods have stable identity?Persistent storage per pod?
DeploymentStateless web/API serversNoNo
StatefulSetDatabases, queuesYesYes
DaemonSetNode-level agents (logging, monitoring)NoNo
JobOne-off batch tasks, migrationsNoNo
CronJobScheduled recurring tasksNoNo

Learning Outcomes

You can:

  • Explain the reconciliation loop and why controllers are central to Kubernetes
  • Choose the right controller for stateless apps, databases, node agents, and batch jobs
  • Write Deployment, StatefulSet, DaemonSet, Job, and CronJob manifests
  • Understand why StatefulSets are required for databases
  • Observe reconciliation with kubectl get pods -w and kubectl get replicasets

Frequently Asked Questions

What does "reconciliation" mean in Kubernetes?
Controllers continuously compare the desired state (spec) to the actual state and make changes to converge — creating, updating, or deleting resources as needed.
When should I use a StatefulSet instead of a Deployment?
Use a StatefulSet when each pod needs a stable identity, stable DNS name, and persistent storage that stays bound to the same pod across restarts — databases like PostgreSQL, Kafka, Elasticsearch.