StatefulSets

StatefulSets are designed for stateful applications that need stable network identity, stable persistent storage, and ordered deployment and scaling. Unlike Deployments, where pods are interchangeable, StatefulSet pods have unique identities that persist across rescheduling.

What Are StatefulSets?

A StatefulSet manages pods that have stable, unique network identities and stable storage. Each pod in a StatefulSet gets a persistent identifier that is maintained even if the pod is rescheduled to a different node. This makes StatefulSets ideal for databases, message queues, and other distributed systems that require stable identity and persistent data.

graph TB A[StatefulSet: mysql] --> B[mysql-0] A --> C[mysql-1] A --> D[mysql-2] B --> E[Stable Name: mysql-0] B --> F[Stable Storage: data-mysql-0] B --> G[Stable Network: mysql-0.mysql] C --> H[Stable Name: mysql-1] C --> I[Stable Storage: data-mysql-1] C --> J[Stable Network: mysql-1.mysql] style A fill:#e1f5ff style B fill:#fff4e1 style E fill:#e8f5e9 style F fill:#e8f5e9 style G fill:#e8f5e9

Why Use StatefulSets?

StatefulSets provide features essential for stateful applications:

Stable pod identity - Each pod has a persistent, unique name
Stable network identity - Predictable hostnames and DNS entries
Stable storage - Each pod gets its own persistent volume
Ordered deployment - Pods created and terminated in order
Ordered scaling - Scale up and down in a controlled sequence
Graceful shutdown - Pods terminated in reverse order

StatefulSet vs Deployment

The key difference is how pods are identified and managed:

graph TB subgraph deployment[Deployment Pods] A[Pod: nginx-abc123] B[Pod: nginx-def456] C[Pod: nginx-ghi789] A -.->|Interchangeable| D[Any pod can replace any other] B -.-> D C -.-> D end subgraph statefulset[StatefulSet Pods] E[Pod: mysql-0] F[Pod: mysql-1] G[Pod: mysql-2] E -.->|Unique Identity| H[Each pod has unique role] F -.-> H G -.-> H E --> I[Storage: data-mysql-0] F --> J[Storage: data-mysql-1] G --> K[Storage: data-mysql-2] end style A fill:#fff4e1 style E fill:#e8f5e9 style I fill:#e1f5ff

Use Deployments when:

  • Pods are stateless and interchangeable
  • Any pod can handle any request
  • Storage can be shared or ephemeral

Use StatefulSets when:

  • Pods have unique identity and roles
  • Each pod needs its own persistent storage
  • Pods must start/stop in order
  • Stable network identity is required

Stable Pod Identity

Each pod in a StatefulSet gets a stable name based on the StatefulSet name and an ordinal index:

<statefulset-name>-<ordinal>

Example: A StatefulSet named mysql with 3 replicas creates:

  • mysql-0
  • mysql-1
  • mysql-2

These names persist even if pods are deleted and recreated.

graph LR A[StatefulSet: mysql<br/>replicas: 3] --> B[mysql-0] A --> C[mysql-1] A --> D[mysql-2] E[Delete mysql-1] --> F[Recreates mysql-1] F --> G[Same name, same storage] style A fill:#e1f5ff style F fill:#fff4e1 style G fill:#e8f5e9

Stable Network Identity

StatefulSets create stable network identities using headless Services. Each pod gets a stable DNS name:

<pod-name>.<service-name>.<namespace>.svc.cluster.local

For a pod mysql-0 in a StatefulSet with service mysql:

mysql-0.mysql.default.svc.cluster.local

This allows applications to discover and connect to specific pod instances reliably.

graph TB A[Client Application] --> B[Headless Service: mysql] B --> C[mysql-0.mysql] B --> D[mysql-1.mysql] B --> E[mysql-2.mysql] C --> F[Pod: mysql-0] D --> G[Pod: mysql-1] E --> H[Pod: mysql-2] style A fill:#e1f5ff style B fill:#fff4e1 style C fill:#e8f5e9

Stable Storage

Each pod in a StatefulSet gets its own PersistentVolumeClaim (PVC). The PVC is created automatically with a stable name:

<volume-claim-template-name>-<statefulset-name>-<ordinal>

Example: For a volumeClaimTemplate named data in a StatefulSet mysql:

  • data-mysql-0
  • data-mysql-1
  • data-mysql-2

When a pod is deleted and recreated, it reattaches to the same PVC, preserving data.

graph TD A[StatefulSet: mysql] --> B[Creates PVCs] B --> C[data-mysql-0] B --> D[data-mysql-1] B --> E[data-mysql-2] C --> F[Pod: mysql-0] D --> G[Pod: mysql-1] E --> H[Pod: mysql-2] I[Delete mysql-1] --> J[Pod Terminated] J --> K[Recreate mysql-1] K --> D style A fill:#e1f5ff style C fill:#fff4e1 style D fill:#fff4e1 style K fill:#e8f5e9

Ordered Deployment and Scaling

StatefulSets deploy and scale pods in a strict order:

Ordered Deployment

Pods are created sequentially, starting from index 0:

  1. mysql-0 starts and becomes Ready
  2. mysql-1 starts and becomes Ready
  3. mysql-2 starts and becomes Ready

Ordered Scaling

Scaling Up:

  • Pods are created in order (0, 1, 2, …)

Scaling Down:

  • Pods are terminated in reverse order (2, 1, 0)
  • Each pod must be fully terminated before the next one starts terminating
