Kubernetes Workloads

Each major section below ends with a quick knowledge check — try to answer before revealing. Track your progress as you go:

0/0 checks

Pod Lifecycle

stateDiagram-v2
    [*] --> Pending: Pod created, waiting for scheduler
    Pending --> Running: Node assigned, containers starting
    Running --> Succeeded: All containers exited 0 (Job complete)
    Running --> Failed: Container exited non-zero, restartPolicy=Never
    Running --> Running: Container crash, restartPolicy=Always (CrashLoopBackOff if repeated)
    Pending --> Failed: Image pull error, resource unavailable
graph LR
    classDef phase fill:#3498db,stroke:#2980b9,color:#fff
    classDef cstate fill:#9b59b6,stroke:#8e44ad,color:#fff
    classDef good fill:#2ecc71,stroke:#27ae60,color:#fff
    classDef bad fill:#e74c3c,stroke:#c0392b,color:#fff
    classDef warn fill:#f39c12,stroke:#d68910,color:#000

    PENDING["Pending Scheduled, pulling image"]:::phase
    RUNNING["Running At least 1 container running"]:::phase
    SUCCEEDED["Succeeded All containers exited 0"]:::good
    FAILED["Failed Container exited non-zero"]:::bad
    UNKNOWN["Unknown Node unreachable"]:::warn

    PENDING --> RUNNING
    RUNNING --> SUCCEEDED
    RUNNING --> FAILED
    RUNNING --> UNKNOWN

Step through the phases one at a time:

1. Pending. The pod object exists but isn't running yet — the scheduler hasn't assigned a node, or a node is assigned and the kubelet is still pulling images. No containers are executing.
2. Running. The pod has been bound to a node and at least one container is executing. Init containers (if any) have already completed in order before this point.
3. Succeeded. Every container exited with code 0 and none will restart — a terminal phase. Normal for a Job's pod, not something you'd expect from a long-running service.
4. Failed. A container exited non-zero and restartPolicy: Never means kubelet won't try again — also terminal. Contrast with restartPolicy: Always, where the same crash keeps the pod phase at Running while the container itself cycles through CrashLoopBackOff.
5. Unknown. The node has stopped reporting — kubelet can't be reached, so the control plane has no current information about the pod's containers. Not the pod's fault; it's a node/network problem.

Container states within a Running pod:

State Meaning
Waiting Starting, pulling image, or CrashLoopBackOff
Running Process is executing
Terminated Process exited (check exitCode: 0=success, 137=OOMKilled, 1=error)

CrashLoopBackOff — container keeps crashing. Kubernetes applies exponential backoff (10s → 20s → 40s → 80s → 160s → 5min cap) between restart attempts. Not a stuck state — it will keep retrying. Check logs with kubectl logs <pod> --previous.

A container keeps crashing but the pod's restartPolicy is Always. Does the pod phase flip to Failed?


Probes

Probes are health checks kubelet runs against your container. Three types, each with a distinct purpose.

graph TD
    classDef probe fill:#8e44ad,stroke:#6c3483,color:#fff
    classDef action fill:#e74c3c,stroke:#c0392b,color:#fff
    classDef ok fill:#2ecc71,stroke:#27ae60,color:#fff
    classDef note fill:#f39c12,stroke:#d68910,color:#000

    subgraph StartupProbe["Startup Probe"]
        SP["Runs FIRST during container startup Blocks liveness+readiness until it passes For slow-starting apps (legacy, DB migrations)"]:::probe
        SP -->|"fails failureThreshold times"| KILL1["Container killed and restarted"]:::action
        SP -->|"passes"| SP_OK["Liveness + Readiness probes begin"]:::ok
    end

    subgraph LivenessProbe["Liveness Probe"]
        LP["Runs throughout pod lifetime Is the process still alive and not deadlocked?"]:::probe
        LP -->|"fails consecutively"| KILL2["Container killed and restarted kubelet respects restartPolicy"]:::action
        LP -->|"passes"| LP_OK["Container stays running"]:::ok
    end

    subgraph ReadinessProbe["Readiness Probe"]
        RP["Runs throughout pod lifetime Is the process ready to serve traffic?"]:::probe
        RP -->|"fails"| REMOVE["Pod IP removed from Service endpoints Traffic stops flowing to this pod"]:::action
        RP -->|"passes"| ADD["Pod IP added to Service endpoints Traffic flows to pod"]:::ok
    end

