06. Le stockage
Ce qui arrive vraiment au disque — et pourquoi trois PostgreSQL sur trois nœuds ne font pas trois stockages indépendants.
PVC, une
StorageClass désigne un provisionneur, le provisionneur fabrique un
PV. Sur Kind, ce PV est un répertoire sur un nœud :
il épingle le pod qui l'utilise, ne survit pas au nœud, ne se redimensionne pas et
ne se snapshote pas. La réplication des données vient de PostgreSQL, pas du
stockage. HA du calcul ≠ HA du stockage.
La chaîne, de la demande au disque#
Aucun manifeste de ce dépôt ne parle de disque. Ils demandent une quantité, et la chaîne se débrouille :
C'est la bonne abstraction : le même database.yaml tourne sur Kind
et sur un cloud, et ce qui change est la StorageClass, pas
l'application. Encore faut-il savoir ce que la classe fait réellement — parce
que les propriétés qu'elle apporte ou non décident du comportement en panne.
Ce que Kind fournit vraiment#
Une seule classe, marquée par défaut — d'où le fait qu'aucun manifeste du
dépôt ne précise de storageClassName :
$ kubectl get sc
NAME DEFAUT PROVISIONNEUR BINDING RECLAIM EXPANSION
standard true rancher.io/local-path WaitForFirstConsumer Delete <none>| Propriété | Valeur ici | Ce que ça implique |
|---|---|---|
| Provisionneur | rancher.io/local-path | Le volume est un répertoire sur un nœud, pas un disque réseau |
volumeBindingMode | WaitForFirstConsumer | Le PV n'est créé qu'une fois le pod planifié |
reclaimPolicy | Delete | Supprimer le PVC détruit les données |
allowVolumeExpansion | non défini | Agrandir un PVC est refusé |
| Snapshots | aucune VolumeSnapshotClass | Pas de snapshot possible — le CRD n'existe même pas |
À convergence, les manifests demandent sept PVC : trois PostgreSQL, trois RabbitMQ et un Valkey. L’extrait suivant est un relevé partiel ancien, où un seul nœud RabbitMQ avait créé son volume ; il ne décrit pas un cluster entièrement prêt.
$ kubectl get pv -o json | ...
task-manager-db-1 1Gi Delete noeud=task-manager-worker3
chemin=/var/local-path-provisioner/pvc-fc1a1ac0…_task-manager_task-manager-db-1
task-manager-db-2 1Gi Delete noeud=task-manager-worker
task-manager-db-3 1Gi Delete noeud=task-manager-worker2
valkey-data-valkey-0 1Gi Delete noeud=task-manager-worker
persistence-rabbitmq-server-2 1Gi Delete noeud=task-manager-workerWaitForFirstConsumer : l'ordre compte#
Immediate lie ou provisionne le volume dès la demande. Avec du stockage local, il peut imposer un nœud incompatible avec les autres contraintes du pod. Ce n’est pas un échec systématique : le scheduler tient aussi compte de l’affinité du PV. WaitForFirstConsumer permet de choisir conjointement le placement et le stockage.
WaitForFirstConsumer inverse l'ordre : le scheduler place le pod
d'abord, en tenant compte de l'anti-affinité et des ressources, et le provisionneur
crée ensuite le volume sur le nœud retenu. C'est ce qui fait cohabiter
l'anti-affinité required du chapitre 01 avec un stockage local.
Le volume épingle le pod — et ça change le scénario de drain#
Une fois créé, le PV porte une contrainte de placement qui n'est pas négociable :
# kubectl get pv … -o yaml, pour le volume de task-manager-db-1
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- task-manager-worker3Le volume est sur ce nœud, physiquement. Tout pod qui le réclame ira donc sur ce nœud, ou n'ira nulle part.
Le scénario de drain
observe qu'une instance PostgreSQL évincée reste en Pending, et
l'attribue à l'anti-affinité required. C'est vrai, mais incomplet :
même sans anti-affinité, elle resterait bloquée. Son volume est
un répertoire de task-manager-worker3, et un nœud cordonné n'accepte
plus de pod.
Deux mécanismes indépendants produisent donc le même symptôme, et il faut lever les deux pour que le repli fonctionne. Sur un vrai cluster, c'est le stockage réseau qui lève le second — le volume se détache et se rattache ailleurs. Avec du stockage local, la donnée ne voyage pas.
HA du calcul ≠ HA du stockage#
C'est la confusion que ce chapitre existe pour dissiper. Les trois instances PostgreSQL ont bien trois volumes sur trois nœuds distincts. On pourrait en conclure que la donnée est répliquée trois fois par le stockage. Elle ne l'est pas.
| Ce qui est répliqué | Par quoi | Ce qui se passe si un nœud meurt |
|---|---|---|
| Les données PostgreSQL | la réplication en flux (WAL) de PostgreSQL, pilotée par CloudNativePG | Un réplica est promu, la donnée survit — à la fenêtre de réplication près (RPO) |
| Le volume | rien | Le répertoire disparaît à la suppression du conteneur Kind ; une panne temporaire le rend inaccessible |
| Les messages RabbitMQ | rien (queues classiques, cf. ch. 03) | Indisponibles tant que le nœud est arrêté ; perdus si son volume est détruit |
| Le cache Valkey | rien | Sans importance : le cache est jetable |
Autrement dit, la résilience des données de ce dépôt ne doit rien au stockage : elle vient entièrement de l'applicatif qui réplique au-dessus. C'est une architecture parfaitement valide — c'est même celle que recommande CloudNativePG — mais il faut la nommer, parce qu'elle a une conséquence directe : perdre un nœud perd un volume, et seule la réplication PostgreSQL rend cette perte supportable.
Ce que la production ajoute#
| Manque ici | Ce que ça apporte |
|---|---|
| Un pilote CSI avec stockage réseau | Le volume se détache d'un nœud et se rattache à un autre : le repli redevient possible |
allowVolumeExpansion: true | Agrandir un PVC sans recréer la base. Le PVC demande 1 Gi ; local-path n’en fait pas un quota disque dur |
Une VolumeSnapshotClass | Des snapshots si le pilote les supporte ; leur cohérence applicative et leur durée de restauration doivent être vérifiées |
reclaimPolicy: Retain | Un kubectl delete pvc malheureux ne détruit plus les données |
| Des zones de disponibilité | Répartir les volumes sur des défaillances vraiment indépendantes — ce que Kind ne peut pas simuler |
k8s/clean.sh ne supprime pas les PVC : il détruit le cluster Kind,
donc les conteneurs qui servaient de nœuds, donc les répertoires qui servaient de
volumes. Le résultat est le même, par un chemin différent — et c'est aussi
pourquoi reclaimPolicy: Retain ne protégerait de rien ici : il n'y a
pas de disque qui survive au nœud.
À retenir#
- Le manifeste s'arrête au
PVC. Ce qu'on obtient dépend de la StorageClass, et donc du cluster : même YAML, garanties radicalement différentes. - Sur Kind, un PV est un répertoire sur un nœud, avec une
nodeAffinityqui épingle tout pod qui le réclame. - C'est la seconde raison pour laquelle une instance évincée
reste
Pendingau drain — l'anti-affinité n'est que la première. WaitForFirstConsumercoordonne le placement et le provisionnement ; il évite une affectation de volume trop précoce qui pourrait bloquer le pod.- HA du calcul ≠ HA du stockage. Ici, ce sont les WAL de PostgreSQL qui répliquent, pas le disque. RabbitMQ, lui, ne réplique rien.
- Ni expansion, ni snapshot : deux propriétés qu'on croit acquises et qui sont des options de la classe, pas de Kubernetes.