Run Stateful Applications with Persistent Storage

KubernetesBeginner
Practice Now

Introduction

Deployments treat replicas as interchangeable. Stateful applications often require stable Pod names, predictable startup order, and storage that survives Pod replacement. StatefulSets combine those identities with per-Pod claims.

You will inspect dynamic storage, deploy a two-replica StatefulSet, write data, replace a Pod, and prove that identity and data survive.

Track three identities separately: the StatefulSet controls desired replicas, each Pod receives a stable ordinal such as -0, and each ordinal receives its own PersistentVolumeClaim. The Pod can be replaced while the claim and stored data remain.

Inspect Dynamic Storage

In this step, inspect the cluster's StorageClass before requesting storage.

A PersistentVolumeClaim describes what a workload needs; a StorageClass describes how the cluster can provision it. Checking available classes first prevents writing a claim that can never bind.

cd /home/labex/project/stateful-storage
kubectl get storageclasses
kubectl get storageclass -o custom-columns='NAME:.metadata.name,PROVISIONER:.provisioner,DEFAULT:.metadata.annotations.storageclass\.kubernetes\.io/is-default-class'

The default StorageClass dynamically creates a PersistentVolume when a claim omits storageClassName. This avoids node-specific hostPath configuration in learner manifests.

Look for (default) in the normal table or true in the custom DEFAULT column. The provisioner name is implementation detail for this environment; the important fact is that a default class exists.

In this environment, the result is:

NAME                 PROVISIONER                ...
standard (default)   k8s.io/minikube-hostpath   ...

NAME       PROVISIONER                DEFAULT
standard   k8s.io/minikube-hostpath   true

standard is therefore the default class. Because the StatefulSet will omit storageClassName, Kubernetes will choose this class automatically.

Create a StatefulSet and Headless Service

In this step, create a headless Service for stable DNS and a StatefulSet with one claim per replica.

This manifest introduces several connected fields. Read them before applying:

  • clusterIP: None makes the Service headless, so DNS can represent individual Pods.
  • serviceName: stateful-web links the StatefulSet to that Service.
  • volumeClaimTemplates creates one claim named from the template and Pod ordinal.
  • volumeMounts places each Pod's own claim at NGINX's document directory.

The labels still perform the familiar Service-to-Pod selection learned in the first course.

cd /home/labex/project/stateful-storage
cat <<'EOF' > stateful-web.yaml
apiVersion: v1
kind: Namespace
metadata: {name: practice-stateful}
---
apiVersion: v1
kind: Service
metadata: {name: stateful-web, namespace: practice-stateful}
spec:
  clusterIP: None
  selector: {app: stateful-web}
  ports: [{name: http, port: 80, targetPort: http}]
---
apiVersion: apps/v1
kind: StatefulSet
metadata: {name: stateful-web, namespace: practice-stateful}
spec:
  serviceName: stateful-web
  replicas: 2
  selector: {matchLabels: {app: stateful-web}}
  template:
    metadata: {labels: {app: stateful-web}}
    spec:
      containers:
        - name: web
          image: nginx:1.27-alpine
          imagePullPolicy: IfNotPresent
          ports: [{name: http, containerPort: 80}]
          volumeMounts: [{name: web-data, mountPath: /usr/share/nginx/html}]
  volumeClaimTemplates:
    - metadata: {name: web-data}
      spec:
        accessModes: [ReadWriteOnce]
        resources: {requests: {storage: 100Mi}}
EOF

Apply the manifest and wait for ordered rollout. StatefulSets normally create ordinal 0 before ordinal 1, so the second Pod may appear a little later:

kubectl apply -f stateful-web.yaml
kubectl rollout status statefulset/stateful-web -n practice-stateful --timeout=90s
kubectl get pods,pvc -n practice-stateful

Pod names use stable ordinals: stateful-web-0 and stateful-web-1. Each claim includes the template name, Pod name, and ordinal.

You should see claims web-data-stateful-web-0 and web-data-stateful-web-1, both Bound. Bound means Kubernetes found or dynamically provisioned storage satisfying each request.

The rollout messages make the ordered behavior visible, followed by the two claims:

Waiting for 2 pods to be ready...
Waiting for 1 pods to be ready...
partitioned roll out complete: 2 new pods have been updated...

pod/stateful-web-0   1/1   Running   ...
pod/stateful-web-1   1/1   Running   ...

persistentvolumeclaim/web-data-stateful-web-0   Bound   ...   100Mi   RWO   standard
persistentvolumeclaim/web-data-stateful-web-1   Bound   ...   100Mi   RWO   standard

The generated volume IDs differ on every run. Focus on the stable claim names, Bound status, requested capacity, and storage class.

Write Pod-Specific Persistent Data

In this step, write different content to each Pod's mounted claim.

kubectl exec ... -- sh -c is used because output redirection (>) must run inside the container. Without sh -c, your local shell would try to redirect output on the VM instead of inside the Pod.

cd /home/labex/project/stateful-storage
kubectl exec stateful-web-0 -n practice-stateful -- sh -c 'echo persistent-zero > /usr/share/nginx/html/index.html'
kubectl exec stateful-web-1 -n practice-stateful -- sh -c 'echo persistent-one > /usr/share/nginx/html/index.html'
kubectl exec stateful-web-0 -n practice-stateful -- cat /usr/share/nginx/html/index.html
kubectl exec stateful-web-1 -n practice-stateful -- cat /usr/share/nginx/html/index.html
kubectl get pods -n practice-stateful -o custom-columns='POD:.metadata.name,HOSTNAME:.spec.hostname,CLAIM:.spec.volumes[0].persistentVolumeClaim.claimName'

The Pods run the same image but attach different claims, so their data is not interchangeable.

The two cat commands should return different strings. The custom table then links each stable Pod name to its claim, turning the storage relationship into visible evidence.

persistent-zero
persistent-one
POD              HOSTNAME         CLAIM
stateful-web-0   stateful-web-0   web-data-stateful-web-0
stateful-web-1   stateful-web-1   web-data-stateful-web-1

The ordinal appears in all three columns. That repeated 0 or 1 is the identity that connects a Pod to its own storage.

Replace a Pod and Recover Its Data

In this step, delete ordinal 0 and let the StatefulSet recreate the same identity.

Deleting a managed Pod is not the same as reducing desired replicas. The StatefulSet still requires two Pods, so it creates a replacement named stateful-web-0 and reattaches the claim reserved for that ordinal.

cd /home/labex/project/stateful-storage
kubectl delete pod stateful-web-0 -n practice-stateful --wait=true
kubectl wait --for=condition=Ready pod/stateful-web-0 -n practice-stateful --timeout=90s
kubectl exec stateful-web-0 -n practice-stateful -- cat /usr/share/nginx/html/index.html
kubectl get pod stateful-web-0 -n practice-stateful -o jsonpath='pod={.metadata.name} claim={.spec.volumes[0].persistentVolumeClaim.claimName}{"\n"}'

The Pod object is new, but its name and claim association are stable. The recovered file proves the data belonged to the PersistentVolume rather than the deleted container filesystem.

pod "stateful-web-0" deleted from practice-stateful namespace
pod/stateful-web-0 condition met
persistent-zero
pod=stateful-web-0 claim=web-data-stateful-web-0

The important line is persistent-zero: it was written before deletion and read from the replacement Pod.

If the file had been written outside the mounted directory, it would have disappeared with the old container filesystem. Persistence depends on the mount path, not on the StatefulSet name alone.

Summary

You connected the default StorageClass, dynamic PVC provisioning, a headless Service, stable StatefulSet ordinals, and per-Pod storage. Replacing a Pod preserved both its identity and data.