Storage Persistente in Kubernetes: CSI, PV, StorageClass e StatefulSet
Kubernetes e nato come piattaforma per workload stateless, ma la realta delle applicazioni enterprise e ben diversa: database, code di messaggi, sistemi di cache persistente, file system condivisi. Tutti richiedono storage che sopravviva al ciclo di vita di un Pod. Gestire questo storage in modo affidabile, performante e portabile tra cloud provider diversi e una delle sfide piu concrete del giorno per giorno in produzione.
In questo articolo esploreremo l'intero layer di storage Kubernetes: dal Container Storage Interface (CSI) che standardizza l'integrazione con i provider, ai PersistentVolume e StorageClass per il provisioning dinamico, fino ai StatefulSet per gestire database come PostgreSQL, Cassandra e Redis su Kubernetes.
Cosa Imparerai
- Il modello di storage Kubernetes: Volume, PersistentVolume, PersistentVolumeClaim
- Come funziona il Container Storage Interface (CSI) e i driver piu usati
- StorageClass e provisioning dinamico: configurazione per AWS EBS, GCE PD, Azure Disk
- Access Mode: ReadWriteOnce, ReadOnlyMany, ReadWriteMany - quando usare quale
- StatefulSet: identity stabile, PVC automatici, rolling update ordinato
- Come eseguire PostgreSQL su Kubernetes con StatefulSet
- Backup e restore dei PersistentVolume con Velero
Il Modello di Storage Kubernetes
Kubernetes definisce una gerarchia di astrazione per lo storage che separa "cosa serve" (PersistentVolumeClaim) da "come e fornito" (PersistentVolume e StorageClass). Questo permette agli sviluppatori di richiedere storage senza conoscere i dettagli del provider cloud sottostante.
Le Primitive di Storage
| Risorsa | Scope | Chi la gestisce | Descrizione |
|---|---|---|---|
| Volume | Pod | Developer | Storage effimero legato al ciclo di vita del Pod |
| PersistentVolume (PV) | Cluster | Admin / Provisioner | Pezzo di storage nel cluster, ciclo di vita indipendente dai Pod |
| PersistentVolumeClaim (PVC) | Namespace | Developer | Richiesta di storage da un Pod |
| StorageClass | Cluster | Admin | Definisce il "tipo" di storage e il provisioner |
Ciclo di Vita di un PersistentVolume
Il ciclo di vita di un PV attraversa diversi stati. Capirli e fondamentale per il troubleshooting:
- Available: Il PV esiste ed e libero, non associato a nessun PVC
- Bound: Il PV e stato legato a un PVC che soddisfa i requisiti
- Released: Il PVC e stato eliminato, ma il PV non e ancora disponibile (i dati sono ancora li)
- Failed: Il PV ha fallito il reclaim automatico
# Verifica lo stato dei PersistentVolume nel cluster
kubectl get pv -o wide
# Output tipico:
# NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS
# pv-db-001 100Gi RWO Retain Bound production/postgres fast-ssd
# pv-db-002 100Gi RWO Retain Available fast-ssd
# Descrizione dettagliata di un PV
kubectl describe pv pv-db-001
# Verifica i PVC in un namespace
kubectl get pvc -n production
kubectl describe pvc postgres-data -n production
Container Storage Interface (CSI)
Prima del CSI, ogni provider di storage doveva mantenere plugin integrati nel codice sorgente di Kubernetes (in-tree plugins). Questo creava accoppiamento forte e rendeva difficile aggiornare i plugin indipendentemente da Kubernetes. Il CSI risolve questo con un'interfaccia gRPC standard che permette a terze parti di creare driver di storage come Pod Kubernetes indipendenti.
Driver CSI Principali
| Driver CSI | Provider | Tipo Storage | ReadWriteMany |
|---|---|---|---|
| aws-ebs-csi-driver | AWS | Block (gp3, io2) | No |
| aws-efs-csi-driver | AWS | NFS (EFS) | Si |
| gce-pd-csi-driver | GCP | Block (pd-ssd, pd-balanced) | No (RWX solo FileStore) |
| azuredisk-csi-driver | Azure | Block (Premium SSD) | No |
| azurefile-csi-driver | Azure | NFS (Azure Files) | Si |
| csi-rook-ceph | Rook/Ceph | Block/FS/Object | Si (CephFS) |
| longhorn | Rancher | Block distribuito | Si (con NFS) |
Installazione del Driver CSI EBS su EKS
# Installa il driver CSI EBS su Amazon EKS
# Prima, crea un IAM Role con le policy necessarie
aws eks create-addon \
--cluster-name my-cluster \
--addon-name aws-ebs-csi-driver \
--service-account-role-arn arn:aws:iam::ACCOUNT_ID:role/EBSCSIRole
# Verifica il daemonset del driver CSI
kubectl get daemonset -n kube-system ebs-csi-node
kubectl get deployment -n kube-system ebs-csi-controller
StorageClass: Provisioning Dinamico
Il provisioning statico (creare manualmente i PV) e impraticabile in produzione. Con il provisioning dinamico, Kubernetes crea automaticamente il PV quando un PVC viene creato, usando il driver CSI configurato nella StorageClass.
StorageClass per AWS EBS
# storage-classes-aws.yaml
# StorageClass per dischi gp3 (performance ottimale)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
annotations:
storageclass.kubernetes.io/is-default-class: "false"
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer # IMPORTANTE: evita cross-AZ mounting
reclaimPolicy: Retain # Protegge i dati in produzione
allowVolumeExpansion: true
parameters:
type: gp3
iops: "3000"
throughput: "125"
encrypted: "true"
kmsKeyId: "arn:aws:kms:eu-west-1:ACCOUNT:key/KEY_ID"
---
# StorageClass per io2 (database ad alto IOPS)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ultra-fast-ssd
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
allowVolumeExpansion: true
parameters:
type: io2
iops: "32000"
encrypted: "true"
---
# StorageClass per EFS (ReadWriteMany, NFS)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: shared-storage
provisioner: efs.csi.aws.com
reclaimPolicy: Retain
parameters:
provisioningMode: efs-ap
fileSystemId: fs-XXXXXXXX
directoryPerms: "700"
StorageClass per GKE (Google Kubernetes Engine)
# storage-classes-gke.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd-gke
provisioner: pd.csi.storage.gke.io
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
allowVolumeExpansion: true
parameters:
type: pd-ssd
replication-type: regional-pd # replica su 2 zone
availability-class: regional-hard-failover
volumeBindingMode: Perche WaitForFirstConsumer
Usa sempre WaitForFirstConsumer invece di Immediate quando
il cluster ha piu Availability Zone. Con Immediate, il PV viene creato
nella zona in cui e schedulato il provisioner, che potrebbe essere diversa dalla zona
dove il Pod verra schedulato. Risultato: il Pod non riesce a montare il volume.
WaitForFirstConsumer crea il PV nella stessa zona del Pod.
PersistentVolumeClaim in Pratica
Il PVC e il modo con cui un Pod richiede storage. Il PVC specifica dimensione, access mode e StorageClass. Kubernetes trova o crea un PV compatibile e lo lega al PVC.
# pvc-database.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data
namespace: production
labels:
app: postgres
tier: database
annotations:
# Snapshot policy (con alcuni storage providers)
storageclass.kubernetes.io/is-default-class: "false"
spec:
accessModes:
- ReadWriteOnce
storageClassName: fast-ssd
resources:
requests:
storage: 100Gi
# Espandi a 200Gi in futuro con:
# kubectl patch pvc postgres-data -p '{"spec":{"resources":{"requests":{"storage":"200Gi"}}}}'
Access Mode: Scegliere Quello Giusto
| Access Mode | Abbreviazione | Cosa significa | Uso tipico |
|---|---|---|---|
| ReadWriteOnce | RWO | Un solo nodo in lettura/scrittura | Database, applicazioni single-instance |
| ReadOnlyMany | ROX | Molti nodi in sola lettura | Dati statici condivisi, configurazioni |
| ReadWriteMany | RWX | Molti nodi in lettura/scrittura | NFS condivisi, upload handler |
| ReadWriteOncePod | RWOP | Un solo Pod in lettura/scrittura | Storage esclusivo per singolo Pod (K8s 1.29+) |
StatefulSet: Workload con Identita Stabile
I Deployment sono ottimi per workload stateless, ma per database e applicazioni che richiedono identita stabile (hostname predicibile, volume dedicato, ordine di avvio) serve lo StatefulSet. Le differenze chiave rispetto ai Deployment:
- Identita stabile: I Pod hanno nomi predicibili:
myapp-0,myapp-1,myapp-2 - Ordine di avvio: I Pod vengono avviati in ordine (0, poi 1, poi 2) e spenti in ordine inverso
- PVC dedicati: Ogni Pod ottiene il proprio PVC tramite
volumeClaimTemplates - Headless Service: Ogni Pod ha un DNS entry stabile:
myapp-0.myapp.namespace.svc.cluster.local
PostgreSQL su Kubernetes con StatefulSet
Ecco una configurazione completa e production-ready per PostgreSQL con StatefulSet, inclusi ConfigMap per la configurazione, Secret per le credenziali e Service headless:
# postgres-statefulset.yaml
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: production
labels:
app: postgres
spec:
ports:
- port: 5432
name: postgres
clusterIP: None # Headless Service - abilita il DNS per Pod individuali
selector:
app: postgres
---
# Service per accesso al master (read/write)
apiVersion: v1
kind: Service
metadata:
name: postgres-master
namespace: production
spec:
ports:
- port: 5432
selector:
app: postgres
role: master
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: production
spec:
serviceName: postgres # deve corrispondere al nome del headless Service
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
# Anti-affinity: distribuisce le repliche su nodi diversi
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: postgres
topologyKey: kubernetes.io/hostname
initContainers:
# init container per configurare i permessi del volume
- name: init-postgres
image: postgres:16
command:
- bash
- "-c"
- |
chown -R 999:999 /var/lib/postgresql/data
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
containers:
- name: postgres
image: postgres:16
ports:
- containerPort: 5432
name: postgres
env:
- name: POSTGRES_DB
value: myapp
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-credentials
key: username
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-credentials
key: password
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
- name: postgres-config
mountPath: /etc/postgresql/postgresql.conf
subPath: postgresql.conf
resources:
requests:
memory: "2Gi"
cpu: "1000m"
limits:
memory: "4Gi"
cpu: "2000m"
livenessProbe:
exec:
command:
- pg_isready
- -U
- $(POSTGRES_USER)
- -d
- $(POSTGRES_DB)
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command:
- pg_isready
- -U
- $(POSTGRES_USER)
- -d
- $(POSTGRES_DB)
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: postgres-config
configMap:
name: postgres-config
# PVC template: ogni Pod ottiene il proprio volume da 100Gi
volumeClaimTemplates:
- metadata:
name: postgres-data
spec:
accessModes:
- ReadWriteOnce
storageClassName: fast-ssd
resources:
requests:
storage: 100Gi
ConfigMap per Configurazione PostgreSQL
# postgres-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: postgres-config
namespace: production
data:
postgresql.conf: |
# Performance tuning per 4GB RAM
shared_buffers = 1GB
work_mem = 64MB
maintenance_work_mem = 256MB
effective_cache_size = 3GB
# WAL settings
wal_level = replica
max_wal_senders = 5
wal_keep_size = 1GB
# Checkpoint
checkpoint_completion_target = 0.9
max_wal_size = 4GB
min_wal_size = 1GB
# Connection settings
max_connections = 200
# Logging
log_min_duration_statement = 1000 # log query lente >1s
log_checkpoints = on
log_connections = on
log_disconnections = on
Volume Snapshot e Backup
I Volume Snapshot permettono di creare backup point-in-time dei PersistentVolume usando le capacita native del provider cloud (snapshot EBS, GCE PD snapshot, etc.).
# Installa le CRD per Volume Snapshot (se non presenti)
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshotclasses.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshotcontents.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshots.yaml
# VolumeSnapshotClass
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: ebs-snapshot-class
driver: ebs.csi.aws.com
deletionPolicy: Retain
---
# Crea uno snapshot del volume di PostgreSQL
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: postgres-snapshot-20260801
namespace: production
spec:
volumeSnapshotClassName: ebs-snapshot-class
source:
persistentVolumeClaimName: postgres-data-postgres-0
---
# Verifica lo stato dello snapshot
kubectl get volumesnapshot -n production
# Restore da snapshot: crea un nuovo PVC da snapshot
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data-restored
namespace: production
spec:
accessModes:
- ReadWriteOnce
storageClassName: fast-ssd
resources:
requests:
storage: 100Gi
dataSource:
name: postgres-snapshot-20260801
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
Velero per Backup Completo del Cluster
Velero e lo strumento di riferimento per backup e restore di cluster Kubernetes interi, inclusi i PersistentVolume:
# Installa Velero con il plugin EBS
velero install \
--provider aws \
--plugins velero/velero-plugin-for-aws:v1.8.0 \
--bucket my-velero-backups \
--backup-location-config region=eu-west-1 \
--snapshot-location-config region=eu-west-1 \
--secret-file ./credentials-velero
# Crea un backup del namespace production con i volume
velero backup create production-backup-20260801 \
--include-namespaces production \
--snapshot-volumes \
--wait
# Verifica il backup
velero backup describe production-backup-20260801
velero backup logs production-backup-20260801
# Schedule: backup giornaliero a mezzanotte
velero schedule create daily-production \
--schedule="0 0 * * *" \
--include-namespaces production \
--snapshot-volumes \
--ttl 720h # mantieni per 30 giorni
# Restore in un nuovo cluster
velero restore create --from-backup production-backup-20260801 \
--namespace-mappings production:production-restored
Storage per Workload AI/ML
I workload di training ML hanno requisiti di storage particolari: accesso parallelo ad alto throughput per dataset di grandi dimensioni, spesso da piu GPU worker simultaneamente.
# PVC con ReadWriteMany per training distribuito
# Usa EFS (AWS) o CephFS (on-premise) per RWX
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ml-dataset-storage
namespace: ml-training
spec:
accessModes:
- ReadWriteMany
storageClassName: shared-storage # EFS o CephFS
resources:
requests:
storage: 10Ti # 10TB per dataset ImageNet, etc.
---
# Job di training che accede ai dati in parallelo
apiVersion: batch/v1
kind: Job
metadata:
name: distributed-training
namespace: ml-training
spec:
parallelism: 8 # 8 worker GPU in parallelo
completions: 8
template:
spec:
containers:
- name: trainer
image: pytorch/pytorch:2.3.0-cuda12.1-cudnn8-runtime
resources:
limits:
nvidia.com/gpu: "1"
volumeMounts:
- name: dataset
mountPath: /data
readOnly: true # tutti i worker leggono, nessuno scrive
- name: checkpoints
mountPath: /checkpoints
volumes:
- name: dataset
persistentVolumeClaim:
claimName: ml-dataset-storage
readOnly: true
- name: checkpoints
persistentVolumeClaim:
claimName: ml-checkpoints-rwx # RWX per checkpoint condivisi
Best Practices per lo Storage Kubernetes
Checklist Storage di Produzione
- Usa sempre reclaimPolicy: Retain per i dati di produzione.
Deleteelimina i dati automaticamente quando il PVC viene cancellato - volumeBindingMode: WaitForFirstConsumer: Evita i problemi di cross-AZ binding in cluster multi-zona
- allowVolumeExpansion: true: Configura le StorageClass per permettere l'espansione dei volumi senza downtime
- Monitora l'uso del disco: Configura alert su Prometheus quando un PVC supera l'80% di capacita
- Snapshot automatici: Configura VolumeSnapshotClass e backup schedulati
- Testa il restore: Un backup non testato e inutile. Fai restore test mensili
- Separa i PVC per ruolo: Un PVC per i dati, uno per i log, uno per i backup temporanei
- StatefulSet con anti-affinity: Distribui le repliche su nodi e zone diverse
Anti-Pattern: Non Fare Questo
- Non usare hostPath in produzione: Lega il Pod a un nodo specifico e non e portabile
- Non usare emptyDir per dati persistenti: Si cancella quando il Pod viene riavviato
- Non usare reclaimPolicy: Delete per dati di produzione: Puoi perdere tutto per errore
- Non montare lo stesso PVC (RWO) su piu Pod: Causa corruzione dei dati
Monitoraggio dello Storage con Prometheus
# Metriche chiave da monitorare con kube-state-metrics
# Aggiungi alert a Prometheus
# Alert: PVC vicino alla capacita massima
groups:
- name: kubernetes-storage
rules:
- alert: PVCStorageUsageHigh
expr: |
kubelet_volume_stats_used_bytes /
kubelet_volume_stats_capacity_bytes > 0.80
for: 5m
labels:
severity: warning
annotations:
summary: "PVC {{ $labels.persistentvolumeclaim }} e all'80% della capacita"
description: "Namespace: {{ $labels.namespace }}"
- alert: PVCStorageFull
expr: |
kubelet_volume_stats_used_bytes /
kubelet_volume_stats_capacity_bytes > 0.95
for: 2m
labels:
severity: critical
annotations:
summary: "PVC {{ $labels.persistentvolumeclaim }} quasi piena!"
- alert: PVCNotBound
expr: |
kube_persistentvolumeclaim_status_phase{phase="Pending"} == 1
for: 10m
labels:
severity: warning
annotations:
summary: "PVC {{ $labels.persistentvolumeclaim }} in stato Pending da 10 minuti"
Conclusioni e Prossimi Passi
Lo storage Kubernetes e uno dei layer piu critici per le applicazioni enterprise in produzione. Il Container Storage Interface standardizza l'integrazione con qualsiasi provider, il provisioning dinamico con StorageClass elimina il lavoro manuale, e i StatefulSet forniscono le primitive necessarie per gestire database con identita stabile.
La chiave per uno storage robusto in produzione e una combinazione di scelte architetturali corrette (reclaimPolicy Retain, WaitForFirstConsumer, anti-affinity), monitoring proattivo con Prometheus e una strategia di backup testata regolarmente con Velero o VolumeSnapshot.

Commenti
Caricamento commenti...
Accedi per commentare