Stockage
Faire survivre des données à des Pods conçus pour disparaître.
Stockage persistant
Tout ce que vous avez appris jusqu’ici repose sur une hypothèse commode : les Pods sont interchangeables et jetables. Cette hypothèse est exacte pour une API sans état, et elle s’effondre dès qu’il y a des données à conserver. Le système de fichiers d’un conteneur meurt avec lui : détruisez le Pod d’une base PostgreSQL déployée naïvement, et les tables sont perdues. Pas corrompues — perdues.
Il ne suffit pas de « monter un volume ». La difficulté est que dans un cluster, le disque et le calcul sont séparés : un Pod peut être reprogrammé sur un autre nœud, et un répertoire local du nœud d’origine devient alors inaccessible. Le stockage doit donc être une ressource du cluster, pas du nœud, et cette ressource doit suivre le Pod.
Kubernetes répond par un découplage en trois objets qui déroute au premier abord mais qui a une logique nette. Le PersistentVolume représente un morceau de stockage réel. Le PersistentVolumeClaim est une demande formulée par l’application : « il me faut 10 Gio en lecture-écriture ». La StorageClass décrit comment ces volumes sont fabriqués à la demande. Le développeur n’écrit que la réclamation ; il ne sait pas — et n’a pas besoin de savoir — s’il obtiendra un disque EBS, un volume Ceph ou un répertoire local.
Puis vient une question qu’on ne peut pas éluder : même avec un volume qui survit, un Deployment reste inadapté à une base de données. Ses Pods sont anonymes, créés en parallèle, et se partageraient le même volume. Le StatefulSet existe pour ce cas précis, et cette leçon explique pourquoi il n’est pas un simple Deployment amélioré.
Les volumes éphémères, et leur bon usage
Avant le stockage persistant, un mot sur emptyDir, souvent mal compris. Il crée un répertoire vide au démarrage du Pod, partagé entre ses conteneurs, et détruit à la disparition du Pod.
apiVersion: v1
kind: Pod
metadata:
name: pod-avec-cache
spec:
containers:
- name: app
image: mon-api:1.0.0
volumeMounts:
- name: cache
mountPath: /var/cache/app
- name: collecteur-logs
image: busybox:1.36
command: ["sh", "-c", "tail -F /var/cache/app/app.log"]
volumeMounts:
- name: cache
mountPath: /var/cache/app
volumes:
- name: cache
emptyDir:
sizeLimit: 500Mi
Ce n’est pas un pis-aller : c’est le bon outil pour un cache, un répertoire temporaire, ou un espace d’échange entre deux conteneurs du même Pod. emptyDir devient une erreur seulement quand on lui confie des données qui doivent survivre. Ajoutez medium: Memory pour un tmpfs en RAM — rapide, mais décompté de la limite mémoire du Pod.
PersistentVolume et PersistentVolumeClaim
Le volume, côté infrastructure
Un PersistentVolume est un objet du cluster, indépendant de tout Pod. Il a son propre cycle de vie.
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-donnees-01
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: manuel
hostPath:
path: /mnt/donnees/pv-01 # démonstration locale uniquement
Le hostPath mérite un avertissement franc : il pointe un répertoire du nœud, donc le volume n’est utilisable que par un Pod placé sur ce nœud. Il convient à un cluster local à un nœud pour comprendre le mécanisme, et il est à proscrire ailleurs. En production, le champ de type est remplacé par un pilote CSI qui parle au stockage réel du fournisseur.
La réclamation, côté application
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: donnees-app
namespace: atelier
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: manuel # doit correspondre à celui du PV visé
Kubernetes cherche un PersistentVolume compatible — capacité suffisante, mode d’accès compatible, même storageClassName — et effectue la liaison. Un PV lié à un PVC est réservé exclusivement : aucun autre PVC ne l’obtiendra.
kubectl get pv
kubectl get pvc -n atelier
# STATUS = Pending ? La cause est dans les événements.
kubectl describe pvc donnees-app -n atelier
Un PVC bloqué en Pending a presque toujours l’une de ces trois causes : aucun PV avec assez de capacité, un storageClassName qui ne correspond à rien, ou un mode d’accès que le stockage ne prend pas en charge. Les événements de describe le disent explicitement — c’est un des rares endroits où le message d’erreur de Kubernetes est directement exploitable.
Utiliser le PVC dans un Pod
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-avec-donnees
spec:
replicas: 1 # voir plus bas : ReadWriteOnce impose 1 réplique
selector:
matchLabels:
app: app-donnees
template:
metadata:
labels:
app: app-donnees
spec:
containers:
- name: app
image: mon-api:1.0.0
volumeMounts:
- name: donnees
mountPath: /var/lib/app/donnees
volumes:
- name: donnees
persistentVolumeClaim:
claimName: donnees-app
Le conteneur voit un répertoire ordinaire. Toute la mécanique de liaison lui est invisible, et c’est précisément l’objectif du découplage.
Les modes d’accès
C’est le champ le plus souvent mal compris, parce qu’il ne dépend pas de Kubernetes mais de la technologie de stockage sous-jacente.
| Mode | Abréviation | Signification |
|---|---|---|
ReadWriteOnce | RWO | montable en lecture-écriture par un seul nœud |
ReadOnlyMany | ROX | montable en lecture seule par plusieurs nœuds |
ReadWriteMany | RWX | montable en lecture-écriture par plusieurs nœuds |
ReadWriteOncePod | RWOP | un seul Pod, même sur le même nœud |
Ce que votre stockage accepte réellement se lit sur les volumes existants, jamais dans une documentation générique :
kubectl get pv -o custom-columns=NOM:.metadata.name,MODES:.spec.accessModes,CLASSE:.spec.storageClassName
Deux précisions décisives. D’abord, ReadWriteOnce porte sur le nœud, pas sur le Pod : plusieurs Pods d’un même nœud peuvent partager un volume RWO. C’est ReadWriteOncePod qui garantit l’exclusivité au Pod, ce que veut une base de données.
Ensuite, et c’est ce qui bloque les débutants : la grande majorité des stockages par blocs ne fournit que RWO. Les disques EBS d’AWS, les Persistent Disks de Google, les Managed Disks d’Azure ne s’attachent qu’à une machine à la fois. ReadWriteMany exige un système de fichiers réseau — NFS, AWS EFS, Azure Files, CephFS — souvent plus lent et toujours à provisionner séparément.
Conséquence pratique immédiate : un Deployment avec replicas: 3 et un PVC en ReadWriteOnce ne fonctionnera pas de façon fiable. Si les Pods atterrissent sur des nœuds différents, ceux qui ne sont pas sur le nœud propriétaire du volume restent en Pending ou en ContainerCreating. Ce n’est pas une bizarrerie de Kubernetes, c’est la nature du stockage par blocs.
Les StorageClass et le provisionnement dynamique
Créer les PersistentVolumes à la main ne passe pas l’échelle. Une StorageClass décrit un type de stockage, et un provisionneur fabrique les volumes à la demande, au moment où un PVC arrive.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: rapide-ssd
provisioner: kubernetes.io/no-provisioner # à remplacer par le pilote réel
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Delete
allowVolumeExpansion: true
Trois champs à comprendre plutôt qu’à recopier :
provisionerdésigne le pilote qui crée le stockage. Il est spécifique au fournisseur :ebs.csi.aws.comsur AWS,pd.csi.storage.gke.iosur Google Cloud,disk.csi.azure.comsur Azure,rancher.io/local-pathavec le provisionneur local de kind.volumeBindingMode: WaitForFirstConsumerretarde la création du volume jusqu’à ce qu’un Pod soit ordonnancé. C’est presque toujours le bon réglage : le volume est alors créé dans la même zone de disponibilité que le Pod. AvecImmediate, un volume peut naître dans une zone où le Pod ne pourra jamais être placé, et le Pod reste bloqué.allowVolumeExpansion: trueautorise l’agrandissement ultérieur du PVC. Sans ce champ, la seule voie est sauvegarde, nouveau volume, restauration.
Avec une StorageClass, le PVC devient plus court, et aucun PV n’est écrit à la main :
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: donnees-postgres
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
storageClassName: rapide-ssd
# Les classes disponibles ; (default) marque celle utilisée si on omet le champ
kubectl get storageclass
# Le PV a été créé automatiquement au moment de la liaison
kubectl get pv
Dites-le clairement : ce que vous obtenez dépend entièrement du fournisseur. La même StorageClass n’existe pas d’un cluster à l’autre, un manifeste avec storageClassName: gp3 échoue partout ailleurs que sur AWS, et il n’y a pas de nom universel. C’est le point où la portabilité de Kubernetes s’arrête, et il vaut mieux le savoir avant de promettre un déploiement multi-cloud.
Omettre storageClassName fait appliquer la classe marquée par défaut, si le cluster en a une. C’est commode en développement, et fragile dès qu’un manifeste voyage.
La politique de récupération
Elle décide du sort du stockage réel quand le PVC est supprimé :
Delete: le volume et les données sont supprimés. C’est la valeur par défaut du provisionnement dynamique.Retain: le volume subsiste avec ses données, en statutReleased. Il faut intervenir à la main pour le réutiliser.
Pour toute donnée qui compte, mettez Retain. C’est votre seule protection contre un kubectl delete pvc malheureux — et vos sauvegardes, qui restent indispensables : Kubernetes ne sauvegarde rien.
# Basculer un PV existant en Retain
kubectl patch pv pv-donnees-01 \
-p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'
Pourquoi un Deployment ne convient pas à une base de données
Rassemblons les raisons, parce que c’est la question la plus fréquente et qu’elle mérite une réponse précise.
- Les Pods d’un Deployment sont anonymes. Leur nom contient un suffixe aléatoire qui change à chaque recréation. Une réplication PostgreSQL a besoin d’adresser une instance précise, de façon stable.
- Ils partagent le même PVC. Le template de Pod est unique, donc
claimNameest le même pour tous. Trois répliques écriraient dans le même volume, ce qui corrompt une base — quand le stockage l’autorise, ce que RWO n’autorise généralement pas. - Ils sont créés et détruits en parallèle, dans un ordre indéfini. Une base avec un primaire et des secondaires exige que le primaire soit prêt avant les secondaires.
- La suppression détruit tout. Aucune notion d’identité stable ne survit.
Le symptôme est immédiat sur un Deployment à trois répliques partageant un PVC en ReadWriteOnce :
kubectl get pods -l app=postgres
# postgres-6b9f7c4d8-2xk4m 1/1 Running (sur le nœud du volume)
# postgres-6b9f7c4d8-7fd2p 0/1 ContainerCreating (volume déjà attaché ailleurs)
# postgres-6b9f7c4d8-q8v1t 0/1 ContainerCreating
Le StatefulSet
Le StatefulSet répond point par point à cette liste.
apiVersion: v1
kind: Service
metadata:
name: postgres
labels:
app: postgres
spec:
clusterIP: None # Service « headless » : requis par le StatefulSet
selector:
app: postgres
ports:
- port: 5432
name: postgres
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres # doit désigner le Service headless ci-dessus
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16-alpine
ports:
- containerPort: 5432
name: postgres
env:
- name: POSTGRES_DB
value: "appdb"
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: DB_PASSWORD
# PostgreSQL exige un sous-répertoire quand le point de montage
# contient un lost+found : sans PGDATA, l'initialisation échoue.
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: donnees
mountPath: /var/lib/postgresql/data
readinessProbe:
exec:
command: ["pg_isready", "-U", "app_user", "-d", "appdb"]
initialDelaySeconds: 10
periodSeconds: 5
volumeClaimTemplates:
# Un PVC distinct est créé pour CHAQUE réplique, automatiquement.
- metadata:
name: donnees
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
Ce que ce manifeste produit, et qu’aucun Deployment ne produit :
kubectl get pods -l app=postgres
# postgres-0, postgres-1, postgres-2 — noms stables, ordinaux, prévisibles
kubectl get pvc
# donnees-postgres-0, donnees-postgres-1, donnees-postgres-2
Quatre garanties concrètes :
- Identité stable.
postgres-1recréé s’appelle encorepostgres-1et retrouvedonnees-postgres-1, avec les mêmes données. - Volume dédié par réplique, grâce à
volumeClaimTemplates. Plus de partage, plus de corruption. - Ordre garanti. Création de 0, puis 1, puis 2, chaque Pod attendant que le précédent soit prêt. Suppression dans l’ordre inverse.
- DNS individuel. Le Service headless donne à chaque Pod un nom joignable :
postgres-0.postgres.atelier.svc.cluster.local. C’est ce qui permet de configurer une réplication.
Deux points de vigilance. D’abord, les PVC créés par volumeClaimTemplates ne sont pas supprimés avec le StatefulSet : c’est une protection voulue, et cela signifie qu’un nettoyage complet demande une suppression explicite.
kubectl delete statefulset postgres
kubectl get pvc # les PVC sont toujours là, données intactes
kubectl delete pvc donnees-postgres-0 # suppression explicite si voulue
Ensuite, une remarque d’honnêteté sur l’exploitation : un StatefulSet fournit une identité et un volume stables, rien de plus. Il ne configure pas la réplication, ne gère pas les bascules de primaire, ne sauvegarde rien. Pour une base en production, on utilise un opérateur dédié (CloudNativePG, Zalando Postgres Operator) qui apporte cette logique, ou un service managé hors du cluster. Le StatefulSet est la fondation, pas la solution complète.
Agrandir un volume
Si la StorageClass porte allowVolumeExpansion: true, il suffit d’augmenter la demande :
kubectl patch pvc donnees-postgres-0 \
-p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'
kubectl get pvc donnees-postgres-0
Trois limites à connaître : le rétrécissement est impossible, certains pilotes exigent un redémarrage du Pod pour que le système de fichiers soit étendu, et l’opération ne fonctionne pas du tout sur un PV provisionné à la main. Dimensionnez donc avec une marge dès le départ.
À retenir
- Le système de fichiers d’un conteneur meurt avec lui.
emptyDirest fait pour les caches et les échanges entre conteneurs d’un Pod, jamais pour des données à conserver. - Le triptyque PV / PVC / StorageClass découple l’infrastructure de l’application : le développeur n’écrit qu’une réclamation, le provisionneur fabrique le volume. Un PVC en
Pendings’explique presque toujours par une classe inexistante ou un mode d’accès non pris en charge. ReadWriteOnceporte sur le nœud et c’est le seul mode qu’offrent la plupart des stockages par blocs.ReadWriteManyexige un système de fichiers réseau à provisionner séparément — un Deployment multi-réplique sur un PVC RWO ne marchera pas.- La StorageClass dépend du fournisseur : provisionneur, noms de classes et politiques de récupération changent d’un cloud à l’autre. Mettez
Retainsur ce qui compte, et sauvegardez : Kubernetes ne sauvegarde rien. - Pour une base de données, il faut un StatefulSet : noms ordinaux stables, un PVC par réplique via
volumeClaimTemplates, démarrage ordonné et DNS individuel par Service headless. Il fournit la fondation, pas la réplication ni les bascules — cela relève d’un opérateur ou d’un service managé.
Conseil : exercez-vous à supprimer un Pod de StatefulSet (
kubectl delete pod postgres-1) et à vérifier qu’il revient avec le même nom et les mêmes données. C’est la démonstration de ce que le Deployment ne peut pas offrir.
Commentaires
Les commentaires sont alimentés par GitHub Discussions
Connectez-vous avec GitHub pour participer à la discussion