Seeking Wisdom · AI/ML Engineering · Investing · Books · Projects · About ·

Kubernetes by Example · Part 6

StatefulSets and Jobs

Three database replicas with random names, fighting over one disk. Some pods need to be somebody — and some workloads aren't servers at all.

assumes Parts 1–5~18 minutesstorage appears

The symptom

airwaves needs somewhere to keep the station list and the transcripts. You reach for what you know — a Deployment with three replicas — and it goes wrong in a way that's hard to even describe.

$ kubectl get pods -l app=catalogue
NAME                         READY   STATUS             RESTARTS
catalogue-6f9d4b7c8-k2mp4    0/1     CrashLoopBackOff   5
catalogue-6f9d4b7c8-qw8xn    1/1     Running            0
catalogue-6f9d4b7c8-z4lr9    0/1     Error              3

The logs are a mess of replicas failing to find each other, two of them claiming to be primary, and at least one complaining that its data directory is locked by someone else.

Delete the broken ones and the replacements have new names, so whatever they'd agreed among themselves is void and they start arguing again.

Every one of these problems traces to the same root. A Deployment's model is that its pods are identical and disposable — and a database replica is neither.

When a pod needs to be somebody

Three things a Deployment deliberately refuses to give you, because for most workloads they'd be dead weight: a name that survives a restart, a disk that follows that name, and a predictable order of starting and stopping.

A StatefulSet gives you all three. And while we're here, the two workloads that aren't servers at all.

Deployment — pods are interchangeable scribe-7d4f8b-x2kmp random name · no disk of its own scribe-7d4f8b-m9q4p dies → replacement has a different name started in any order, deleted in any order nothing addresses one of them specifically StatefulSet — pods have identity catalogue-0 catalogue-0.catalogue.airwaves.svc catalogue-1 same name after every restart catalogue-2 created last, deleted first its own disk data-catalogue-0 data-catalogue-1 data-catalogue-2 follows the name, not the pod Job — runs until it succeeds, then stops backfill-x7k2 Completed — and kept retries up to backoffLimit, then gives up for good CronJob — makes a Job on a schedule schedule: "0 3 * * *"in the cluster's timezone, not yours concurrencyPolicy: Forbid — skip if the last one is still going startingDeadlineSeconds — how late is too late to bother 0 is ready before 1 starts. Always. delete the StatefulSet — these stay

Picking the right one

Deployment — pods are interchangeable. Almost everything.

DaemonSet — one per node, placed by locality. Part 5.

StatefulSet — pods have names and their own disks. Databases, queues, anything that forms a cluster.

Job — runs until it succeeds, then stops. CronJob — makes Jobs on a schedule.

The commands

Reading a StatefulSet and its disks

Click any piece of the command.

Jobs, and running one on demand

Click any piece of the command.

Scheduling, and the ways it surprises you

Click any piece of the command.

Build it

The catalogue as a StatefulSet with its own storage, plus the nightly prune that keeps transcripts from growing forever.

The catalogue

kubectl apply -f manifests/catalogue.yaml
kubectl get sts,pods,pvc -l app=catalogue -w

Watch the order. catalogue-0 appears alone and must go Ready before catalogue-1 is created at all. On a Deployment all three would have appeared at once.

One disk each

kubectl get pvc -l app=catalogue
kubectl exec catalogue-0 -- sh -c 'echo "jazz-fm" > /data/stations.txt'
kubectl exec catalogue-0 -- cat /data/stations.txt

The nightly prune, run now

kubectl apply -f manifests/prune.yaml
kubectl get cronjob prune

# do not wait until 3am
kubectl create job prune-now --from=cronjob/prune
kubectl logs job/prune-now

Break it

1 · Kill a pod and watch it come back as itself

kubectl delete pod catalogue-0
kubectl get pods -l app=catalogue -w

# once it is Running again
kubectl exec catalogue-0 -- cat /data/stations.txt

Same name, same data. Compare with Part 2, where a deleted pod was replaced by a stranger that remembered nothing. The disk did not follow the pod — it followed the ordinal, and the new pod inherited both.

2 · Scale down and find the leftovers

kubectl scale sts catalogue --replicas=1
kubectl get pods -l app=catalogue
kubectl get pvc -l app=catalogue

Two pods gone, three PVCs still there. Kubernetes will not delete storage to satisfy a scale-down, and it is right not to.

Scale back up and the old data reappears:

kubectl scale sts catalogue --replicas=3
kubectl exec catalogue-2 -- ls -la /data

That's either a relief or a shock. Either way, cleaning up a StatefulSet properly means deleting its PVCs by hand, and nothing will prompt you.

3 · Make a CronJob overlap itself

kubectl apply -f manifests/slow-cron.yaml   # runs every minute, takes 90s
sleep 150
kubectl get jobs -l app=slow-cron

Two Jobs running at once, because concurrencyPolicy defaults to Allow and nobody chose it. For a prune job writing to shared storage, that's two processes deleting the same rows.

The fix is concurrencyPolicy: Forbid, and the lesson is that this default is wrong for most real jobs.

What follows from this

Because……this is what happens
Names are stableOther things can depend on catalogue-0 specifically. That is what makes clustering possible.
Claims follow the ordinalA replaced pod reattaches to the same data. The disk belongs to the slot, not the instance.
Startup is orderedPod 0 must be ready before 1 exists. A stuck low ordinal blocks everything above it.
PVCs are never auto-deletedScaling down and deleting leave storage behind. Clean-up is manual and unprompted.
Jobs are kept when finishedLogs survive for inspection, and Jobs pile up until ttlSecondsAfterFinished removes them.
CronJobs default to AllowSlow runs overlap silently. Set Forbid for anything touching shared state.
Schedules use the cluster's clockSet timeZone explicitly or your nightly job runs at someone else's night.

Cheat sheet

Part 6 · statefulsets, jobs, cronjobs

• Use a StatefulSet when pods need identity: a stable name, their own disk, or a defined start order.

• Names are ordinals — catalogue-0, -1, -2 — and they survive restarts.

• volumeClaimTemplates gives each pod its own PVC, bound to the ordinal, not the instance.

• Creation is 0→N, deletion is N→0, and each waits for the last. podManagementPolicy: Parallel opts out.

• A headless Service (clusterIP: None) gives per-pod DNS. StatefulSets require a serviceName.

• PVCs are never deleted automatically. Scale down, delete the set — the storage stays. Clean up by hand.

• --cascade=orphan lets you recreate a StatefulSet without the pods ever going down.

• A Job runs to success, retries to backoffLimit, and is kept afterwards. Use ttlSecondsAfterFinished.

• kubectl create job X --from=cronjob/Y runs a scheduled job right now. Learn this one.

• concurrencyPolicy defaults to Allow — usually wrong. Set timeZone too.