Skip to content

🎓 LEVEL 34 DEBRIEF: StatefulSet Volume Templates

Congratulations! You've mastered StatefulSet volumeClaimTemplates - the key to providing persistent, per-pod storage for stateful applications!


📊 What You Fixed

The Problem:

# All pods sharing ONE PVC
volumes:
- name: database-storage
  persistentVolumeClaim:
    claimName: database-storage  # ❌ All pods use same PVC!

Result: Database corruption - all 3 postgres pods writing to same files!

The Solution:

# Each pod gets its OWN PVC
volumeClaimTemplates:
- metadata:
    name: database-storage
  spec:
    accessModes: [ReadWriteOnce]
    resources:
      requests:
        storage: 5Gi

Result: 3 separate PVCs created automatically: - database-storage-postgres-cluster-0 - database-storage-postgres-cluster-1 - database-storage-postgres-cluster-2


🔍 Understanding StatefulSet Storage

Why StatefulSets Need Special Storage

Stateful Applications: - Databases (PostgreSQL, MySQL, MongoDB) - Message queues (Kafka, RabbitMQ) - Distributed systems (etcd, ZooKeeper) - Each instance needs its own persistent identity and data

Wrong Approach (Deployment with single PVC):

kind: Deployment  # ❌ For stateful apps
replicas: 3
volumes:
- persistentVolumeClaim:
    claimName: shared-data
Problems: - All pods write to same storage - Data conflicts and corruption - Cannot scale independently - No stable pod identity

Right Approach (StatefulSet with volumeClaimTemplates):

kind: StatefulSet  # ✅ For stateful apps
replicas: 3
volumeClaimTemplates:
- metadata:
    name: data
  spec:
    accessModes: [ReadWriteOnce]
    resources:
      requests:
        storage: 10Gi

Benefits: - Each pod gets dedicated storage - Stable pod names (pod-0, pod-1, pod-2) - Ordered deployment and scaling - Persistent storage survives pod restarts


🎯 volumeClaimTemplates Deep Dive

How It Works

  1. StatefulSet Created:

    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      name: web
    spec:
      replicas: 3
      volumeClaimTemplates:
      - metadata:
          name: www
        spec:
          accessModes: [ReadWriteOnce]
          resources:
            requests:
              storage: 1Gi
    

  2. Kubernetes Creates PVCs Automatically:

  3. www-web-0 (for pod web-0)
  4. www-web-1 (for pod web-1)
  5. www-web-2 (for pod web-2)

  6. Each Pod Gets Its PVC:

  7. Pod web-0 → PVC www-web-0
  8. Pod web-1 → PVC www-web-1
  9. Pod web-2 → PVC www-web-2

PVC Naming Pattern

<volumeClaimTemplate-name>-<statefulset-name>-<ordinal>

Examples: - data-mysql-0 - data-mysql-1 - logs-app-0 - cache-redis-2

Lifecycle Management

When Pod Deleted: - Pod is removed - PVC remains (not deleted!) - Data preserved

When Pod Recreated: - New pod gets SAME PVC - Data from previous pod available - Maintains state across restarts

When StatefulSet Scaled Down:

kubectl scale statefulset web --replicas=2
- Pod web-2 deleted - PVC www-web-2 retained - Data safe for future scale-up

When Scaled Back Up:

kubectl scale statefulset web --replicas=3
- New pod web-2 created - Automatically bound to existing www-web-2 - Previous data intact!


💥 Common Mistakes

Mistake 1: Using Static PVC with StatefulSet

# ❌ Wrong
kind: StatefulSet
spec:
  volumes:
  - name: data
    persistentVolumeClaim:
      claimName: my-pvc  # All pods share this!

Problem: Data conflicts, corruption, or "volume already mounted" errors

Fix: Use volumeClaimTemplates

Mistake 2: Forgetting volumeClaimTemplates

