Salta al contenuto principale

02 - Storage Persistente in Kubernetes: CSI, PV, StorageClass e StatefulSet

Storage Persistente in Kubernetes: CSI, PV, StorageClass e StatefulSet Kubernetes e nato come piattaforma per workload stateless, ma la realta delle...

Cosa ne pensi di questo articolo?

02 - Storage Persistente in Kubernetes: CSI, PV, StorageClass e StatefulSet
Kubernetes AvanzatoFoto:panumas nikhomkhaiPexels

In Breve

Storage Persistente in Kubernetes: CSI, PV, StorageClass e StatefulSet Kubernetes e nato come piattaforma per workload stateless, ma la realta delle...

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. Delete elimina 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.

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti