StatefulSets and volumeClaimTemplates

How a StatefulSet gives each replica a stable name, a DNS entry and its own PersistentVolumeClaim, and what happens to those claims when you scale or delete it.

Storage · 10%30 min2 tasks

After this lesson

  • 01You can write a StatefulSet with a headless Service and a volumeClaimTemplate and predict the names of the Pods and claims it creates.
  • 02You can scale a StatefulSet and explain which claims are created, kept or deleted under each retention policy.
  • 03You can choose between a Deployment and a StatefulSet for a given workload and say why.

StatefulSets are where the three storage competencies meet: each replica gets a claim through a volumeClaimTemplate, the claim is satisfied by dynamic provisioning from a StorageClass, and the PV and PVC life cycle you learned in the last two lessons plays out once per replica. The exam uses them for "create a StatefulSet with N replicas and persistent storage" and for "scale it and check the volumes". Both are fast if you know the naming rules, because the grader checks names.

StatefulSet versus Deployment

DeploymentStatefulSet
Pod namesRandom suffix (web-7d9c8-x4k2p)Ordinal (web-0, web-1, ...) that survives rescheduling
Network identityOnly through a ServiceEach Pod has its own DNS name via a headless Service
StorageAll replicas share the Pod template's volumesEach replica gets its own claim from volumeClaimTemplates and gets it back after a restart
Creation and deletionAll at onceIn order, 0 upward; removed highest ordinal first
UpdatesReplicaSets, surge and unavailable budgetsIn-place rolling update from the highest ordinal down, optionally partitioned
Use it forStateless services, anything behind a load balancerDatabases, queues, anything where "which replica am I" matters

Stable identity: the headless Service

A StatefulSet names a Service in spec.serviceName. Make it headless (clusterIP: None) and the DNS for each Pod becomes <pod>.<service>.<namespace>.svc.cluster.local, for example blog-0.blog.st-sts.svc.cluster.local, because the StatefulSet sets each Pod's hostname to its name and subdomain to the Service name. The Service itself does not need to exist for the Pods to start, only for those names to resolve, and its selector must match the Pod labels like any other Service. Clients that need "the primary" connect to blog-0 by name; clients that want any replica use the Service name, which returns every ready Pod IP.

A headless Service and a two-replica StatefulSet with per-Pod storageyaml
apiVersion: v1
kind: Service
metadata:
  name: blog
  namespace: st-sts
spec:
  clusterIP: None
  selector:
    app: blog
  ports:
    - name: http
      port: 80
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: blog
  namespace: st-sts
spec:
  serviceName: blog
  replicas: 2
  selector:
    matchLabels:
      app: blog
  template:
    metadata:
      labels:
        app: blog
    spec:
      containers:
        - name: web
          image: nginx:1.27
          ports:
            - containerPort: 80
              name: http
          volumeMounts:
            - name: content
              mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
    - metadata:
        name: content
      spec:
        accessModes:
          - ReadWriteOnce
        storageClassName: standard
        resources:
          requests:
            storage: 100Mi

Ordered creation and podManagementPolicy

With the default podManagementPolicy: OrderedReady, Pod 0 is created and must be Running and Ready before Pod 1 starts; scaling down removes the highest ordinal first and waits for it to terminate before the next. That is what a database that elects a leader wants. Parallel launches and terminates all Pods at once and only affects scaling, not updates. Either way a Pod that is deleted comes back with the same name, the same claim and the same DNS entry, which is the whole promise.

volumeClaimTemplates and claim names

Each entry in volumeClaimTemplates is a PVC spec without a namespace. For every Pod the controller creates one claim per template named <template>-<statefulset>-<ordinal>: template content on StatefulSet blog gives content-blog-0, content-blog-1, and so on. The Pod template's volumeMounts refer to the template name as if it were a volume, so there is no volumes: entry for it. The claims are ordinary PVCs: the StorageClass provisions them, kubectl get pvc lists them, and they are bound to the Pod with that ordinal forever. volumeClaimTemplates cannot be changed on an existing StatefulSet; only replicas, ordinals, template, updateStrategy, minReadySeconds and persistentVolumeClaimRetentionPolicy are mutable. To change the template, delete the StatefulSet (with --cascade=orphan if the Pods must keep running) and recreate it.

Create, inspect and scalebash
kubectl apply -f blog.yaml
kubectl -n st-sts rollout status sts/blog --timeout=180s
kubectl -n st-sts get pods -l app=blog -o wide          # blog-0, blog-1
kubectl -n st-sts get pvc                                # content-blog-0, content-blog-1, both Bound
kubectl -n st-sts scale sts blog --replicas=3           # creates content-blog-2 then blog-2
kubectl -n st-sts get sts blog -o jsonpath='{.spec.persistentVolumeClaimRetentionPolicy}'