Step through the evaluation order:

1. Startup probe (if defined) runs first. Liveness and readiness probes don't start yet — they're blocked until this one passes. Meant for slow-starting apps so a long boot isn't mistaken for a hang.
2. Startup probe passes. Liveness and readiness probes begin running, independently of each other, for the rest of the container's life.
3. Liveness probe fails failureThreshold times in a row. kubelet kills the container and restarts it per restartPolicy. The pod itself isn't removed — this is a container-level action.
4. Readiness probe fails. The pod's IP is pulled from the Service's endpoints — traffic stops routing to it. The container keeps running untouched; this is purely a traffic decision, not a restart.
5. Readiness probe passes again. The pod's IP is added back to the Service's endpoints and traffic resumes — no restart was ever needed for this path.

Probe implementation types:

# HTTP GET — most common for web services
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 10   # wait before first probe (startup time)
  periodSeconds: 10          # probe every 10s
  failureThreshold: 3        # fail 3 times before action
  timeoutSeconds: 5

# TCP Socket — for non-HTTP (gRPC, databases)
readinessProbe:
  tcpSocket:
    port: 5432
  initialDelaySeconds: 5

# Exec — run command inside container
livenessProbe:
  exec:
    command: ["pg_isready", "-U", "postgres"]
  periodSeconds: 30

# gRPC (K8s 1.24+)
livenessProbe:
  grpc:
    port: 8080
    service: "liveness"

Key probe interactions:

  • Liveness failure → container restart (pod stays, container restarts)
  • Readiness failure → pod removed from service endpoints (pod stays running, just gets no traffic)
  • Startup probe present → liveness and readiness are BLOCKED until startup passes

A container's readiness probe starts failing, but its liveness probe keeps passing. Does kubelet restart the container?


QoS Classes

Kubernetes assigns a QoS class to every pod. This determines OOM kill priority when a node runs out of memory.

graph TD
    classDef guaranteed fill:#2ecc71,stroke:#27ae60,color:#fff
    classDef burstable fill:#f39c12,stroke:#d68910,color:#000
    classDef besteffort fill:#e74c3c,stroke:#c0392b,color:#fff

    subgraph G["Guaranteed — OOM killed LAST"]
        G1["requests == limits for ALL containers cpu: 500m request = 500m limit memory: 256Mi request = 256Mi limit"]:::guaranteed
    end

    subgraph B["Burstable — OOM killed if over request"]
        B1["requests < limits (or only requests set) cpu: 200m request, 1000m limit memory: 128Mi request, 512Mi limit"]:::burstable
    end

    subgraph BE["BestEffort — OOM killed FIRST"]
        BE1["No requests, no limits set Kernel kills these first under memory pressure"]:::besteffort
    end

    G --> B --> BE
    BE -.- NOTE["OOM kill order: BestEffort first, then Burstable over their request, then Guaranteed (almost never)"]

Production recommendation: Set both requests and limits for all containers. Use Guaranteed class for critical services (controllers, databases). Never run production workloads as BestEffort.

A container sets a memory request of 128Mi and a memory limit of 512Mi, with no CPU request/limit set at all. What QoS class does the pod get?


Rolling Updates and Rollbacks