graph TD A[Scale Up: 2 -> 3] --> B[Create mysql-2] B --> C[Wait for mysql-2 Ready] C --> D[Scaling Complete] E[Scale Down: 3 -> 2] --> F[Terminate mysql-2] F --> G[Wait for mysql-2 Terminated] G --> H[Scaling Complete] I[Scale Down: 2 -> 1] --> J[Terminate mysql-1] J --> K[Wait for mysql-1 Terminated] K --> L[Scaling Complete] style B fill:#fff4e1 style F fill:#ffe1e1 style J fill:#ffe1e1 style D fill:#e8f5e9 style H fill:#e8f5e9 style L fill:#e8f5e9

Basic StatefulSet Example

Here’s a StatefulSet running MySQL with persistent storage:

apiVersion: v1
kind: Service
metadata:
  name: mysql
  labels:
    app: mysql
spec:
  ports:
  - port: 3306
    name: mysql
  clusterIP: None  # Headless service
  selector:
    app: mysql
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  serviceName: mysql  # Must match Service name
  replicas: 3
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:8.0
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secret
              key: password
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: fast-ssd
      resources:
        requests:
          storage: 10Gi

Key fields:

  • serviceName - Name of the headless Service (required)
  • volumeClaimTemplates - Templates for creating PVCs for each pod
  • podManagementPolicy - Ordered (default) or Parallel

StatefulSet Lifecycle

graph TD A[StatefulSet Created] --> B[Create mysql-0] B --> C[Wait for mysql-0 Ready] C --> D[Create mysql-1] D --> E[Wait for mysql-1 Ready] E --> F[Create mysql-2] F --> G[All Pods Running] H[Update StatefulSet] --> I{Update Strategy} I -->|RollingUpdate| J[Update Pods One by One] I -->|OnDelete| K[Manual Pod Deletion Required] J --> L[Delete mysql-2] L --> M[Recreate mysql-2] M --> N[Delete mysql-1] N --> O[Recreate mysql-1] O --> P[Delete mysql-0] P --> Q[Recreate mysql-0] style A fill:#e1f5ff style G fill:#e8f5e9 style J fill:#fff4e1

Update Strategies

StatefulSets support two update strategies:

RollingUpdate (Default)

Updates pods one at a time in reverse ordinal order:

spec:
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      partition: 0  # All pods updated (can use for canary)

You can use partition for canary updates:

  • partition: 2 - Only update pods with index >= 2
  • partition: 1 - Update pods with index >= 1
  • partition: 0 - Update all pods

OnDelete

Pods are updated only when manually deleted:

spec:
  updateStrategy:
    type: OnDelete

Use Cases

StatefulSets are ideal for:

Databases - MySQL, PostgreSQL, MongoDB clusters
Message queues - RabbitMQ, Kafka
Distributed systems - etcd, Consul, Elasticsearch
Applications requiring stable identity - Any app where pods have unique roles
Applications needing persistent storage - Each pod needs its own data directory

Example: MySQL Replication

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  serviceName: mysql
  replicas: 3
  template:
    spec:
      containers:
      - name: mysql
        image: mysql:8.0
        args:
        - --server-id=$(echo $HOSTNAME | sed 's/mysql-//')
        - --log-bin
        - --gtid-mode=ON
        - --enforce-gtid-consistency
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secret
              key: password
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 50Gi

Best Practices

  1. Always create a headless Service - Required for stable network identity

  2. Use volumeClaimTemplates - Ensures each pod gets persistent storage

  3. Set resource requests and limits - Important for scheduling

  4. Use appropriate access modes - ReadWriteOnce for databases, ReadWriteMany if shared storage needed

  5. Plan for ordered operations - Be aware that scaling takes time due to ordering

  6. Use health probes - Ensure pods are ready before continuing deployment

livenessProbe:
  exec:
    command:
    - /bin/sh
    - -c
    - mysqladmin ping -h localhost
  initialDelaySeconds: 30
  periodSeconds: 10

readinessProbe:
  exec:
    command:
    - /bin/sh
    - -c
    - mysqladmin ping -h localhost
  initialDelaySeconds: 5
  periodSeconds: 5
  1. Handle graceful shutdown - Use preStop hooks for clean termination
lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "mysqladmin shutdown"]
  1. Monitor pod identities - Ensure applications use stable DNS names

  2. Test backup and restore - Verify data persistence across pod rescheduling

  3. Use podManagementPolicy: Parallel for independent pods - Only if pods don’t need ordered startup

Common Operations

View StatefulSet Status

# List StatefulSets
kubectl get statefulsets
kubectl get sts

# Detailed information
kubectl describe statefulset mysql

# View pods (note the stable names)
kubectl get pods -l app=mysql

Scaling

# Scale up
kubectl scale statefulset mysql --replicas=5

# Scale down (pods terminated in reverse order)
kubectl scale statefulset mysql --replicas=2

Updating

# Update container image
kubectl set image statefulset mysql mysql=mysql:8.0.33

# Check rollout status
kubectl rollout status statefulset mysql

# Rollback
kubectl rollout undo statefulset mysql

Accessing Pods

# Access a specific pod by name
kubectl exec -it mysql-0 -- mysql -u root -p

# Connect using stable DNS
kubectl run -it --rm client --image=mysql:8.0 --restart=Never -- mysql -h mysql-0.mysql

Troubleshooting

Pods Not Starting

# Check StatefulSet events
kubectl describe sts mysql

# Check pod events
kubectl describe pod mysql-0

# Check PVCs
kubectl get pvc -l app=mysql

Storage Issues

# Verify PVCs are bound
kubectl get pvc

# Check storage class
kubectl get storageclass

# Verify pod can mount volume
kubectl describe pod mysql-0

Network Issues

# Verify headless service exists
kubectl get svc mysql

# Test DNS resolution
kubectl run -it --rm test --image=busybox --restart=Never -- nslookup mysql-0.mysql

See Also