Aller au contenu

Mettre en place un cluster PostgreSQL haute disponibilité à 3 nœuds avec Patroni et etcd

25 juin 2026 · 8 min read · Read in English

Sommaire

Construire un cluster PostgreSQL haute disponibilité (Patroni + etcd + HAProxy + Keepalived)

PostgreSQL seul est un point unique de défaillance : si le serveur tombe, l'application tombe. Cet article montre comment monter un cluster à 3 nœuds qui survit à la perte d'une machine sans intervention humaine et sans changer la chaîne de connexion côté applications.

L'objectif n'est pas de copier-coller des commandes sans comprendre, mais de saisir pourquoi chaque brique existe, pour pouvoir adapter le tout à votre propre réseau.

Le résultat : une seule IP virtuelle (la VIP) sur laquelle les applications se connectent comme sur une base normale. Derrière, trois serveurs se surveillent en permanence ; en cas de panne du primaire, un réplica est promu en quelques secondes et la VIP bascule automatiquement.


Partie 1 — L'architecture et pourquoi ce stack

Le problème de la haute disponibilité

Pour qu'une base reste disponible quand un serveur tombe, il faut résoudre quatre problèmes distincts :

  1. Stocker la donnée plusieurs fois → réplication PostgreSQL (un primaire écrit, des réplicas suivent).
  2. Décider qui est le primaire, et le changer sans conflit quand il tombe → un orchestrateur (Patroni).
  3. Se mettre d'accord à plusieurs sur cette décision, même en cas de coupure réseau → un magasin à consensus (etcd).
  4. Diriger les clients vers le bon serveur sans qu'ils sachent qui est primaire → un routeur (HAProxy) + une adresse unique qui suit le service (VIP via Keepalived).

Chaque composant règle exactement un de ces problèmes. C'est pour ça qu'il y en a quatre.

Les quatre composants

ComposantRôleCe qu'il résout
PostgreSQLLe moteur de base de donnéesStocke et réplique la donnée
etcdMagasin clé-valeur distribué (le « DCS »)Source de vérité partagée : qui est le leader
PatroniOrchestrateur au-dessus de PostgreSQLBootstrap, réplication, élection, failover
HAProxyProxy TCPRoute chaque connexion vers le primaire courant
KeepalivedGestion d'IP virtuelle (VRRP)Fait flotter une IP unique vers un nœud sain

Pourquoi trois nœuds, et pas deux

etcd (comme tout système à consensus) décide à la majorité. Avec 3 nœuds, la majorité est 2 : le cluster continue de fonctionner si un nœud tombe. Avec 2 nœuds, perdre un nœud fait perdre la majorité — plus personne ne peut décider qui est primaire, et le cluster se fige par sécurité (pour éviter le split-brain, deux primaires qui divergent).

Règle simple : un nombre impair de nœuds. Trois est le minimum utile.

Le chemin d'une requête

Application
    │  se connecte à la VIP (ex: 192.168.11.50:5432)
    ▼
Keepalived  ── la VIP est montée sur le nœud sain prioritaire
    ▼
HAProxy     ── teste les 3 nœuds, n'envoie qu'au PRIMAIRE
    ▼
PostgreSQL (primaire)  ←── répliquent ──  PostgreSQL (réplicas ×2)
    ▲
Patroni     ── surveille, et via etcd décide qui est primaire

Point clé : HAProxy ne devine pas qui est primaire. Il interroge l'API REST de Patroni sur chaque nœud (GET /primary). Seul le nœud réellement primaire répond 200. Les réplicas répondent autre chose et sont écartés. Quand le failover se produit, c'est l'ancien réplica devenu primaire qui se met à répondre 200, et HAProxy redirige le trafic — automatiquement, sans reconfiguration.

Sécurité : TLS partout

Tout le trafic interne est chiffré et authentifié par certificats X.509 :

  • etcd exige des certificats mutuels (peer-to-peer et client). Une CA privée signe un certificat par nœud.
  • PostgreSQL utilise un certificat partagé dont les SAN couvrent la VIP + les 3 IP + localhost, pour que la connexion soit valide quel que soit le nœud qui sert.
  • pg_hba n'autorise que des connexions hostssl (jamais en clair), restreintes au sous-réseau du cluster.

Partie 2 — Préparation : réseau, configuration, pré-vol

Tout le déploiement tient dans deux scripts et un fichier de configuration :

  • cluster.envtoutes les variables (IP, ports, secrets, version PG). C'est le seul fichier à éditer.
  • preflight.sh — vérifie que la machine est prête, n'écrit rien.
  • deploy.sh — installe et configure tout. Idempotent : rejouable sans casser un cluster existant.

Les trois fichiers sont dans le repo des labs du blog, sous-dossier postgresql-ha-cluster. Récupérez-les :

git clone https://github.com/denisakp/dev-life-labs
cd dev-life-labs/postgresql-ha-cluster

C'est ce dossier que vous copierez ensuite sur chaque nœud (voir Partie 3).

