13 — Stockage Kubernetes#
Le problème du stockage éphémère#
Un Pod Kubernetes est, par nature, éphémère. Quand un Pod est recréé — qu’il s’agisse d’un crash, d’une mise à jour rolling, ou d’un reschedule sur un autre nœud — toutes les données écrites dans son système de fichiers disparaissent.
Analogie : le château de sable
Un conteneur sans volume persistent, c’est comme un château de sable. Magnifique, fonctionnel, mais la prochaine marée (redémarrage) l’efface complètement. Si vous voulez que votre travail survive aux marées, il vous faut construire sur du rocher — c’est le rôle des volumes persistants.
Pour une application stateless (API REST sans état), c’est acceptable. Mais pour une base de données, un système de fichiers partagé, ou tout service qui doit mémoriser des données — il faut une solution de stockage persistant.
emptyDir : volume temporaire partagé#
emptyDir est le type de volume le plus simple : un répertoire créé vide au démarrage du Pod, partagé entre tous les conteneurs du Pod. Il est détruit quand le Pod disparaît.
apiVersion: v1
kind: Pod
metadata:
name: app-avec-sidecar
spec:
containers:
- name: app
image: mon-app:1.0
volumeMounts:
- name: shared-data
mountPath: /app/cache
- name: sidecar-log-shipper
image: fluentd:latest
volumeMounts:
- name: shared-data
mountPath: /var/log/app # Lit les logs produits par 'app'
volumes:
- name: shared-data
emptyDir:
medium: "" # Disque du nœud (par défaut)
# medium: Memory # Utilise la RAM (tmpfs) : plus rapide, mais limité
sizeLimit: 500Mi
Cas d’usage de emptyDir
emptyDir sur disque : cache temporaire, fichiers intermédiaires entre conteneurs (sidecar pattern).
emptyDir en mémoire (tmpfs) : données sensibles qui ne doivent pas toucher le disque (tokens, clés temporaires), ou cache haute performance.
hostPath : monter un répertoire du nœud#
hostPath monte un répertoire du nœud hôte dans le Pod. C’est puissant, mais dangereux.
volumes:
- name: host-data
hostPath:
path: /var/log/containers # Répertoire du nœud hôte
type: DirectoryOrCreate # Crée le dossier s'il n'existe pas
Danger de hostPath en production
hostPath donne au Pod un accès direct au système de fichiers du nœud. Un Pod malveillant pourrait monter / et lire tous les fichiers du nœud. En production, hostPath est généralement interdit par les PodSecurityAdmission policies. Réservez-le aux cas très spécifiques (DaemonSets de monitoring comme node-exporter).
PersistentVolume et PersistentVolumeClaim#
Kubernetes sépare la gestion du stockage physique (côté administrateur) de la demande de stockage (côté développeur) grâce à deux ressources complémentaires.
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[3], line 34
30 ax.text(12.0, 8.1, "Développeur\n/ application", ha='center', va='center',
31 fontsize=10, fontweight='bold', color='white')
33 # PV
---> 34 boite(ax, 3.0, 5.5, 4.5, 1.4, "PersistentVolume (PV)", "capacité: 50Gi | AccessMode: RWO\nstorageClass: fast-ssd | Reclaim: Retain", "#E67E22", fontsize=8)
36 # PVC
37 boite(ax, 11.0, 5.5, 4.5, 1.4, "PersistentVolumeClaim (PVC)", "capacité: ≥20Gi | AccessMode: RWO\nstorageClass: fast-ssd", "#4A90D9", fontsize=8)
TypeError: boite() got an unexpected keyword argument 'fontsize'
Manifestes PV et PVC#
# PersistentVolume (créé par l'admin)
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-postgres-data
spec:
capacity:
storage: 50Gi
accessModes:
- ReadWriteOnce # Un seul Pod peut monter en lecture-écriture
persistentVolumeReclaimPolicy: Retain # Garde les données après libération
storageClassName: fast-ssd
csi:
driver: pd.csi.storage.gke.io
volumeHandle: projects/mon-projet/zones/europe-west1-b/disks/postgres-disk
---
# PersistentVolumeClaim (créé par le dev)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-postgres
namespace: production
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi # Demande ≤ capacité du PV
storageClassName: fast-ssd # Doit correspondre à un PV ou une StorageClass
Les modes d’accès#
StorageClass : provisionnement dynamique#
Créer manuellement un PV pour chaque PVC devient vite fastidieux. La StorageClass automatise ce provisionnement.
Analogie : le distributeur automatique
Sans StorageClass, créer du stockage, c’est comme aller au sous-sol chercher un disque dur physique à chaque fois. Avec une StorageClass, c’est comme un distributeur automatique : vous déposez votre PVC (commande), et le provisioner crée le disque automatiquement dans le cloud.
# StorageClass : définit un "profil" de stockage
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
annotations:
storageclass.kubernetes.io/is-default-class: "true" # Utilisée si pas de storageClass spécifiée
provisioner: pd.csi.storage.gke.io # Driver CSI du cloud provider
parameters:
type: pd-ssd # SSD rapide
replication-type: regional-pd # Réplication multi-zone
zones: europe-west1-b,europe-west1-c
reclaimPolicy: Delete # Supprime le disque quand le PVC est supprimé
allowVolumeExpansion: true # Permet d'agrandir le volume en ligne
volumeBindingMode: WaitForFirstConsumer # Crée le disque quand le Pod est schedulé
# Lister les StorageClasses disponibles
kubectl get storageclasses
# NAME PROVISIONER RECLAIMPOLICY
# standard (default) kubernetes.io/gce-pd Delete
# fast-ssd pd.csi.storage.gke.io Delete
# slow-hdd kubernetes.io/gce-pd Retain
# Créer un PVC avec la StorageClass (le PV est créé automatiquement !)
kubectl apply -f pvc.yaml
kubectl get pvc
# NAME STATUS VOLUME CAPACITY
# pvc-postgres Bound pvc-abc-123-def-456 20Gi
Simulation d’un scheduler de PV#
import json
from typing import Optional
# Simulation du processus de binding PV ↔ PVC par le scheduler Kubernetes
class PersistentVolume:
def __init__(self, name, capacity_gi, access_modes, storage_class, status="Available"):
self.name = name
self.capacity_gi = capacity_gi
self.access_modes = access_modes
self.storage_class = storage_class
self.status = status
self.bound_to = None
def __repr__(self):
return (f"PV({self.name}, {self.capacity_gi}Gi, "
f"{'+'.join(self.access_modes)}, {self.storage_class}, {self.status})")
class PersistentVolumeClaim:
def __init__(self, name, namespace, requested_gi, access_modes, storage_class):
self.name = name
self.namespace = namespace
self.requested_gi = requested_gi
self.access_modes = access_modes
self.storage_class = storage_class
self.status = "Pending"
self.bound_pv = None
def __repr__(self):
return (f"PVC({self.name}, {self.requested_gi}Gi, "
f"{'+'.join(self.access_modes)}, {self.storage_class}, {self.status})")
class PVScheduler:
"""
Simule l'algorithme de binding PV/PVC du controller-manager Kubernetes.
Critères de sélection (dans l'ordre) :
1. storageClass doit correspondre
2. accessModes : le PV doit supporter TOUS les modes demandés
3. capacité : le PV doit avoir au moins la capacité demandée
4. statut : le PV doit être Available
5. Parmi les candidats : sélectionne le plus petit PV suffisant
"""
def __init__(self):
self.pvs = []
self.pvcs = []
self.binding_log = []
def add_pv(self, pv: PersistentVolume):
self.pvs.append(pv)
def add_pvc(self, pvc: PersistentVolumeClaim):
self.pvcs.append(pvc)
def find_best_pv(self, pvc: PersistentVolumeClaim) -> Optional[PersistentVolume]:
"""Trouve le PV le mieux adapté à un PVC."""
candidates = []
for pv in self.pvs:
if pv.status != "Available":
continue
if pv.storage_class != pvc.storage_class:
continue
# Vérifie que tous les accessModes du PVC sont dans le PV
if not all(mode in pv.access_modes for mode in pvc.access_modes):
continue
if pv.capacity_gi < pvc.requested_gi:
continue
candidates.append(pv)
if not candidates:
return None
# Sélectionne le plus petit PV suffisant (meilleur fit)
return min(candidates, key=lambda p: p.capacity_gi)
def bind(self, pvc: PersistentVolumeClaim, pv: PersistentVolume):
"""Lie un PVC à un PV."""
pv.status = "Bound"
pv.bound_to = pvc.name
pvc.status = "Bound"
pvc.bound_pv = pv.name
self.binding_log.append(f" BOUND : {pvc.name} ({pvc.requested_gi}Gi) → {pv.name} ({pv.capacity_gi}Gi)")
def schedule_all(self):
"""Lance le processus de binding pour tous les PVCs en attente."""
print("\n=== Scheduler PV/PVC — Résolution des bindings ===\n")
print(f"PVs disponibles ({len(self.pvs)}) :")
for pv in self.pvs:
print(f" {pv}")
print(f"\nPVCs en attente ({len(self.pvcs)}) :")
for pvc in self.pvcs:
print(f" {pvc}")
print("\n--- Résolution ---")
for pvc in self.pvcs:
best_pv = self.find_best_pv(pvc)
if best_pv:
self.bind(pvc, best_pv)
else:
pvc.status = "Pending"
self.binding_log.append(f" PENDING : {pvc.name} — aucun PV correspondant trouvé !")
print("\nRésultats :")
for log_entry in self.binding_log:
symbol = "✓" if "BOUND" in log_entry else "⚠"
print(f"{symbol} {log_entry}")
print("\nÉtat final des PVs :")
for pv in self.pvs:
bound_info = f" → {pv.bound_to}" if pv.bound_to else ""
print(f" {pv.name:20s} {pv.status:10s}{bound_info}")
# Création du scheduler et des ressources
scheduler = PVScheduler()
# PVs disponibles
scheduler.add_pv(PersistentVolume("pv-ssd-10gi", 10, ["ReadWriteOnce"], "fast-ssd"))
scheduler.add_pv(PersistentVolume("pv-ssd-50gi", 50, ["ReadWriteOnce"], "fast-ssd"))
scheduler.add_pv(PersistentVolume("pv-ssd-100gi", 100, ["ReadWriteOnce", "ReadOnlyMany"], "fast-ssd"))
scheduler.add_pv(PersistentVolume("pv-nfs-200gi", 200, ["ReadWriteMany", "ReadOnlyMany"], "nfs-shared"))
scheduler.add_pv(PersistentVolume("pv-hdd-500gi", 500, ["ReadWriteOnce"], "slow-hdd"))
# PVCs à résoudre
scheduler.add_pvc(PersistentVolumeClaim("pvc-postgres", "prod", 20, ["ReadWriteOnce"], "fast-ssd"))
scheduler.add_pvc(PersistentVolumeClaim("pvc-redis", "prod", 8, ["ReadWriteOnce"], "fast-ssd"))
scheduler.add_pvc(PersistentVolumeClaim("pvc-assets", "prod", 50, ["ReadWriteMany"], "nfs-shared"))
scheduler.add_pvc(PersistentVolumeClaim("pvc-backup", "prod", 200, ["ReadWriteOnce"], "slow-hdd"))
scheduler.add_pvc(PersistentVolumeClaim("pvc-impossible", "dev", 150, ["ReadWriteOnce"], "fast-ssd"))
scheduler.schedule_all()
CSI : Container Storage Interface#
Le CSI est le standard qui permet à n’importe quel vendor de stockage de développer un plugin compatible Kubernetes sans modifier le code du noyau K8s.
StatefulSet avec stockage#
Les StatefulSets sont conçus pour les applications avec état (bases de données, message queues). Ils garantissent une identité stable à chaque Pod et gèrent automatiquement les PVCs grâce aux volumeClaimTemplates.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: production
spec:
serviceName: "postgres" # Service headless obligatoire pour DNS stable
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
# La magie du StatefulSet : un PVC dédié par réplica
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 50Gi
# Résultat : 3 PVCs créés automatiquement :
# data-postgres-0 → Pod postgres-0 (toujours le même !)
# data-postgres-1 → Pod postgres-1
# data-postgres-2 → Pod postgres-2
Backup et restauration : Velero#
Velero : sauvegarde des ressources et des volumes
Velero est l’outil de référence pour sauvegarder un cluster Kubernetes. Il peut sauvegarder à la fois les ressources K8s (manifestes) et les données des volumes (via les snapshots CSI ou une copie de fichiers avec Restic/Kopia).
# Installation de Velero (avec le plugin AWS S3)
velero install \
--provider aws \
--plugins velero/velero-plugin-for-aws:v1.8.0 \
--bucket mon-bucket-backup \
--backup-location-config region=eu-west-1
# Créer une sauvegarde complète du namespace production
velero backup create backup-prod-20260321 \
--include-namespaces production \
--snapshot-volumes
# Lister les sauvegardes
velero backup get
# NAME STATUS ERRORS CREATED EXPIRES
# backup-prod-20260321 Completed 0 2026-03-21 10:30:00 29d
# Restaurer depuis une sauvegarde
velero restore create --from-backup backup-prod-20260321
# Snapshots CSI natifs (alternative moderne à Velero pour les volumes)
kubectl apply -f - <<EOF
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: snapshot-postgres-20260321
spec:
volumeSnapshotClassName: csi-aws-vsc
source:
persistentVolumeClaimName: data-postgres-0
EOF
Récapitulatif#
Dans ce chapitre, nous avons résolu le problème du stockage éphémère en explorant l’écosystème complet de Kubernetes : de emptyDir pour les besoins temporaires aux StatefulSets avec PVCs dédiés pour les bases de données. Le prochain chapitre aborde l’exposition des services vers le monde extérieur via Ingress et Gateway API.