FastAPI Hexagonal Code source

06. Le stockage

Ce qui arrive vraiment au disque — et pourquoi trois PostgreSQL sur trois nœuds ne font pas trois stockages indépendants.

TL;DR — Un pod réclame un 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 :

Pod PVC StorageClass provisionneur PV « il me faut » 1 Gi, RWO standard local-path un répertoire Le manifeste s'arrête au PVC. Tout ce qui suit dépend du cluster.

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 iciCe que ça implique
Provisionneurrancher.io/local-pathLe volume est un répertoire sur un nœud, pas un disque réseau
volumeBindingModeWaitForFirstConsumerLe PV n'est créé qu'une fois le pod planifié
reclaimPolicyDeleteSupprimer le PVC détruit les données
allowVolumeExpansionnon définiAgrandir un PVC est refusé
Snapshotsaucune VolumeSnapshotClassPas 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-worker

WaitForFirstConsumer : 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-worker3

Le volume est sur ce nœud, physiquement. Tout pod qui le réclame ira donc sur ce nœud, ou n'ira nulle part.

Ce qui complète le scénario de drain du chapitre 03

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 quoiCe qui se passe si un nœud meurt
Les données PostgreSQLla réplication en flux (WAL) de PostgreSQL, pilotée par CloudNativePGUn réplica est promu, la donnée survit — à la fenêtre de réplication près (RPO)
Le volumerienLe répertoire disparaît à la suppression du conteneur Kind ; une panne temporaire le rend inaccessible
Les messages RabbitMQrien (queues classiques, cf. ch. 03)Indisponibles tant que le nœud est arrêté ; perdus si son volume est détruit
Le cache ValkeyrienSans 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 iciCe que ça apporte
Un pilote CSI avec stockage réseauLe volume se détache d'un nœud et se rattache à un autre : le repli redevient possible
allowVolumeExpansion: trueAgrandir un PVC sans recréer la base. Le PVC demande 1 Gi ; local-path n’en fait pas un quota disque dur
Une VolumeSnapshotClassDes snapshots si le pilote les supporte ; leur cohérence applicative et leur durée de restauration doivent être vérifiées
reclaimPolicy: RetainUn 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
Où passent les volumes au nettoyage

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#