# ❌ Wrong
kind: StatefulSet
spec:
  replicas: 3
  template:
    spec:
      containers:
      - volumeMounts:
        - name: data
          mountPath: /data
  # Missing volumeClaimTemplates!

Problem: Pods have no storage or use emptyDir

Fix: Add volumeClaimTemplates

Mistake 3: Wrong Access Mode

# ❌ Wrong for StatefulSet
volumeClaimTemplates:
- spec:
    accessModes: [ReadWriteMany]  # Unnecessary!

Problem: More expensive storage, not needed for per-pod PVCs

Fix: Use ReadWriteOnce (each PVC mounted by single pod)

Mistake 4: Deleting PVCs Manually

# ❌ Don't do this!
kubectl delete pvc data-mysql-0

Problem: Pod cannot start, data lost

Fix: Let StatefulSet manage PVCs, delete StatefulSet if needed

Mistake 5: Not Planning for Storage Growth

# ❌ Small initial size
volumeClaimTemplates:
- spec:
    resources:
      requests:
        storage: 1Gi  # Too small for database!

Problem: Running out of space, PVC resizing complicated

Fix: Plan capacity, use appropriate initial size


🏗️ Real-World Patterns

Pattern 1: PostgreSQL Cluster

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres
  replicas: 3
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:14
        env:
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: postgres-secret
              key: password
        - name: PGDATA
          value: /var/lib/postgresql/data/pgdata
        ports:
        - containerPort: 5432
        volumeMounts:
        - name: data
          mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ReadWriteOnce]
      storageClassName: fast-ssd
      resources:
        requests:
          storage: 100Gi

Pattern 2: Multiple Volumes Per Pod

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: app
spec:
  volumeClaimTemplates:
  - metadata:
      name: data  # Application data
    spec:
      accessModes: [ReadWriteOnce]
      resources:
        requests:
          storage: 50Gi
  - metadata:
      name: logs  # Separate volume for logs
    spec:
      accessModes: [ReadWriteOnce]
      resources:
        requests:
          storage: 20Gi

Result: - data-app-0 + logs-app-0 - data-app-1 + logs-app-1 - Each pod gets 2 PVCs

Pattern 3: StatefulSet with Shared ConfigMap

apiVersion: apps/v1
kind: StatefulSet
spec:
  template:
    spec:
      containers:
      - volumeMounts:
        - name: data
          mountPath: /data  # Per-pod storage
        - name: config
          mountPath: /config  # Shared config
      volumes:
      - name: config
        configMap:
          name: app-config  # Shared by all pods
  volumeClaimTemplates:  # Per-pod storage
  - metadata:
      name: data
    spec:
      accessModes: [ReadWriteOnce]
      resources:
        requests:
          storage: 10Gi

🚨 REAL-WORLD HORROR STORY: The Shared Database Volume

The Incident: $3.2M Data Corruption Disaster

Company: Healthcare SaaS platform Date: September 2021 Impact: 11 hours downtime, 3,200 customers affected, $3.2M revenue loss, HIPAA investigation

What Happened

DevOps team deployed PostgreSQL cluster:

# Deployment (should have been StatefulSet!)
kind: Deployment  # ❌ Wrong for database!
metadata:
  name: postgres
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: postgres
        volumeMounts:
        - name: db-storage
          mountPath: /var/lib/postgresql/data
      volumes:
      - name: db-storage
        persistentVolumeClaim:
          claimName: postgres-data  # ❌ All pods share this PVC!

The Timeline

09:00 - Deployment updated to 3 replicas for "high availability" 09:05 - All 3 pods started, all mounting same PVC 09:06 - Database files corrupted (multiple postgres processes writing) 09:10 - Customer reports: "Cannot access patient records" 09:15 - Database completely unusable 09:30 - Emergency declared, all 3,200 customers affected 10:00 - Attempted rollback failed (data already corrupted) 12:00 - Restore from backup started 14:00 - Backup restoration failed (backup also corrupted) 16:00 - Switched to older backup (4 hours of data loss) 20:00 - Service restored, 4 hours of patient data permanently lost