graph LR
    classDef v1 fill:#3498db,stroke:#2980b9,color:#fff
    classDef v2 fill:#2ecc71,stroke:#27ae60,color:#fff
    classDef terminating fill:#e74c3c,stroke:#c0392b,color:#fff
    classDef rs fill:#9b59b6,stroke:#8e44ad,color:#fff

    subgraph Start["Start: 3x v1 running"]
        V1A["v1 pod"]:::v1
        V1B["v1 pod"]:::v1
        V1C["v1 pod"]:::v1
    end

    subgraph Step1["maxSurge=1, maxUnavailable=0 Create 1 new v2 pod (surge)"]
        V1D["v1 pod"]:::v1
        V1E["v1 pod"]:::v1
        V1F["v1 pod"]:::v1
        V2A["v2 pod (new)"]:::v2
    end

    subgraph Step2["v2 pod passes readiness Terminate 1 v1 pod"]
        V1G["v1 pod"]:::v1
        V1H["v1 pod"]:::v1
        V1I["v1 pod terminating"]:::terminating
        V2B["v2 pod ✅"]:::v2
    end

    subgraph End["All 3 replaced with v2"]
        V2C["v2 pod"]:::v2
        V2D["v2 pod"]:::v2
        V2E["v2 pod"]:::v2
    end

    subgraph ReplicaSets["Deployment keeps both ReplicaSets"]
        RS1["ReplicaSet v1 replicas: 0 (kept for rollback)"]:::rs
        RS2["ReplicaSet v2 replicas: 3"]:::rs
    end
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1         # max pods ABOVE desired (create new before deleting old)
      maxUnavailable: 0   # max pods BELOW desired (zero-downtime: never remove before replacement ready)

Rollback: The old ReplicaSet is never deleted (kept at 0 replicas). Rollback simply scales old RS back up and new RS back down:

kubectl rollout undo deployment/my-app                    # rollback to previous
kubectl rollout undo deployment/my-app --to-revision=3    # rollback to specific revision
kubectl rollout history deployment/my-app                  # see revision history
kubectl rollout status deployment/my-app                   # watch rollout progress
kubectl rollout pause deployment/my-app                    # pause mid-rollout (canary hold)
kubectl rollout resume deployment/my-app                   # resume

After a Deployment rolls out v2, is the v1 ReplicaSet deleted?


StatefulSet vs Deployment

graph TD
    classDef deploy fill:#3498db,stroke:#2980b9,color:#fff
    classDef stateful fill:#9b59b6,stroke:#8e44ad,color:#fff
    classDef pvc fill:#2ecc71,stroke:#27ae60,color:#fff
    classDef storage fill:#e67e22,stroke:#d35400,color:#fff

    subgraph DeploymentModel["Deployment: interchangeable pods"]
        D1["app-7f9d4-abc random name"]:::deploy
        D2["app-7f9d4-def random name"]:::deploy
        D3["app-7f9d4-ghi random name"]:::deploy
        SHARED_STORAGE["Shared storage or no persistent storage"]:::storage
        D1 --- SHARED_STORAGE
        D2 --- SHARED_STORAGE
    end

    subgraph StatefulSetModel["StatefulSet: stable identity"]
        SS1["postgres-0 stable name, index 0"]:::stateful
        SS2["postgres-1 stable name, index 1"]:::stateful
        SS3["postgres-2 stable name, index 2"]:::stateful
        PVC1["PVC: postgres-data-postgres-0 bound only to postgres-0"]:::pvc
        PVC2["PVC: postgres-data-postgres-1 bound only to postgres-1"]:::pvc
        PVC3["PVC: postgres-data-postgres-2"]:::pvc
        SS1 --> PVC1
        SS2 --> PVC2
        SS3 --> PVC3
    end

StatefulSet guarantees:

Guarantee Detail
Stable pod names <name>-0, <name>-1, <name>-2 — never random
Stable DNS postgres-0.postgres-svc.ns.svc.cluster.local (requires Headless Service)
Stable storage Each pod gets its own PVC via volumeClaimTemplates. PVC survives pod deletion.
Ordered startup Pods start in order: 0 → 1 → 2. Each must be Running+Ready before next starts.
Ordered shutdown Pods stop in reverse order: 2 → 1 → 0.