What happens to the claims when you scale down or delete

By default, nothing. Scale from 3 to 2 and content-blog-2 stays Bound with its data, so scaling back up gives blog-2 its old disk back. Delete the StatefulSet and every claim survives, which is why a cluster accumulates orphaned claims after test runs. spec.persistentVolumeClaimRetentionPolicy (stable since v1.32) changes that per case: whenScaled and whenDeleted each take Retain (the default) or Delete. With whenScaled: Delete the claim for a removed ordinal is deleted once its Pod is gone; with whenDeleted: Delete all claims go with the StatefulSet. Whether the PV and the data then disappear is still the PV's reclaim policy, which the StorageClass set. The field is mutable, so you can add it to a running StatefulSet with a patch.

Set a retention policy on a running StatefulSetbash
kubectl -n st-sts patch sts blog -p '{"spec":{"persistentVolumeClaimRetentionPolicy":{"whenScaled":"Delete","whenDeleted":"Retain"}}}'
kubectl -n st-sts scale sts blog --replicas=2
kubectl -n st-sts get pvc                                # content-blog-2 is gone after blog-2 terminates
# Cleaning up by hand when the policy is Retain:
kubectl -n st-sts delete sts blog
kubectl -n st-sts delete pvc -l app=blog                 # claims inherit the selector labels

updateStrategy and partition

updateStrategy.type is RollingUpdate by default: after a change to spec.template, Pods are replaced one at a time from the highest ordinal to the lowest, each waiting for the previous one to be Ready. OnDelete changes nothing until you delete a Pod yourself, which is how you control exactly when each replica moves. rollingUpdate.partition: N is the canary knob: only Pods with ordinal N or higher are updated, the rest keep the old template until you lower the partition to 0. Because the claims are bound to ordinals, none of this touches storage; blog-1 comes back on the new image with content-blog-1 still mounted.

Canary one replica, then finish the rolloutbash
kubectl -n st-sts patch sts blog -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":1}}}}'
kubectl -n st-sts set image sts/blog web=nginx:1.26     # only blog-1 (ordinal >= 1) is replaced
kubectl -n st-sts get pods -l app=blog -o custom-columns=NAME:.metadata.name,IMAGE:.spec.containers[0].image
kubectl -n st-sts patch sts blog -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":0}}}}'
kubectl -n st-sts rollout status sts/blog --timeout=180s

In the docs · the tab you get on exam day

Taskst-sts-1
Storage6%~9 min

context · kubectl config use-context kind-cka

A StatefulSet with per-replica storage

In namespace st-sts create a headless Service named blog on port 80 with selector app=blog, and a StatefulSet named blog with 2 replicas of image nginx:1.27 that uses that Service for its network identity. Every replica must get its own 100Mi ReadWriteOnce volume from the StorageClass standard through a volumeClaimTemplate named content, mounted at /usr/share/nginx/html. Both Pods must be Running and both claims Bound.
  1. 01

    Set up the scenario

    curl -fsSL https://codewithmuh.com/labs/cka/t/st-sts-1/setup | bash
  2. 02

    Solve it in your terminal

    Work the way the exam makes you: kubectl, the docs, nothing else. Put any answer files under /tmp/cka/.

  3. 03

    Check your work

    curl -fsSL https://codewithmuh.com/labs/cka/t/st-sts-1/verify | bash
    Counts toward your readiness score.
  4. 04

    Clean up

    curl -fsSL https://codewithmuh.com/labs/cka/t/st-sts-1/cleanup | bash
Taskst-sts-2
Storage4%~6 min

context · kubectl config use-context kind-cka

Scale a StatefulSet and control what happens to its claims

The StatefulSet blog in namespace st-sts runs 2 replicas, each with its own volume. Scale it to 3 replicas and make sure the new replica gets its own claim. Also configure the StatefulSet so that from now on the claim of a replica is deleted when the StatefulSet is scaled down, but all claims are kept if the StatefulSet itself is deleted. All 3 Pods must be Running when you finish.
  1. 01

    Set up the scenario

    curl -fsSL https://codewithmuh.com/labs/cka/t/st-sts-2/setup | bash
  2. 02

    Solve it in your terminal

    Work the way the exam makes you: kubectl, the docs, nothing else. Put any answer files under /tmp/cka/.

  3. 03

    Check your work

    curl -fsSL https://codewithmuh.com/labs/cka/t/st-sts-2/verify | bash
    Counts toward your readiness score.
  4. 04

    Clean up

    curl -fsSL https://codewithmuh.com/labs/cka/t/st-sts-2/cleanup | bash