La couche données ne tient plus à un seul nœud : Patroni, et tout se met à flotter
Le billet précédent se terminait sur un aveu : 3 managers, ingress actif/actif, nœuds nomades… mais la couche données restait épinglée à rpi51. Quorum ≠ HA des données. Déplacer ou perdre rpi51 coupait toutes les apps à base de données. C’était le « prochain chantier ». Le voici.
Le principe : surtout pas un FS distribué
Le réflexe pour qu’un volume « suive » un service qui bouge, c’est un système de fichiers réseau partagé (GlusterFS, Ceph…). Pour une base de données, j’ai fait le calcul et il dit non : mettre Postgres sur du FS réseau, c’est échanger la latence locale du NVMe contre la latence réseau et un domaine de panne partagé.
La bonne approche est l’inverse : rendre le service mobile en sortant son état vers une couche déjà répliquée. Deux familles :
- L’état lourd à fort fsync (les bases) → réplication applicative : un cluster Postgres qui réplique lui-même, sur disque local rapide. C’est Patroni.
- L’état léger (config, clés, comptes) → on le régénère au démarrage depuis un secret / une base. J’appelle ça l’auth-as-code.
Patroni : chaque base devient un duo
Chaque base applicative est désormais un cluster Patroni à 2 membres — pg-a sur rpi51,
pg-b sur rpi52 — avec le leader élu via un etcd à 3 membres (un par manager). Devant
chaque cluster, un HAProxy qui garde l’alias que l’app utilisait déjà (vw-postgres,
n8n-postgres…) et route vers le membre que Patroni déclare leader (health-check = l’API
Patroni :8008/leader). Résultat : les chaînes de connexion des apps ne changent pas.
app ──▶ haproxy (alias inchangé) ──▶ pg-a (rpi51) ⇄ pg-b (rpi52)
health = /leader leader élu via etcd (quorum 2/3)
Un déplacement planifié de rpi51 devient un switchover de quelques secondes ; un crash, un failover automatique dès l’expiration du bail etcd. Migration par dump/restore dans des volumes neufs, l’ancien gardé en rollback le temps d’un soak.
Puis : faire flotter tout le reste
Patroni règle les bases. Restait à dé-épingler les apps elles-mêmes (db==true → apps==true).
Trois surprises en chemin :
n8n. Je voulais externaliser ses données binaires vers R2 (S3). Sauf que le mode S3 est une
fonction sous licence entreprise — le conteneur communautaire la refuse net. Repli sur le
mode default : les binaires vivent inline dans les données d’exécution, donc dans le Postgres
HA, bornés par la purge à 14 jours. Le volume local devient jetable.
ntfy. Son seul état durable est un auth.db SQLite (utilisateurs + ACL). Piège : la CLI de
ntfy ne sait pas créer cet auth.db — seul le serveur le fait au premier lancement. La parade :
un provision.sh qui démarre le serveur, attend l’auth.db, puis reconstruit les comptes
depuis des secrets Docker à chaque démarrage. Sur un nœud frais, ntfy se reprovisionne à
l’identique. Auth-as-code.
Vaultwarden — le gros chantier qui n’existait pas. Je redoutais celui-là : un gestionnaire
de mots de passe, avec (croyais-je) des pièces jointes à pousser sur S3 et une clé à manipuler.
J’ouvre le volume… et il ne contient que la clé de signature RSA (1,6 Ko), un cache d’icônes
jetable, et un config.json. Zéro pièce jointe. Tout le « chantier S3 » était imaginaire. La
clé RSA part dans un secret Docker (RSA_KEY_FILENAME pointe dessus), la config était déjà en
variables d’env, le volume devient jetable. Au passage, un contre-piège : chez Vaultwarden,
config.json écrase les variables d’env (l’inverse de l’intuition) — il a donc fallu le
supprimer pour que l’env redevienne la source de vérité. Et perdre la clé RSA ? Ça force une
reconnexion de tous, jamais une perte de données : le coffre est chiffré côté client avec la
clé maître de chaque utilisateur ; la clé RSA ne signe que les jetons d’auth.
Les drills : la seule preuve qui compte
Une archi HA non testée est une hypothèse. Trois drills :
- Déplacement gracieux de rpi51 —
make movebascule les leaders, les apps suivent. ✅ - Crash brutal de rpi51 (
reboot -f, sans évacuation) — failover etcd, les apps reschedulent. ✅ - Crash brutal de rpi52 — symétrique. ✅
Bilan : perdre n’importe lequel des deux nœuds de données, à chaud ou brutalement, ne coupe plus aucune app. Mais les drills ont surtout révélé ce que je ne cherchais pas.
Un SPOF caché. En préparant le crash de rpi52, je découvre que mes deux répliques de ce
site s’étaient empilées sur rpi52. La cause : spread: node.id n’est qu’une préférence de
placement, et Swarm ne rééquilibre jamais au retour d’un nœud — après les drills précédents,
les deux répliques avaient atterri du même côté. Un crash de rpi52 aurait coupé tout le site.
Correctif : max_replicas_per_node: 1 (anti-affinité dure). Le drill l’a ensuite validé en
direct — le site est resté à 200 pendant toute la fenêtre.
Une fausse panne que je me suis infligée. Pendant le crash, je monitorais en bouclant des
curl toutes les 3 secondes. Stickers s’est mis à répondre 429. Panique brève… puis les logs
tranchent : zone bot_limit, client = mon IP, user-agent = curl. Mon propre monitoring
avait épuisé le rate-limiter anti-bot. La détection de vraie IP marchait parfaitement, les vrais
utilisateurs n’ont rien vu. Le rate-limiter faisait son travail — c’est moi qui jouais au bot.
Bilan : la couche données promise au billet précédent est livrée. Chaque base est un cluster Patroni, chaque app flotte, et trois drills le prouvent. Plus aucun point unique de défaillance applicatif : on déplace ou on perd un nœud, le service tient. Le seul état encore épinglé est de l’infra reproductible (Prometheus, Portainer). Le code est public.
📬 La newsletter homelab
Les nouveaux articles + retours d'expérience self-hosting, sans spam. Désinscription en un clic.