When to use StatefulSet: Databases (PostgreSQL, MySQL), distributed systems with leader election (Kafka, Zookeeper, etcd), any app that needs stable network identity or per-instance storage.

A StatefulSet pod postgres-1 is deleted and recreated. Does it get a new PVC?


ReplicaSet vs StatefulSet — Deep Comparison

ReplicaSet (via Deployment) — Stateless Pods

A ReplicaSet ensures N identical, interchangeable pod replicas are running. You never create a ReplicaSet directly — you use a Deployment which manages ReplicaSets for you (rolling updates, rollback).

graph TD
    DEP["Deployment<br/>manages ReplicaSets"] --> RS_OLD["ReplicaSet v1<br/>replicas: 0 (kept for rollback)"]
    DEP --> RS_NEW["ReplicaSet v2<br/>replicas: 3"]
    RS_NEW --> P1["api-7f9d4-abc<br/>random suffix"]
    RS_NEW --> P2["api-7f9d4-def<br/>random suffix"]
    RS_NEW --> P3["api-7f9d4-ghi<br/>random suffix"]
    P1 & P2 & P3 --> PVC_SHARED["Shared PVC (optional)<br/>ReadWriteMany<br/>e.g. EFS"]

    classDef pod fill:#3498db,stroke:#2980b9,color:#fff
    classDef rs fill:#e67e22,stroke:#d35400,color:#fff
    class P1,P2,P3 pod
    class RS_OLD,RS_NEW rs

Pod identity: none. Pod api-7f9d4-abc is identical to api-7f9d4-def. If one dies, the ReplicaSet creates a replacement with a new random name — no data is lost because stateless pods don't hold state.

Scale up/down: all pods started/stopped in parallel, any order. Scale from 3→5: 2 new pods created simultaneously.

StatefulSet — Stateful Pods with Identity

graph TD
    STS["StatefulSet: postgres"] --> HS["Headless Service<br/>clusterIP: None<br/>postgres-svc"]
    STS --> P0["postgres-0<br/>stable index"]
    STS --> P1["postgres-1<br/>stable index"]
    STS --> P2["postgres-2<br/>stable index"]
    P0 --> PVC0["PVC: data-postgres-0<br/>always bound to postgres-0"]
    P1 --> PVC1["PVC: data-postgres-1<br/>always bound to postgres-1"]
    P2 --> PVC2["PVC: data-postgres-2<br/>always bound to postgres-2"]
    HS --> DNS0["DNS: postgres-0.postgres-svc.default.svc.cluster.local"]
    HS --> DNS1["DNS: postgres-1.postgres-svc.default.svc.cluster.local"]

    classDef pod fill:#9b59b6,stroke:#8e44ad,color:#fff
    classDef pvc fill:#2ecc71,stroke:#27ae60,color:#fff
    class P0,P1,P2 pod
    class PVC0,PVC1,PVC2 pvc

Headless Service (clusterIP: None) — required for StatefulSet DNS. Unlike a regular Service (which load-balances), a Headless Service creates individual DNS records for each pod. postgres-0.postgres-svc always resolves to postgres-0's IP — clients can target a specific pod (e.g., write to primary, read from replicas).

Ordered Startup and Deletion

sequenceDiagram
    participant STS as StatefulSet Controller
    participant P0 as postgres-0
    participant P1 as postgres-1
    participant P2 as postgres-2

    Note over STS: Scale up (0 --> 3)
    STS->>P0: create postgres-0
    P0-->>STS: Running + Ready
    STS->>P1: create postgres-1 (only after P0 ready)
    P1-->>STS: Running + Ready
    STS->>P2: create postgres-2 (only after P1 ready)
    P2-->>STS: Running + Ready

    Note over STS: Scale down (3 --> 1)
    STS->>P2: delete postgres-2 (highest index first)
    P2-->>STS: terminated
    STS->>P1: delete postgres-1
    P1-->>STS: terminated
    Note over P0: postgres-0 remains

