Chapter 38 of 47 · More workload types

StatefulSets: apps with identity and data

A Deployment treats its Pods like identical cups from a stack: take any one, throw away a broken one, grab a new one. That works for posts and comments. A database is different. Each copy has its own data and its own job (primary, replica), so each one needs a fixed name and its own disk. A StatefulSet gives Pods exactly that.

Why a Deployment is not enough

Try running PostgreSQL with 3 replicas in a Deployment and three things go wrong:

Random names

Pods are called db-7d9f8-x2kqp, and a replacement gets a new random name. Replicas need to say "I follow db-0", and that name must not change.

One shared disk

All replicas share one PVC (chapter 31). Three databases writing the same files corrupt each other. Each needs its own disk.

All start at once

Replicas must start after the primary exists, so they have something to copy from.

Any Pod may be killed

When scaling down, a Deployment may remove the primary. You want the newest replica to go first.

The four promises

A StatefulSet with three Pods, each with its own diskStatefulSet db creates db-0, db-1 and db-2 in order. Each Pod has a stable DNS name through the headless Service and its own PersistentVolumeClaim, data-db-0, data-db-1 and data-db-2. StatefulSet db 1st2nd3rd db-0primary db-1replica db-2replica data-db-0 data-db-1 data-db-2 DNS: db-0.db-headless, db-1.db-headless, db-2.db-headless
  1. Stable name: Pods are db-0, db-1, db-2. A replacement for db-1 is called db-1 again.
  2. Stable network address: with a headless Service, each Pod gets its own DNS name, db-1.db-headless (chapter 26).
  3. Its own disk, forever: each Pod gets its own PVC, data-db-1, and always gets that same one back, even on another node.
  4. Order: started 0, 1, 2 one after another; stopped 2, 1, 0.

Try it: scale, delete, scale again

Scale up and watch the Pods start one by one. Delete a Pod and see it come back with the same name and disk. Scale down, then up again, and watch the old disks get reused. Then switch to Parallel and compare.

The YAML

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: db
spec:
  serviceName: db-headless
  replicas: 3
  podManagementPolicy: OrderedReady
  selector:
    matchLabels: { app: db }
  template:
    metadata:
      labels: { app: db }
    spec:
      containers:
        - name: postgres
          image: postgres:16
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: [ReadWriteOnce]
        resources:
          requests:
            storage: 10Gi
  persistentVolumeClaimRetentionPolicy:
    whenScaled: Retain
    whenDeleted: Retain

serviceName

The headless Service (clusterIP: None) that gives each Pod its own DNS name. You create it yourself, next to the StatefulSet.

$ kubectl get pods,pvc -l app=db
NAME       READY   STATUS
pod/db-0   1/1     Running
pod/db-1   1/1     Running
pod/db-2   1/1     Running

NAME                              STATUS   CAPACITY
persistentvolumeclaim/data-db-0   Bound    10Gi
persistentvolumeclaim/data-db-1   Bound    10Gi
persistentvolumeclaim/data-db-2   Bound    10Gi

Order of start and stop

  1. db-0  Pending → Running → Ready ✓

    Only db-0 starts. It becomes the primary.

  2. db-0  Ready
    db-1  Pending → Running → Ready ✓

    db-1 starts only after db-0 is Ready, so it can connect to db-0.db-headless and copy its data.

  3. db-0  Ready
    db-1  Ready
    db-2  Pending → Running → Ready ✓

    Then db-2. If db-1 never became Ready, db-2 would wait forever, which is a feature: you notice the problem before it spreads.

  1. db-2  Terminating

    The highest number goes first, the newest replica.

  2. db-1  Terminating
    db-0  Running      ← the primary is never touched first

    Then db-1. db-0, the oldest and usually the primary, stays.

Updates and staged rollouts

Change the image and apply. A StatefulSet updates from the highest number down: db-2, then db-1, then db-0, each only after the previous is Ready again. That way the primary is updated last.

To test a new version on just a few Pods first, use a partition: only Pods with a number greater than or equal to the partition get the new version.

updateStrategy:
  type: RollingUpdate
  rollingUpdate:
    partition: 2

db-0  postgres:16      (old)
db-1  postgres:16      (old)
db-2  postgres:16.4    ← only this one

A canary: one replica runs the new version. Watch it for a while.

The other strategy, OnDelete, waits for you to delete each Pod by hand, like for DaemonSets (chapter 37).

Scaling down safely

What a StatefulSet does not do

A StatefulSet gives names, disks and order. It knows nothing about databases. It does not:

The database does the first two itself, and the rest is your job, or an operator's: a controller that knows how to run one specific database (chapter 46). In practice, most people install PostgreSQL, MySQL or Kafka with a Helm chart or an operator, which creates the StatefulSet for them. Or they use a managed cloud database and keep the cluster stateless.

StatefulSet or Deployment?

Recap

Quick check

You scale db from 3 to 2, and a week later back to 3. Does db-2 start empty?
Show answer

No. Its PVC data-db-2 was kept (retention policy Retain), so the new db-2 gets the old disk back with last week's data. The database must be able to catch up from there, or you delete the PVC before scaling up.

StatefulSets: common questions

Are StatefulSet volumes deleted when I scale down?

Not by default. The PVCs are kept, and a Pod with the same number gets its old disk back when you scale up again. persistentVolumeClaimRetentionPolicy can change this.