Lesson  in  Kubernetes 101

StatefulSet

When replicas are not interchangeable: stable identity, a PersistentVolumeClaim of its own per replica and a headless Service that gives each Pod a DNS name.

Everything you have deployed with Deployments shares a silent premise: the replicas are interchangeable. It does not matter which Pod serves the request, it does not matter in which order they start, it does not matter which one dies. That is why their names are random suffixes (web-7d4b9c8f6d-x2klp) and why they all share the same volume or none.

A database does not work like that. The primary node is not interchangeable with a replica. The disk of db-0 holds the data of db-0 and nobody else's. And the startup order matters, because the second node needs to know whom to replicate from.

The StatefulSet is the object that gives those three guarantees: stable identity, its own storage per replica and order.

Work from the dev-machine tab.

Step 1: The headless Service and the StatefulSet

Create statefulset.yaml with the two pieces, which are inseparable:

cat << 'EOF' > statefulset.yaml
apiVersion: v1
kind: Service
metadata:
  name: db
spec:
  clusterIP: None
  selector:
    app: db
  ports:
  - name: http
    port: 80
    targetPort: 80
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: db
spec:
  serviceName: db
  replicas: 2
  selector:
    matchLabels:
      app: db
  template:
    metadata:
      labels:
        app: db
    spec:
      containers:
      - name: db
        image: ghcr.io/iximiuz/labs/nginx:alpine
        command: ["sh", "-c"]
        args:
        - |
          [ -f /data/identidad.txt ] || echo "$(hostname)-$(date +%s)" > /data/identidad.txt
          exec nginx -g 'daemon off;'
        ports:
        - containerPort: 80
        volumeMounts:
        - name: db
          mountPath: /data
  volumeClaimTemplates:
  - metadata:
      name: db
    spec:
      accessModes:
      - ReadWriteOnce
      storageClassName: local-path
      resources:
        requests:
          storage: 100Mi
EOF

The command writes a unique identity to the disk (/data/identidad.txt, "identidad" meaning identity) only if it does not exist yet. That file is the evidence we will use at the end.

The YAML, explained in questions and answers

What is a Service with clusterIP: None?

A headless Service: it gives up the virtual IP and the load balancing. Instead of resolving a name to a single IP that spreads traffic, the DNS returns the IPs of all the Pods, and on top of that it gives each Pod its own name: db-0.db.tienda.svc.cluster.local. It is exactly the opposite of what you wanted with a Deployment (not caring whom you talk to) and exactly what you need here (talking to a specific one).

What is serviceName for?

It tells the StatefulSet which headless Service governs the DNS domain of its Pods. Without it there are no stable names per replica. It is a field that is often forgotten, and whose symptom is a DNS that does not resolve.

What is volumeClaimTemplates and how does it differ from a normal volumes?

It is a PVC factory: for each replica, the StatefulSet creates its own PersistentVolumeClaim following this template. The result is db-db-0 and db-db-1, each with its volume and its data. Compare it with a Deployment whose Pods mounted the same PVC: there they would all write to the same disk, which is exactly what a database does not want.

Are the Pod names still random?

No, and that is the first guarantee: they are ordinal and predictable (db-0, db-1, db-2). If db-0 dies, another Pod called db-0 comes back, with the same DNS name and mounting the same PVC. A Deployment will never give you that.

What does the order guarantee?

That db-1 does not start until db-0 is Ready, and that when scaling down the highest ordinal is removed first. It is controlled with podManagementPolicy: the default value OrderedReady is the one I have just described; Parallel turns it off when your application does not need it and you want fast startups.

Apply it and watch the startup, which is unlike anything you have seen:

kubectl apply -f statefulset.yaml
kubectl get pods -l app=db --watch

First db-0, and only when it is Ready, db-1. Nothing like a Deployment's three at once. When they finish, look at the infrastructure that has appeared on its own:

kubectl get statefulset,pods,pvc,pv -l app=db
kubectl get pvc

Two Pods with their own names, two PVCs with derived names (db-db-0, db-db-1) and two PersistentVolumes provisioned by local-path.

Step 2: DNS per replica

Launch a client and ask the DNS about each Pod, by name:

kubectl run cliente --image=ghcr.io/iximiuz/labs/nginx:alpine --command -- sleep 100000
kubectl wait --for=condition=Ready pod/cliente --timeout=60s
kubectl exec cliente -- nslookup db-0.db.tienda.svc.cluster.local
kubectl exec cliente -- nslookup db.tienda.svc.cluster.local

The first query returns the IP of one specific Pod. The second, that of all the Pods of the Service (that is what headless means: with no virtual IP, the Service name returns the full list). Compare it with the web of the Exposing the application with a Service lesson, which returned a single virtual IP that load balanced behind the scenes.

This is the piece that makes a database cluster possible: each node can refer to its peers by a name that never changes, even if the Pods are recreated and change IP.

Step 3: The identity test

Write down what db-0 wrote to its disk:

kubectl exec db-0 -- cat /data/identidad.txt

And now kill it:

kubectl delete pod db-0
kubectl get pods -l app=db --watch

Watch calmly what happens. A Pod comes back that is called db-0 again (not db-9f8c7, as would have happened with a Deployment), and as soon as it is up:

kubectl exec db-0 -- cat /data/identidad.txt

The same content as before. The Pod is new (another UID, another IP), but its identity and its disk have survived: the StatefulSet mounted the PVC datos-db-0 for it again, which never got deleted. The command did not rewrite the file because it already existed.

Note

💡 The PVCs of a StatefulSet are not deleted when scaling down or when deleting the StatefulSet (unless you configure persistentVolumeClaimRetentionPolicy). It is deliberate and sometimes puzzling: you delete the StatefulSet, create it again, and the old data shows up. Caution by design, because throwing away a database's data by accident is a mistake that cannot be undone.

Summary

  • StatefulSet when the replicas are not interchangeable: databases, queues, consensus systems.
  • Three guarantees: stable identity (db-0), its own PVC per replica (volumeClaimTemplates) and startup and shutdown order.
  • The headless Service (clusterIP: None) gives each Pod its DNS name; serviceName ties them together.
  • The PVCs outlive the Pod, the scaling and even the StatefulSet itself.
  • Practical rule: if you hesitate between Deployment and StatefulSet, it is a Deployment. The StatefulSet is chosen when the application forces you to.
Previous lesson
Deployment and lifecycle
Next lesson
DaemonSet