Why ordered? Databases need the primary (index 0) to be healthy before replicas join. Shutting down the replica before the primary prevents split-brain.

Scaling a StatefulSet down from 3 to 1, which pod is deleted first?


Which Applications Use Which

graph TD
    APP["Your Application"] --> Q1{"Does each instance<br/>need its own<br/>persistent data?"}
    Q1 -->|No| Q2{"Multiple instances<br/>interchangeable?"}
    Q1 -->|Yes| STATEFUL["StatefulSet"]
    Q2 -->|Yes| DEPLOYMENT["Deployment + ReplicaSet"]
    Q2 -->|No singleton| SINGLETON["Deployment replicas:1<br/>or StatefulSet replicas:1"]

    classDef dep fill:#3498db,stroke:#2980b9,color:#fff
    classDef sts fill:#9b59b6,stroke:#8e44ad,color:#fff
    class DEPLOYMENT dep
    class STATEFUL,SINGLETON sts

Deployment (ReplicaSet) — Stateless Workloads

Application Why Deployment
REST API servers All pods identical, share DB connection, any pod can handle any request
GraphQL / gRPC services Stateless request handling
Web frontends No per-pod data
Worker processes (SQS consumer) Any worker can process any message
Batch processors Jobs consume from a queue, no local state
ML inference servers (vLLM, TorchServe) Model loaded in each pod's memory but no unique per-pod data to persist
API gateways, proxies Pure request routing, stateless

StatefulSet — Stateful Workloads

Application Why StatefulSet Key requirement
PostgreSQL Each replica has its own WAL + data directory Per-pod PVC, ordered startup (primary first)
MySQL / MariaDB Master-replica with per-node data Stable names for replication config
Redis (cluster mode) Each shard owns specific key slots Stable identity for cluster topology
Apache Kafka Each broker owns specific partitions on disk Stable broker ID = stable pod name
Zookeeper Quorum requires stable myid zoo-0, zoo-1, zoo-2 fixed identity
Elasticsearch Each node stores specific shards Per-pod PVC, stable cluster node names
Cassandra Each node owns a token range Stable gossip identity
etcd Raft consensus requires stable peer URLs etcd-0.etcd etc.

The Key Question to Ask

"If I delete this pod and a new one starts with a different name and no local data,
does the application still work correctly?"

YES → Deployment
NO  → StatefulSet

Redis Standalone (replicas:1, in-memory cache): Deployment — if it restarts, it starts fresh. The cache warms up again. Acceptable.

Redis Cluster (sharded, data must persist): StatefulSet — each node owns data, must reconnect to cluster with same identity.

PostgreSQL primary: StatefulSet — data on disk, WAL, per-node state. Restarting with a different name would break replication setup.

A worker pod deletes itself and a replacement starts with a completely different name and no data carried over from the old one. Does the app still work correctly?


Comparison Table

Feature Deployment (ReplicaSet) StatefulSet
Pod names Random (app-abc123-xyz) Stable (app-0, app-1)
Pod DNS Single Service (load-balanced) Headless Service (per-pod DNS)
Storage Shared PVC or no PVC Per-pod PVC via volumeClaimTemplates
PVC on pod delete PVC deleted with pod PVC survives pod deletion
Scale up order Parallel (all at once) Sequential (0, 1, 2...)
Scale down order Any order Reverse (N, N-1, N-2...)
Rolling update New RS created, old RS scaled down Pod-by-pod, highest index first
Use case Stateless APIs, workers Databases, queues, consensus systems

Init Containers

