K8s by Example: StatefulSets

StatefulSets provide stable Pod identities and persistent storage. Unlike Deployments, Pods are not interchangeable and keep the same name, DNS, and storage across restarts. Use for: databases, caches, Kafka.

statefulset.yaml

StatefulSet requires serviceName pointing to a headless Service. Pods get ordinal names: postgres-0, postgres-1, postgres-2.

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres
  replicas: 3
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:15
          ports:
            - containerPort: 5432
headless-service.yaml

Headless Service (clusterIP: None) provides stable DNS for each Pod. DNS entries: postgres-0.postgres.default.svc.cluster.local, postgres-1.postgres.default.svc.cluster.local. This is how Pods discover each other for clustering.

apiVersion: v1
kind: Service
metadata:
  name: postgres
spec:
  clusterIP: None
  selector:
    app: postgres
  ports:
    - port: 5432
statefulset-storage.yaml

Each Pod gets a persistent volume via volumeClaimTemplates. PVCs are named volumename-podname: data-postgres-0, data-postgres-1, data-postgres-2. Volumes survive Pod restarts and rescheduling.

spec:
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: gp3
        resources:
          requests:
            storage: 10Gi
  template:
    spec:
      containers:
        - name: postgres
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
statefulset-policy.yaml

Pod management policies control startup order. OrderedReady (default) starts Pods sequentially (postgres-0 → postgres-1 → postgres-2) and stops in reverse. Parallel starts/stops all simultaneously.

spec:
  podManagementPolicy: OrderedReady
  replicas: 3
statefulset-update.yaml

Update strategies: RollingUpdate (default) updates Pods in reverse order (N-1 → 0). OnDelete only updates when Pod is manually deleted. partition: 1 means postgres-0 stays on old version while postgres-1, postgres-2 get new version. Set partition: 0 to complete rollout.

spec:
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      partition: 1
statefulset-retention.yaml

PVCs persist after StatefulSet deletion by default. whenDeleted: Delete deletes PVCs when StatefulSet deleted. whenScaled: Retain (default) keeps PVCs on scale down. Delete manually to reclaim storage.

spec:
  persistentVolumeClaimRetentionPolicy:
    whenDeleted: Delete
    whenScaled: Retain
terminal

Scale up creates postgres-3, postgres-4 in order. Scale down removes postgres-4, postgres-3 in reverse and waits for each to terminate. PVCs are NOT deleted on scale down, so data-postgres-3 and data-postgres-4 still exist.

$ kubectl scale statefulset postgres --replicas=5
statefulset.apps/postgres scaled

$ kubectl scale statefulset postgres --replicas=3
statefulset.apps/postgres scaled

$ kubectl get pvc
NAME              STATUS   VOLUME       CAPACITY
data-postgres-0   Bound    pv-abc123    10Gi
data-postgres-1   Bound    pv-def456    10Gi
data-postgres-2   Bound    pv-ghi789    10Gi
data-postgres-3   Bound    pv-jkl012    10Gi
data-postgres-4   Bound    pv-mno345    10Gi
terminal

Debug StatefulSets by checking Pod ordering, PVC binding, and headless Service DNS. Common issues: PVC stuck pending (no storage), Pod stuck initializing (previous Pod not ready).

$ kubectl get pods -l app=postgres -w
NAME         READY   STATUS    AGE
postgres-0   1/1     Running   5m
postgres-1   1/1     Running   4m
postgres-2   1/1     Running   3m

$ kubectl get pvc -l app=postgres
NAME              STATUS   VOLUME
data-postgres-0   Bound    pvc-abc123
data-postgres-1   Bound    pvc-def456
data-postgres-2   Bound    pvc-ghi789

$ kubectl run -it --rm debug --image=busybox -- \
    nslookup postgres-0.postgres.default.svc.cluster.local
Name:      postgres-0.postgres.default.svc.cluster.local
Address 1: 10.244.0.15

$ kubectl describe statefulset postgres

Index | GitHub | Use arrow keys to navigate |