🎓 Mission Debrief: Stateful App Data Loss¶
What Happened¶
You were using a Deployment for a database, which is designed for stateless applications.
Databases are stateful workloads that need: - Stable, predictable pod names - Persistent storage that follows the pod - Ordered startup and shutdown - Stable network identities
Deployments don't provide any of these guarantees, which can lead to data loss, corruption, and split-brain scenarios in databases!
How Kubernetes Behaved¶
Deployment behavior (wrong for databases):
Deployment creates pods with random names:
- database-7d8f9c-abc12
- database-7d8f9c-xyz78
- database-7d8f9c-mno34
Pod deleted → New pod created with DIFFERENT name:
- database-7d8f9c-pqr56 # Different name!
Result:
❌ No way to identify "primary" vs "replica"
❌ Configuration tied to pod name doesn't work
❌ Persistent volume can't follow pod identity
❌ Can't do ordered operations (start primary first)
StatefulSet behavior (correct for databases):
StatefulSet creates pods with stable ordinal names:
- database-0 (primary)
- database-1 (replica)
- database-2 (replica)
Pod deleted → New pod created with SAME name:
- database-0 # Same name! Can reattach storage
Result:
✅ database-0 is always the primary
✅ Persistent volume for database-0 stays with it
✅ Ordered startup: 0 → 1 → 2
✅ Stable network ID: database-0.database-service
The Correct Mental Model¶
Stateless vs Stateful Workloads¶
Stateless (use Deployment):
Web Server Example:
┌─────────┐ ┌─────────┐ ┌─────────┐
│ web-abc │ │ web-xyz │ │ web-mno │
└─────────┘ └─────────┘ └─────────┘
↓ ↓ ↓
[No local state - all identical]
Any pod can handle any request
Pods are interchangeable
No need for stable identity
Can scale up/down instantly
Stateful (use StatefulSet):
Database Example:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ database-0 │ │ database-1 │ │ database-2 │
│ (PRIMARY) │ │ (REPLICA) │ │ (REPLICA) │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
↓ ↓ ↓
[PV-0] [PV-1] [PV-2]
database-0 is ALWAYS the primary
Each pod has its own persistent storage
Pods are NOT interchangeable
Ordered operations required
StatefulSet Guarantees¶
1. Stable Pod Names:
2. Stable Network Identity:
DNS for StatefulSet pods:
- database-0.database-service.k8squest.svc.cluster.local
- database-1.database-service.k8squest.svc.cluster.local
- database-2.database-service.k8squest.svc.cluster.local
Even if pod is deleted and recreated, DNS name stays the same!
3. Ordered Startup:
StatefulSet starts pods in order:
1. Create database-0, wait until Ready
2. Create database-1, wait until Ready
3. Create database-2, wait until Ready
This is critical for databases (primary must start first)!
4. Ordered Shutdown:
StatefulSet deletes pods in reverse order:
1. Delete database-2, wait until Terminated
2. Delete database-1, wait until Terminated
3. Delete database-0, wait until Terminated
This prevents data loss (replicas stop before primary)!
5. Persistent Volume Per Pod:
# StatefulSet with volumeClaimTemplates
apiVersion: apps/v1
kind: StatefulSet
spec:
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 10Gi
Creates:
- PVC: data-database-0 → Pod: database-0
- PVC: data-database-1 → Pod: database-1
- PVC: data-database-2 → Pod: database-2
If database-0 pod is deleted, new database-0 pod gets same PVC!
When to Use Each¶
Use Deployment: - Web servers - API backends (stateless) - Batch jobs (stateless) - Microservices (no local state) - Cache layers (can lose data)
Use StatefulSet: - Databases (PostgreSQL, MySQL, MongoDB) - Message queues (Kafka, RabbitMQ) - Distributed caches (Redis cluster) - Consensus systems (etcd, ZooKeeper) - Any app that needs stable identity
Real-World Incident Example¶
Company: Financial services SaaS (10K business customers) Impact: 3-hour database outage, data corruption, $5M+ in damages Cost: $5M in SLA penalties + emergency restoration costs + customer churn
What happened:
A developer was setting up a PostgreSQL database in Kubernetes. They found an example Deployment configuration and used it, not realizing it was for a simple demo (not production).
Broken configuration:
apiVersion: apps/v1
kind: Deployment # ❌ WRONG for database!
metadata:
name: postgres
spec:
replicas: 3 # Primary + 2 replicas
template:
spec:
containers:
- name: postgres
image: postgres:14
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumes:
- name: data
emptyDir: {} # ❌ LOSES DATA ON POD RESTART!
The cascade (Friday, 3 PM - month-end closing):
15:00 - Kubernetes node experiences memory pressure
15:00 - Kubelet evicts postgres pod (postgres-abc123) from node
15:00 - Deployment creates new pod (postgres-xyz789) on different node
15:00 - New pod starts with EMPTY emptyDir volume
15:00 - PostgreSQL initializes NEW empty database ❌
15:01 - Application connects to new pod
15:01 - Application thinks database is empty
15:01 - Application starts fresh (creates schema)
15:02 - OLD pod (postgres-abc123) still running on original node
15:02 - TWO postgres instances, DIFFERENT data ❌
15:05 - Service load balances between both pods
15:05 - Half of requests see old data, half see new (empty) data
15:05 - Application in complete chaos
15:05 - Users report: "My data disappeared!"
15:10 - Support tickets flooding in (500 tickets in 5 minutes)
15:15 - Emergency escalation
15:20 - Team identifies two postgres pods with different data
15:25 - Realize: Using Deployment with emptyDir = disaster
15:30 - Immediate decision: Shut down all postgres pods
15:30 - Application completely down ❌
15:45 - Start recovery process from backups
16:00 - Determine which data is canonical (complex!)
16:30 - Restore from backup (1 hour old)
17:00 - Manually merge data from two conflicting pods
18:00 - Database back online
18:00 - But 1 hour of transactions lost + data inconsistencies
Why it was catastrophic: - Month-end closing - Businesses processing critical financial transactions - Data loss - emptyDir doesn't persist across pod restarts - Split-brain - Two postgres instances with different data - No recovery plan - Unprepared for this scenario - Customer impact - Lost invoices, incomplete transactions - Compliance issues - Financial data integrity compromised
What users experienced:
15:00 - Working normally
15:05 - "Where did my invoice go?" (connected to new pod with empty DB)
15:06 - "Why is my data from yesterday?" (connected to old pod)
15:10 - "Application is completely broken!"
15:30 - "Service Unavailable" (all pods shut down for recovery)
18:00 - "We've lost the last hour of work" (restore from 1hr-old backup)
Root cause: 1. Using Deployment instead of StatefulSet - No stable identity 2. emptyDir instead of PersistentVolume - Data not persisted 3. No testing of failure scenarios - Never tested pod eviction 4. No monitoring of database state - Didn't detect split-brain 5. No backup validation - Backup was 1 hour old (too infrequent)
The correct configuration:
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
clusterIP: None # Headless service
selector:
app: postgres
---
apiVersion: apps/v1
kind: StatefulSet # ✅ CORRECT for database
metadata:
name: postgres
spec:
serviceName: postgres
replicas: 1 # Start with single instance (avoid replication complexity)
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:14
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
subPath: postgres # Important for postgres
volumeClaimTemplates: # ✅ Persistent storage
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: ssd # Use appropriate storage class
resources:
requests:
storage: 100Gi
Process improvements: 1. Architecture review for all stateful workloads 2. Mandatory StatefulSet for databases policy 3. Chaos testing - Regular pod eviction tests 4. Monitoring - Alert on pod name changes (indicates Deployment, not StatefulSet) 5. Backup frequency - Every 15 minutes instead of hourly
Lessons learned: 1. Never use Deployment for databases - Always use StatefulSet 2. Never use emptyDir for stateful data - Always use PersistentVolume 3. Test failure scenarios - Pod eviction, node failure, etc. 4. Monitor database identity - Alert if postgres pods have random names 5. Have rollback plan - Know how to recover from data corruption
Commands You Mastered¶
# Create StatefulSet
kubectl apply -f statefulset.yaml
# Check StatefulSet status
kubectl get statefulset -n <namespace>
# Get pod names (should be stable: app-0, app-1, app-2)
kubectl get pods -n <namespace> -l app=<label>
# Describe StatefulSet
kubectl describe statefulset <name> -n <namespace>
# Check pod DNS names
kubectl run -it --rm debug --image=busybox --restart=Never -n <namespace> -- nslookup database-0.database-service
# Scale StatefulSet (scales in order)
kubectl scale statefulset <name> --replicas=5 -n <namespace>
# Delete StatefulSet (keeps PVCs by default)
kubectl delete statefulset <name> -n <namespace>
# Delete StatefulSet AND PVCs
kubectl delete statefulset <name> -n <namespace> --cascade=orphan
kubectl delete pvc --all -n <namespace>
# Check PVCs created by StatefulSet
kubectl get pvc -n <namespace>
# Restart pod (StatefulSet recreates with same name)
kubectl delete pod <name>-0 -n <namespace>
# Watch it recreate with same name!
# Check pod startup order
kubectl get pods -n <namespace> -l app=<label> -w
Best Practices for StatefulSets¶
✅ DO:¶
-
Use StatefulSet for stateful workloads:
-
Always use PersistentVolumes:
-
Use headless Service:
-
Set appropriate update strategy:
-
Use readiness probes:
❌ DON'T:¶
-
Don't use Deployment for databases:
-
Don't use emptyDir for stateful data:
-
Don't forget serviceName:
-
Don't manually change pod ordinals:
StatefulSet Advanced Patterns¶
Pattern 1: Database Primary/Replica¶
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql
replicas: 3
template:
spec:
initContainers:
- name: init-mysql
image: mysql:5.7
command:
- bash
- "-c"
- |
set -ex
# Primary is pod-0, replicas are pod-1, pod-2, etc.
[[ `hostname` =~ -([0-9]+)$ ]] || exit 1
ordinal=${BASH_REMATCH[1]}
if [[ $ordinal -eq 0 ]]; then
cp /mnt/config-map/primary.cnf /mnt/conf.d/
else
cp /mnt/config-map/replica.cnf /mnt/conf.d/
fi
Pattern 2: Ordered Dependency¶
# Start pods in order, each depends on previous
apiVersion: apps/v1
kind: StatefulSet
spec:
podManagementPolicy: OrderedReady # Default
# vs Parallel (all pods start at once)
Pattern 3: Canary Updates¶
# Update only some pods (testing)
spec:
updateStrategy:
type: RollingUpdate
rollingUpdate:
partition: 2 # Only update pods >= ordinal 2
# Pods 0 and 1 stay on old version
What's Next?¶
You've learned the critical difference between Deployments (stateless) and StatefulSets (stateful), and why databases need stable identities.
Next level (FINAL LEVEL OF WORLD 2!): ReplicaSet chaos! You'll learn why manually creating ReplicaSets without a Deployment causes management nightmares.
Key takeaway: Never use Deployments for stateful workloads like databases. Always use StatefulSets with PersistentVolumes for data persistence and stable pod identities!