Init containers run sequentially before app containers start. Each must exit 0 before the next runs. If any init container fails, the pod restarts.

sequenceDiagram
    participant K as kubelet
    participant I1 as init-container-1
    participant I2 as init-container-2
    participant APP as app container

    K->>I1: start (wait-for-db)
    Note over I1: loops until postgres:5432 is reachable
    I1-->>K: exit 0
    K->>I2: start (run-migrations)
    I2-->>K: exit 0 (migrations done)
    K->>APP: start main application
    Note over APP: DB is ready AND migrated before app starts
spec:
  initContainers:
  # 1. Wait for dependency to be ready
  - name: wait-for-db
    image: busybox
    command: ['sh', '-c', 'until nc -z postgres-svc 5432; do echo waiting; sleep 2; done']

  # 2. Run DB migration before app starts
  - name: migrate
    image: my-app:v2
    command: ["/app/server", "migrate"]
    env:
    - name: DATABASE_URL
      valueFrom:
        secretKeyRef:
          name: db-secret
          key: url

  containers:
  - name: app
    image: my-app:v2
    # starts only after both init containers succeed

Common init container patterns:

Pattern Init container does Why
Wait for dependency nc -z svc port loop Prevent app crash on startup
DB migration ./migrate up Run once before N replicas start
Secret injection Fetch secret from Vault, write to emptyDir Avoid mounting Vault agent sidecar
Config rendering Render templates with env vars App expects rendered config, not templates
Permission fix chown shared volume App needs specific UID on data directory

Init containers share volumes with app containers — this is how they pass data:

volumes:
- name: config
  emptyDir: {}
initContainers:
- name: render-config
  image: renderer:v1
  command: ["render", "--output", "/config/app.yaml"]
  volumeMounts:
  - name: config
    mountPath: /config
containers:
- name: app
  volumeMounts:
  - name: config
    mountPath: /etc/app   # reads rendered config written by init container

A pod has two init containers. The first succeeds, the second fails. Does the app container start?


Ephemeral Containers — Debug Running Pods

Ephemeral containers can be injected into a running pod without restarting it. Used for debugging when the app container has no shell (distroless, scratch).

# Inject a debug container into a running pod
kubectl debug -it my-pod \
  --image=nicolaka/netshoot \    # has tcpdump, curl, dig, netstat
  --target=app \                  # shares app container's process namespace
  -- bash

# Inside netshoot: debug the running app
ps aux              # see app processes (shared PID namespace)
netstat -tlnp       # see what ports app is listening on
curl localhost:8080/health
tcpdump -i any port 8080

# Debug with a copy of the pod (for crashlooping pods)
kubectl debug my-crashlooping-pod \
  --copy-to=debug-copy \
  --image=busybox \
  --container=app \              # override the crashing container's image
  -- sleep 3600
# Now exec into debug-copy to inspect filesystem, env vars, etc.
graph LR
    POD["Running Pod<br/>(distroless — no shell)"] -->|"kubectl debug inject"| EPH["Ephemeral container<br/>nicolaka/netshoot<br/>shares: network NS, PID NS"]
    EPH -->|"can see"| PROC["App processes"]
    EPH -->|"can curl"| NET["App network"]
    EPH -->|"cannot modify"| POD
    NOTE["Pod is NOT restarted<br/>Ephemeral container auto-removed when exited"]

Ephemeral vs exec:

  • kubectl exec — needs a shell already in the container
  • kubectl debug — injects a new container with tools, works on distroless images

You inject an ephemeral debug container into a running pod with kubectl debug. Does the pod restart?


DaemonSet

Runs exactly one pod per node (or per matching node). When new nodes join the cluster, DaemonSet automatically schedules a pod on them.