Pré-requis réseau

Avant tout, il faut :

  • 3 serveurs Debian/Ubuntu sur le même sous-réseau, avec IP fixes.
  • Une IP libre dans ce sous-réseau pour la VIP — vérifiez-la :
ping -c2 192.168.11.50    # NE doit PAS répondre → la VIP est libre
  • Le nom de l'interface réseau de chaque serveur (souvent ens3, enp0s3, eth0…) :
ip link show

Si l'interface (IFACE) est mal renseignée, la VIP ne montera jamais. C'est l'erreur n°1.

Le fichier de configuration

On part du modèle :

cp cluster.env.example cluster.env && chmod 600 cluster.env

Les champs essentiels :

# Identité
CLUSTER_NAME="postgresql-cluster"
PG_VERSION="18"
ETCD_VERSION="v3.6.12"

# Réseau
SUBNET="192.168.11.0/24"      # sous-réseau du cluster
VIP="192.168.11.50"           # IP virtuelle (entrée des applications)
IFACE="ens3"                  # interface qui portera la VIP
APP_SUBNET="192.168.11.0/24"  # d'où viennent les clients

# Nœuds
DB1_NAME="db1"; DB1_IP="192.168.11.45"   # primaire initial, priorité VRRP 100
DB2_NAME="db2"; DB2_IP="192.168.11.46"   # secours, priorité 90
DB3_NAME="db3"; DB3_IP="192.168.11.47"   # secours, priorité 80

# Secrets — générer chacun avec :  openssl rand -base64 24
PG_SUPERUSER_PASSWORD="..."
PG_REPLICATOR_PASSWORD="..."
PATRONI_RESTAPI_PASSWORD="..."
VRRP_AUTH_PASS="........"     # ATTENTION : max 8 caractères (VRRP tronque au-delà)

Deux pièges qui reviennent toujours :

  • VRRP_AUTH_PASS ≤ 8 caractères. Le protocole VRRP tronque silencieusement ; deux nœuds avec des mots de passe différents après troncature ne se voient pas et la VIP peut se dédoubler.
  • cluster.env doit être identique sur les 3 serveurs, secrets compris. Les certificats et l'authentification reposent dessus.

Les priorités VRRP

DB1=100 > DB2=90 > DB3=80. Keepalived monte la VIP sur le nœud sain de plus haute priorité. db1 l'a par défaut ; s'il tombe, db2 la récupère, puis db3. Quand db1 revient, il reprend la VIP (priorité la plus haute). Ces priorités sont indépendantes du rôle PostgreSQL : la VIP suit la santé HAProxy, pas le primaire — mais comme HAProxy ne route que vers le primaire, le résultat est cohérent de bout en bout.

Le pré-vol

À lancer sur chaque nœud avant le déploiement :

sudo ./preflight.sh db1

Il contrôle, sans rien modifier : secrets remplis (pas de CHANGE_ME), VIP dans le sous-réseau, interface présente, IP du nœud correcte, autres nœuds joignables, ports libres, RAM/disque suffisants, horloge synchronisée (NTP) — critique : une dérive d'horloge entre nœuds rend le failover instable.

  • [FAIL] = bloquant, à corriger avant de continuer.
  • [WARN] = vérifiable, mais le déploiement reste possible.

Partie 3 — Déploiement, vérification, failover

L'ordre est impératif : db1 → distribution des certificats → db2 → db3. db1 génère la CA et les certificats de tout le monde ; db2 et db3 ne peuvent pas démarrer sans les recevoir.

Étape 1 — Déployer db1

sudo ./preflight.sh db1
sudo ./deploy.sh db1 --no-upgrade

--no-upgrade saute le apt upgrade global. Recommandé si les VM hébergent d'autres services (on ne veut pas mettre à jour tout le système au passage). Retirez-le pour un serveur dédié neuf.

Ce que fait deploy.sh sur db1, dans l'ordre : mise à jour ciblée → installation des paquets (PostgreSQL, Patroni, HAProxy, Keepalived) + binaire etcd → génération de la CA et de tous les certificats → configuration et démarrage d'etcd → configuration de Patroni, HAProxy, Keepalived → sauvegarde etcd quotidienne planifiée.

À la fin, db1 n'est pas encore primaire : etcd attend le quorum (≥ 2 nœuds). C'est normal. Le script génère deux archives certs-db2.tar.gz et certs-db3.tar.gz.

Étape 2 — Distribuer les certificats

