Steven's Knowledge

Workloads

Pods, Deployments, StatefulSets, DaemonSets, Jobs, and CronJobs - the right workload type for the right job

Workloads

ResourceWhat it managesUse when
PodOne or more containers sharing network/storageAlmost never directly — use a controller
DeploymentStateless replicas with rolling updatesWeb/API servers, workers
StatefulSetReplicas with stable identity and storageDatabases, queues, anything needing stable DNS
DaemonSetOne pod per nodeLog shippers, monitoring agents, CNI
JobRun to completionOne-off batch tasks, migrations
CronJobScheduled JobsBackups, periodic cleanup
ReplicaSetInternal — created by DeploymentDon't write directly

Pods (the foundation)

A pod is a group of co-located containers. They share an IP, can talk over localhost, and live or die together. You almost never create a pod directly — controllers like Deployment do it for you. The schema is still worth knowing:

apiVersion: v1
kind: Pod
metadata:
  name: hello
spec:
  containers:
    - name: app
      image: nginx:1.27
      ports:
        - containerPort: 80
      resources:
        requests: { cpu: "100m", memory: "128Mi" }
        limits:   { cpu: "500m", memory: "256Mi" }
      readinessProbe:
        httpGet: { path: /, port: 80 }
        periodSeconds: 10
      livenessProbe:
        httpGet: { path: /, port: 80 }
        periodSeconds: 20

Two pieces every container should have:

  • Resource requests/limits — requests reserve capacity for scheduling; limits cap actual usage. Without them, a runaway pod can starve neighbors.
  • readinessProbe / livenessProbe — readiness gates traffic ("ready to serve?"); liveness restarts the container if it deadlocks ("still alive?").

Deployment

The default for stateless services. Manages a ReplicaSet which manages pods, and handles rolling updates and rollbacks.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api-server
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1                          # at most 1 extra pod during update
      maxUnavailable: 0                    # never go below replicas
  template:
    metadata:
      labels:
        app: api-server
    spec:
      containers:
        - name: api
          image: myregistry/api-server:v1.2.3
          ports:
            - containerPort: 3000
          env:
            - name: NODE_ENV
              value: "production"
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef: { name: db-credentials, key: url }
          resources:
            requests: { cpu: "250m", memory: "256Mi" }
            limits:   { cpu: "1000m", memory: "512Mi" }
          readinessProbe:
            httpGet: { path: /health, port: 3000 }
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet: { path: /health, port: 3000 }
            initialDelaySeconds: 15
            periodSeconds: 20
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchExpressions:
                    - { key: app, operator: In, values: [api-server] }
                topologyKey: "kubernetes.io/hostname"

The podAntiAffinity block tells the scheduler to spread replicas across nodes, so a single node failure doesn't take down the service.

StatefulSet

For workloads where each pod needs a stable name and its own persistent storage — databases, queues, anything where pod-1 ≠ pod-2.

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres                    # headless Service for DNS
  replicas: 3
  selector:
    matchLabels: { app: postgres }
  template:
    metadata:
      labels: { app: postgres }
    spec:
      containers:
        - name: postgres
          image: postgres:16-alpine
          ports: [{ containerPort: 5432 }]
          env:
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef: { name: postgres-secret, key: password }
          volumeMounts:
            - { name: pgdata, mountPath: /var/lib/postgresql/data }
  volumeClaimTemplates:
    - metadata: { name: pgdata }
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: fast-ssd
        resources:
          requests: { storage: 50Gi }

Pods get stable names (postgres-0, postgres-1, postgres-2) and each gets its own PersistentVolumeClaim (pgdata-postgres-0, ...). DNS resolves postgres-0.postgres to a stable network identity.

StatefulSets are not a turnkey database. They give you stable pods and storage; replication, backups, and failover are your problem. Use a database operator (Zalando Postgres Operator, MongoDB Operator, ...) or a managed DB instead of rolling your own.

DaemonSet

One pod per node — for things every node should run.

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-exporter
  namespace: monitoring
spec:
  selector:
    matchLabels: { app: node-exporter }
  template:
    metadata:
      labels: { app: node-exporter }
    spec:
      hostNetwork: true
      hostPID: true
      containers:
        - name: node-exporter
          image: prom/node-exporter:v1.7.0
          ports: [{ containerPort: 9100, hostPort: 9100 }]
          volumeMounts:
            - { name: proc, mountPath: /host/proc, readOnly: true }
            - { name: sys,  mountPath: /host/sys,  readOnly: true }
      volumes:
        - { name: proc, hostPath: { path: /proc } }
        - { name: sys,  hostPath: { path: /sys } }

Typical DaemonSets: log shipper (Fluent Bit), metrics exporter (Node Exporter), CNI agent, storage driver.

Job

Run-to-completion workloads. A Job creates pods that run until they exit successfully.

apiVersion: batch/v1
kind: Job
metadata:
  name: db-migration
spec:
  backoffLimit: 3                          # retry 3 times on failure
  completions: 1
  template:
    spec:
      restartPolicy: OnFailure
      containers:
        - name: migrate
          image: myregistry/migrator:v1.2.3
          command: ["./migrate", "up"]
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef: { name: db-credentials, key: url }

Run it once per release:

kubectl apply -f migration-job.yaml
kubectl logs -f job/db-migration
kubectl delete job/db-migration            # clean up after success

For parallel processing of a work queue, set parallelism and completions together.

CronJob

Scheduled Jobs.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: db-backup
spec:
  schedule: "0 2 * * *"                    # daily at 02:00
  concurrencyPolicy: Forbid                # don't start a new one if the last is still running
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: backup
              image: myregistry/db-backup:latest
              env:
                - name: DATABASE_URL
                  valueFrom:
                    secretKeyRef: { name: db-credentials, key: url }

concurrencyPolicy options:

ValueBehavior
Allow (default)Overlapping runs are fine
ForbidSkip the new run if the previous hasn't finished
ReplaceKill the previous run and start the new one

Choosing the Right Workload

Quick decision tree:

Need to run a service?
├── Stateless? → Deployment
└── Stateful (own storage, stable name)? → StatefulSet

Need to run on every node? → DaemonSet

Run-to-completion?
├── One-shot? → Job
└── On a schedule? → CronJob

What's Next

You've got the right tool for every kind of workload. Now make them reachable — internally, externally, and from each other → Networking.

On this page