graph LR
    classDef ds fill:#1abc9c,stroke:#16a085,color:#fff
    classDef node fill:#34495e,stroke:#2c3e50,color:#fff

    DS["DaemonSet: fluentd-logger"]:::ds

    subgraph N1["Node 1"]
        P1["fluentd pod (auto-scheduled)"]:::ds
    end
    subgraph N2["Node 2"]
        P2["fluentd pod"]:::ds
    end
    subgraph N3["Node 3 (new node joins)"]
        P3["fluentd pod (auto-added)"]:::ds
    end

    DS --> P1 & P2 & P3

Common DaemonSet use cases: Log collectors (Fluentd, Fluent Bit), metrics agents (node-exporter), security agents (Falco), network plugins (CNI DaemonSets like aws-node), storage drivers.

DaemonSets can be constrained to specific nodes using nodeSelector or affinity — e.g., run GPU monitoring DaemonSet only on GPU nodes.

Does a DaemonSet have a replicas field you set to control how many pods run?


Jobs and CronJobs

graph TD
    classDef job fill:#e67e22,stroke:#d35400,color:#fff
    classDef cron fill:#9b59b6,stroke:#8e44ad,color:#fff
    classDef pod fill:#3498db,stroke:#2980b9,color:#fff
    classDef done fill:#2ecc71,stroke:#27ae60,color:#fff

    subgraph JobFlow["Job: run to completion"]
        J["Job: db-migrate completions: 1 parallelism: 1"]:::job
        J --> JP["Pod created"]:::pod
        JP --> DONE["Pod exits 0 Job status: Complete"]:::done
        JP --> FAIL["Pod exits non-zero Job retries (backoffLimit: 4)"]:::job
    end

    subgraph CronFlow["CronJob: scheduled Jobs"]
        CJ["CronJob: nightly-report schedule: 0 2 * * *"]:::cron
        CJ -->|"2 AM daily"| JOB1["Job created (new Job each trigger)"]:::job
        CJ -->|"next night"| JOB2["Job created"]:::job
    end
apiVersion: batch/v1
kind: Job
metadata:
  name: db-migrate
spec:
  completions: 1       # how many successful completions needed
  parallelism: 1       # how many pods run in parallel
  backoffLimit: 4      # retry up to 4 times on failure
  activeDeadlineSeconds: 300  # kill if takes > 5 min
  template:
    spec:
      restartPolicy: Never    # Job pods must use Never or OnFailure
      containers:
        - name: migrate
          image: my-app:v2
          command: ["/app/server", "migrate"]
---
apiVersion: batch/v1
kind: CronJob
metadata:
  name: nightly-report
spec:
  schedule: "0 2 * * *"
  concurrencyPolicy: Forbid       # don't start new job if previous still running
  successfulJobsHistoryLimit: 3   # keep last 3 successful job records
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: reporter
              image: my-app:v2
              command: ["/app/server", "report"]

A CronJob's schedule fires while the previous run's Job is still active, and concurrencyPolicy: Forbid is set. What happens?


Workload Types at a Glance

Four workload kinds, one question each: what makes them different in practice.

N interchangeable replicas. Random pod names, no stable identity, shared or no persistent storage, load-balanced Service. Scales and updates in any order. Right for stateless APIs, workers, gateways — anything where a fresh pod with no memory of the old one is fine.
N pods with stable identity. Names <name>-0, -1, ... never change, each gets its own PVC that survives pod deletion, ordered startup/shutdown, Headless Service for per-pod DNS. Right for databases and quorum systems (Kafka, Zookeeper, etcd) — anything where "which pod" or "which disk" matters.
Exactly one pod per (matching) node. No replicas field — the node count is the pod count. A new node joining the cluster gets a pod scheduled on it automatically, no manual scaling. Right for node-level agents: log shippers, metrics exporters, CNI plugins, security agents.
Run-to-completion, not long-running. Tracks completions and parallelism instead of a steady replica count, and retries failed pods up to backoffLimit before giving up. A CronJob is just a Job factory on a schedule — each trigger creates a brand-new Job object, it doesn't reuse the last one.