Le déploiement ne fait aucun SSH automatique (par choix : pas de clés à propager, l'authentification reste interactive). Vous copiez vous-même les archives :

scp certs-db2.tar.gz user@192.168.11.46:/tmp/
scp certs-db3.tar.gz user@192.168.11.47:/tmp/
# et le cluster.env, identique :
scp cluster.env user@192.168.11.46:~/dev-life-labs/postgresql-ha-cluster/
scp cluster.env user@192.168.11.47:~/dev-life-labs/postgresql-ha-cluster/

Étape 3 — Déployer db2 puis db3

Sur db2 (puis, à l'identique, sur db3) — cloner le repo, puis déployer :

git clone https://github.com/denisakp/dev-life-labs
cd dev-life-labs/postgresql-ha-cluster
chmod 600 cluster.env
sudo ./preflight.sh db2
sudo ./deploy.sh db2 --no-upgrade

deploy.sh détecte l'archive dans /tmp, extrait les certificats, configure le nœud et rejoint le cluster. Dès que db2 démarre, le quorum etcd se forme (2 nœuds), Patroni élit db1 primaire et db2 se synchronise en réplica. db3 rejoint ensuite de la même façon.

Étape 4 — Vérifier

Depuis n'importe quel nœud :

# 1 Leader + 2 Replica en streaming
patronictl -c /etc/patroni/config.yml list
+ Cluster: postgresql-cluster ------+---------+----+-----------+
| Member | Host          | Role    | State   | TL | Lag in MB |
+--------+---------------+---------+---------+----+-----------+
| db1    | 192.168.11.45 | Leader  | running |  1 |           |
| db2    | 192.168.11.46 | Replica | running |  1 |         0 |
| db3    | 192.168.11.47 | Replica | running |  1 |         0 |
+--------+---------------+---------+---------+----+-----------+
# etcd : 3 membres healthy
etcdctl --endpoints=https://192.168.11.45:2379,https://192.168.11.46:2379,https://192.168.11.47:2379 \
  --cacert=/etc/etcd/ssl/ca.crt --cert=/etc/etcd/ssl/etcd-db1.crt --key=/etc/etcd/ssl/etcd-db1.key \
  endpoint health

# Connexion via la VIP → doit retourner 'f' (primaire, en lecture/écriture)
PGPASSWORD='<superuser>' psql -h 192.168.11.50 -U postgres -c 'SELECT pg_is_in_recovery();'

# La VIP est bien montée sur le primaire
ip addr show ens3 | grep 192.168.11.50

pg_is_in_recovery() qui renvoie f confirme que la VIP mène bien au primaire inscriptible.

Étape 5 — Pare-feu

Sur les 3 nœuds, restreindre les ports du cluster à son sous-réseau :

sudo ufw allow from 192.168.11.0/24 to any port 2379,2380,5432,5433,8008 proto tcp
# VRRP entre les nœuds :
sudo ufw allow from 192.168.11.0/24 to any proto vrrp 2>/dev/null || \
  sudo ufw allow from 192.168.11.0/24 to 224.0.0.18
PortService
2379 / 2380etcd (client / peer)
5432HAProxy — le port exposé aux applications
5433PostgreSQL interne (géré par Patroni)
8008API REST Patroni (health check HAProxy)

Le test qui valide tout : le failover

C'est l'intérêt même du cluster. On simule la panne du primaire :

# Repérer le leader
patronictl -c /etc/patroni/config.yml list

# Sur le nœud leader : couper Patroni
sudo systemctl stop patroni

# Quelques secondes plus tard, sur un autre nœud :
patronictl -c /etc/patroni/config.yml list   # → un réplica est devenu Leader

Ce qui se passe en quelques secondes, sans intervention : Patroni perd le bail (lease) du leader dans etcd → un réplica se porte candidat et est élu → il est promu primaire → son API répond désormais 200 sur /primary → HAProxy redirige le trafic → la VIP reste sur un nœud où HAProxy est sain. Les applications, connectées à la VIP, ne voient qu'une brève coupure.

On réintègre l'ancien primaire :

sudo systemctl start patroni   # il revient comme Replica et se resynchronise

Récapitulatif

BriqueSans elle…
PostgreSQL + réplicationpas de copie de la donnée
etcd (quorum 3)pas d'accord sur qui est primaire → split-brain
Patronipas d'élection ni de failover automatiques
HAProxyles clients ne savent pas qui est primaire
Keepalived (VIP)la chaîne de connexion devrait changer à chaque bascule

Pour adapter à votre environnement, vous ne touchez qu'à un seul fichier (cluster.env) : sous-réseau, IP, interface, secrets. Le reste — certificats, configuration des quatre services, planification des sauvegardes — est généré par deploy.sh, qui est idempotent : en cas d'erreur, corrigez et relancez sudo ./deploy.sh dbX --no-upgrade.

Pièges à retenir :

  • Ordre obligatoire : db1 → certs → db2 → db3.
  • IFACE correct, sinon la VIP ne monte pas.
  • VRRP_AUTH_PASS ≤ 8 caractères.
  • cluster.env identique sur les 3 nœuds.
  • Horloge synchronisée (NTP) sur les trois.
  • Nombre de nœuds impair (3 minimum).

S'abonner aux prochains articles

Recevez les nouveaux articles par e-mail. Pas de spam, désinscription à tout moment.

Propulsé par Buttondown.

Related posts

© 2026 < Denis AKPAGNONITE /> | N1BBzerLZXT