How to create a Kubernetes StatefulSet

Stateful workloads in Kubernetes need Pod identity that survives replacement more predictably than a Deployment can provide. A StatefulSet creates Pods with ordinal names such as web-0 and web-1, which fits applications that depend on stable peer names or one storage claim per replica.

A StatefulSet needs a matching Pod selector and a Service name that points at a headless Service. The Service has no cluster IP; it gives the controller a DNS domain for the StatefulSet Pods while each Pod keeps its own ordinal hostname.

The lab manifest uses a separate namespace, two busybox Pods, and a volumeClaimTemplates entry that creates one 1 GiB claim per Pod. Replace standard with a StorageClass from your cluster, and keep the cleanup step for test namespaces only because deleting the namespace removes the claims and dynamically provisioned volumes.

Steps to create a Kubernetes StatefulSet:

  1. List the available StorageClasses.
    $ kubectl get storageclass
    NAME                 PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
    standard (default)   rancher.io/local-path   Delete          WaitForFirstConsumer   false                  1s

    The manifest uses standard. If your cluster uses another class name, change storageClassName before applying the manifest.

  2. Create a namespace for the StatefulSet test.
    $ kubectl create namespace app-stateful
    namespace/app-stateful created

    Use the application namespace instead of app-stateful when creating a real StatefulSet.

  3. Save the headless Service and StatefulSet manifest.
    web-statefulset.yaml
    apiVersion: v1
    kind: Service
    metadata:
      name: web
    spec:
      clusterIP: None
      selector:
        app: web
      ports:
        - name: http
          port: 80
          targetPort: 80
    ---
    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      name: web
    spec:
      serviceName: web
      replicas: 2
      selector:
        matchLabels:
          app: web
      template:
        metadata:
          labels:
            app: web
        spec:
          containers:
            - name: web
              image: busybox:1.36
              command:
                - sh
                - -c
                - |
                  if [ ! -f /data/identity.txt ]; then hostname > /data/identity.txt; fi
                  sleep 3600
              ports:
                - containerPort: 80
                  name: http
              volumeMounts:
                - name: data
                  mountPath: /data
      volumeClaimTemplates:
        - metadata:
            name: data
          spec:
            accessModes:
              - ReadWriteOnce
            storageClassName: standard
            resources:
              requests:
                storage: 1Gi

    serviceName must match the headless Service name, and selector.matchLabels must match the Pod template labels.

  4. Apply the manifest to the namespace.
    $ kubectl apply -f web-statefulset.yaml --namespace app-stateful
    service/web created
    statefulset.apps/web created
  5. Wait for the StatefulSet rollout to finish.
    $ kubectl rollout status statefulset/web --namespace app-stateful --timeout=180s
    Waiting for statefulset spec update to be observed...
    Waiting for 2 pods to be ready...
    Waiting for 1 pods to be ready...
    partitioned roll out complete: 2 new pods have been updated...
  6. Check the StatefulSet Pod names.
    $ kubectl get pods --namespace app-stateful -l app=web
    NAME    READY   STATUS    RESTARTS   AGE
    web-0   1/1     Running   0          25s
    web-1   1/1     Running   0          5s

    The ordinal suffixes identify each replica. web-0 and web-1 keep those names when Kubernetes replaces the Pods.

  7. Check the per-Pod PersistentVolumeClaims.
    $ kubectl get pvc --namespace app-stateful
    NAME         STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
    data-web-0   Bound    pvc-5cb05dd1-a6b9-485f-a29e-69a0b741ba2e   1Gi        RWO            standard       <unset>                 25s
    data-web-1   Bound    pvc-09ee7941-e182-49e6-8b1b-1a7d2936c02a   1Gi        RWO            standard       <unset>                 5s

    The claim name combines the volumeClaimTemplates name and the Pod ordinal.

  8. Read the identity file from the first Pod.
    $ kubectl exec web-0 --namespace app-stateful -- cat /data/identity.txt
    web-0
  9. Delete web-0 to let the StatefulSet controller recreate it.
    $ kubectl delete pod web-0 --namespace app-stateful
    pod "web-0" deleted from app-stateful namespace

    Do this only in a test namespace or during a controlled maintenance window. Stateful applications may need quorum, leader, or replica-health checks before one Pod is removed.

  10. Wait for the replacement Pod to become ready.
    $ kubectl wait --for=condition=Ready pod/web-0 --namespace app-stateful --timeout=120s
    pod/web-0 condition met
  11. Read the identity file after replacement.
    $ kubectl exec web-0 --namespace app-stateful -- cat /data/identity.txt
    web-0

    The recreated Pod has the same ordinal name and remounts the claim associated with data-web-0.

  12. Delete the lab namespace when the StatefulSet test is finished.
    $ kubectl delete namespace app-stateful
    namespace "app-stateful" deleted

    Skip this command for a real application namespace. Deleting the namespace removes the StatefulSet, Pods, Services, claims, and dynamically provisioned volumes inside it.