Pods are ephemeral. Their storage dies with them. Databases need persistent storage. PersistentVolumes (PVs) are cluster storage resources. PersistentVolumeClaims (PVCs) are requests for storage. StorageClasses enable dynamic provisioning. In this lesson, you'll master persistent storage for stateful applications.
1. Learning Objectives
By the end of this lesson, you will be able to:
- Understand PersistentVolumes (PV) and PersistentVolumeClaims (PVC)
- Create static PVs for pre-provisioned storage
- Use StorageClasses for dynamic provisioning
- Configure StatefulSets with persistent storage
- Backup and restore persistent volumes
- Troubleshoot storage issues
2. Why This Matters
Real-world scenario: Your PostgreSQL database pod dies. Without persistent storage, all your data is gone. With PersistentVolumes, the data survives and attaches to the new pod. Your database can even be rescheduled to another node without data loss.
3. Core Concepts
Static PersistentVolumes
# persistent-volume.yaml - Static PV
apiVersion: v1
kind: PersistentVolume
metadata:
name: postgres-pv
labels:
type: local
storage: ssd
spec:
capacity:
storage: 10Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce # Can be mounted by single pod
# - ReadOnlyMany # Read-only by many pods
# - ReadWriteMany # Read-write by many pods
persistentVolumeReclaimPolicy: Retain
# Reclaim policies: Retain, Recycle, Delete
storageClassName: standard
hostPath:
path: /mnt/data/postgres # For local testing only!
# For production, use cloud storage:
# awsElasticBlockStore:
# volumeID: vol-123456
# fsType: ext4
# gcePersistentDisk:
# pdName: postgres-disk
# fsType: ext4
# azureDisk:
# diskName: postgres-disk
# diskURI: https://...
# nfs:
# server: nfs-server.example.com
# path: /exports/postgres
# persistent-volume-claim.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
namespace: default
spec:
accessModes:
- ReadWriteOnce
volumeMode: Filesystem
resources:
requests:
storage: 5Gi
storageClassName: standard
selector:
matchLabels:
type: local
storage: ssd
kubectl apply -f persistent-volume.yaml
kubectl apply -f persistent-volume-claim.yaml
kubectl get pv
kubectl get pvc
kubectl describe pv postgres-pv
kubectl describe pvc postgres-pvc
Static PersistentVolumes and Claims
StorageClasses (Dynamic Provisioning)
# storage-class.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast
provisioner: kubernetes.io/aws-ebs # Cloud-specific
parameters:
type: gp3
fsType: ext4
iopsPerGB: "10"
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
# AWS EBS StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: gp3-retain
provisioner: ebs.csi.aws.com
parameters:
type: gp3
encrypted: "true"
reclaimPolicy: Retain
# Azure Disk StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: azure-disk
provisioner: disk.csi.azure.com
parameters:
skuname: StandardSSD_LRS
kind: Managed
# GCP Persistent Disk StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: standard-rwo
provisioner: pd.csi.storage.gke.io
parameters:
type: pd-standard
replication-type: none
# Local SSD (for testing)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-ssd
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
kubectl get storageclass
kubectl describe storageclass fast
kubectl get storageclass -o wide
StorageClasses for dynamic provisioning
Using PVC in Pods and Deployments
# pod-with-pvc.yaml
apiVersion: v1
kind: Pod
metadata:
name: postgres-pod
spec:
containers:
- name: postgres
image: postgres:15-alpine
env:
- name: POSTGRES_PASSWORD
value: "password"
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
subPath: postgres
volumes:
- name: postgres-storage
persistentVolumeClaim:
claimName: postgres-pvc
# deployment-with-pvc.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql-deployment
spec:
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8
env:
- name: MYSQL_ROOT_PASSWORD
value: "password"
volumeMounts:
- name: mysql-storage
mountPath: /var/lib/mysql
volumes:
- name: mysql-storage
persistentVolumeClaim:
claimName: mysql-pvc
Using PVCs in pods and deployments
StatefulSet with Persistent Storage
# statefulset-with-pvc.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres-statefulset
spec:
serviceName: postgres-hl
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15-alpine
ports:
- containerPort: 5432
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast
resources:
requests:
storage: 20Gi
# Headless service
apiVersion: v1
kind: Service
metadata:
name: postgres-hl
spec:
clusterIP: None
selector:
app: postgres
ports:
- port: 5432
# Each pod gets its own PVC:
# data-postgres-statefulset-0
# data-postgres-statefulset-1
# data-postgres-statefulset-2
kubectl get pvc
# NAME STATUS VOLUME CAPACITY
# data-postgres-statefulset-0 Bound pv-abc123 20Gi
# data-postgres-statefulset-1 Bound pv-def456 20Gi
# data-postgres-statefulset-2 Bound pv-ghi789 20Gi
StatefulSet with volumeClaimTemplates
Volume Snapshot and Restore
# volume-snapshot-class.yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: csi-snapclass
driver: ebs.csi.aws.com
deletionPolicy: Delete
parameters:
type: snap
# volume-snapshot.yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: postgres-snapshot
spec:
volumeSnapshotClassName: csi-snapclass
source:
persistentVolumeClaimName: postgres-pvc
# Restore from snapshot
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc-restored
spec:
dataSource:
name: postgres-snapshot
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
kubectl get volumesnapshotclass
kubectl get volumesnapshot
kubectl describe volumesnapshot postgres-snapshot
Volume snapshots for backup and restore
4. Complete Project: Production PostgreSQL
# complete-postgres.yaml
---
# StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: postgres-storage
provisioner: kubernetes.io/aws-ebs # Adjust for cloud
parameters:
type: gp3
encrypted: "true"
reclaimPolicy: Retain
allowVolumeExpansion: true
---
# Secret for PostgreSQL
apiVersion: v1
kind: Secret
metadata:
name: postgres-secret
type: Opaque
data:
POSTGRES_PASSWORD: c3VwZXJzZWNyZXQK # supersecret
REPLICATION_PASSWORD: cmVwbGljYXRpb24K # replication
---
# ConfigMap for PostgreSQL configuration
apiVersion: v1
kind: ConfigMap
metadata:
name: postgres-config
data:
postgresql.conf: |
max_connections = 100
shared_buffers = 128MB
effective_cache_size = 384MB
maintenance_work_mem = 64MB
checkpoint_completion_target = 0.9
wal_buffers = 16MB
default_statistics_target = 100
random_page_cost = 1.1
effective_io_concurrency = 200
work_mem = 4MB
huge_pages = try
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.track = all
pg_hba.conf: |
local all all trust
host all all 0.0.0.0/0 md5
host replication replicator 0.0.0.0/0 md5
---
# Headless Service
apiVersion: v1
kind: Service
metadata:
name: postgres-hl
spec:
clusterIP: None
selector:
app: postgres
ports:
- port: 5432
name: postgres
---
# Read Service (for applications)
apiVersion: v1
kind: Service
metadata:
name: postgres-service
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
---
# StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres-hl
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15-alpine
ports:
- containerPort: 5432
name: postgres
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: POSTGRES_PASSWORD
- name: POSTGRES_USER
value: "postgres"
- name: POSTGRES_DB
value: "myapp"
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
- name: config
mountPath: /etc/postgresql
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "1000m"
livenessProbe:
exec:
command:
- pg_isready
- -U
- postgres
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command:
- pg_isready
- -U
- postgres
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: config
configMap:
name: postgres-config
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: postgres-storage
resources:
requests:
storage: 50Gi
---
# Backup CronJob
apiVersion: batch/v1
kind: CronJob
metadata:
name: postgres-backup
spec:
schedule: "0 2 * * *" # Daily at 2 AM
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: postgres:15-alpine
env:
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: POSTGRES_PASSWORD
command:
- /bin/sh
- -c
- |
pg_dump -h postgres-service -U postgres myapp > /backup/backup-$(date +%Y%m%d-%H%M%S).sql
gzip /backup/*.sql
volumeMounts:
- name: backup
mountPath: /backup
volumes:
- name: backup
persistentVolumeClaim:
claimName: postgres-backup-pvc
restartPolicy: OnFailure
# Apply everything
kubectl apply -f complete-postgres.yaml
# Wait for PostgreSQL to be ready
kubectl wait --for=condition=ready pod/postgres-0 --timeout=300s
# Test connectivity
kubectl run test-postgres --image=postgres:15-alpine -it --rm --restart=Never -- \
psql -h postgres-service -U postgres -d myapp -c "SELECT version();"
# Check PVC status
kubectl get pvc
kubectl get pv
Production PostgreSQL with persistent storage
5. Common Errors & Solutions
6. Summary Checklist
7. Next Steps
Next lesson: Kubernetes Lesson 6.6: Probes & Application Health
Comments (0)
This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!
ReplyLeave a Comment