Root Causes

  1. Wrong workload type: Used Deployment instead of StatefulSet
  2. Shared storage: Single PVC shared by all database pods
  3. No validation: Didn't test with multiple replicas
  4. Poor monitoring: No alert on database file conflicts
  5. Backup issues: Backup taken while database was corrupted
  6. Insufficient testing: HA setup never tested in staging

The Aftermath

  • $3.2M in lost revenue and SLA penalties
  • 4 hours of patient data permanently lost
  • HIPAA investigation and fines
  • 15% customer churn
  • Complete infrastructure audit mandated
  • DevOps team restructured

The Correct Solution

apiVersion: apps/v1
kind: StatefulSet  # ✅ Correct for database
metadata:
  name: postgres
spec:
  serviceName: postgres
  replicas: 3
  template:
    spec:
      containers:
      - name: postgres
        volumeMounts:
        - name: data
          mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:  # ✅ Each pod gets own storage
  - metadata:
      name: data
    spec:
      accessModes: [ReadWriteOnce]
      resources:
        requests:
          storage: 100Gi

Lessons Learned

  1. Use StatefulSet for stateful apps: Never use Deployment for databases
  2. Each instance needs own storage: volumeClaimTemplates, not shared PVC
  3. Test HA configurations: Verify replicas work before production
  4. Monitor file locking: Alert on database file conflicts
  5. Validate backups: Test restoration regularly
  6. Staging mirrors production: Test exact production config

🛡️ Best Practices

1. Choose Right Workload Type

# Stateless apps → Deployment
kind: Deployment
# All pods identical, no persistent identity needed

# Stateful apps → StatefulSet
kind: StatefulSet
# Each pod unique, needs own storage and identity

2. Use Appropriate Storage Class

volumeClaimTemplates:
- spec:
    storageClassName: fast-ssd  # For databases
    # fast-ssd: High performance, expensive
    # standard: Normal performance, cheaper

3. Plan Storage Capacity

resources:
  requests:
    storage: 100Gi  # Plan for growth!
# Better to over-provision than resize later

4. Set Resource Limits

containers:
- name: postgres
  resources:
    requests:
      memory: "4Gi"
      cpu: "2"
    limits:
      memory: "8Gi"
      cpu: "4"

5. Use Headless Service

apiVersion: v1
kind: Service
metadata:
  name: postgres
spec:
  clusterIP: None  # Headless service for StatefulSet
  selector:
    app: postgres
  ports:
  - port: 5432

6. Implement Proper Backup Strategy

# CronJob for backups
apiVersion: batch/v1
kind: CronJob
metadata:
  name: postgres-backup
spec:
  schedule: "0 */6 * * *"  # Every 6 hours
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: backup
            image: postgres:14
            command: ["/backup.sh"]
            volumeMounts:
            - name: data
              mountPath: /data
              readOnly: true

🎯 Key Takeaways

  1. StatefulSets for stateful apps - Databases, queues need stable identity
  2. volumeClaimTemplates for per-pod storage - Each instance gets own PVC
  3. PVCs persist across pod restarts - Data survives pod lifecycle
  4. Never share single PVC for databases - Causes corruption and conflicts
  5. Plan storage capacity upfront - Resizing is complex
  6. Use headless services - Enable stable network identity
  7. Test HA before production - Verify multiple replicas work correctly
  8. Monitor and backup - Protect against data loss

🚀 Next Steps

Now that you understand StatefulSet storage, you're ready for:

  • Level 35: StorageClass configuration and dynamic provisioning
  • Level 36: ConfigMap volumes and key management
  • Level 37: Secrets and base64 encoding

Well done! You've mastered StatefulSet volumeClaimTemplates. Remember: stateful apps need stateful storage! 🎉