{"id":6685,"date":"2026-04-08T00:03:00","date_gmt":"2026-04-07T22:03:00","guid":{"rendered":"https:\/\/rootfan.com\/?p=6685"},"modified":"2026-04-08T14:24:43","modified_gmt":"2026-04-08T12:24:43","slug":"haute-disponibilite-postgresql-avec-patroni-etcd-haproxy-et-keepalived","status":"publish","type":"post","link":"https:\/\/rootfan.com\/fr\/postgresql-high-availability-with-patroni-etcd-haproxy-and-keepalived\/","title":{"rendered":"Haute disponibilit\u00e9 PostgreSQL avec Patroni, etcd, HAProxy et keepalived"},"content":{"rendered":"<p>PostgreSQL dispose d'une pile haute disponibilit\u00e9 mature et pr\u00eate pour la production qui ne co\u00fbte rien en licence et est simple \u00e0 exploiter une fois qu'elle est configur\u00e9e. <\/p>\n\n\n\n<p>Ce laboratoire cr\u00e9e un cluster HA \u00e0 six n\u0153uds \u00e0 l'aide de quatre composants open source : <strong>Patroni<\/strong> pour la gestion de cluster et le basculement automatique, <strong>etcd<\/strong> en tant que magasin de consensus distribu\u00e9, <strong>HAProxy<\/strong> pour l'\u00e9quilibrage de charge et le routage de connexion, et <strong>keepalived<\/strong> pour une adresse IP virtuelle flottante qui survit aux pannes des n\u0153uds HAProxy.<\/p>\n\n\n\n<p>Le r\u00e9sultat est un cluster o\u00f9 une d\u00e9faillance primaire est d\u00e9tect\u00e9e et un nouveau primaire est \u00e9lu en moins de 30 secondes, sans aucune intervention manuelle requise. <\/p>\n\n\n\n<!--more-->\n\n\n\n<p>Les basculements sont nets et sans perte de donn\u00e9es. <\/p>\n\n\n\n<p>L'ensemble de la pile est g\u00e9r\u00e9 via une seule CLI<code>patronictl<\/code>qui rend les op\u00e9rations quotidiennes \u2014 basculement, reprise apr\u00e8s incident, r\u00e9initialisation, modifications de configuration \u2014 des commandes simples plut\u00f4t que des proc\u00e9dures en plusieurs \u00e9tapes.<\/p>\n\n\n\n<p>Ce laboratoire couvre tout de A \u00e0 Z : g\u00e9n\u00e9ration de certificats TLS, formation de cluster etcd, configuration de Patroni, configuration de HAProxy, configuration VIP keepalived, et v\u00e9rification compl\u00e8te des basculements et des reprises sur incident. <\/p>\n\n\n\n<p>Chaque \u00e9tape est expliqu\u00e9e avec la sortie attendue et le diagnostic de d\u00e9faillance afin que vous sachiez exactement \u00e0 quoi ressemble le succ\u00e8s \u00e0 chaque \u00e9tape.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<div class=\"wp-block-rank-math-toc-block\" id=\"rank-math-toc\"><h2>Table des mati\u00e8res<\/h2><nav><ul><li><a href=\"#postgre-sql-high-availability-with-patroni\">Alta disponibilidad de PostgreSQL con Patroni<\/a><\/li><li><a href=\"#1-architecture\">1. Architecture<\/a><\/li><li><a href=\"#2-prerequisites\">2. Pr\u00e9requis<\/a><ul><li><a href=\"#step-1-set-the-correct-timezone-on-all-nodes\">\u00c9tape 1 \u2014 R\u00e9glez le fuseau horaire correct sur tous les n\u0153uds<\/a><\/li><li><a href=\"#step-2-confirm-required-ports-are-open\">\u00c9tape 2 \u2014 Confirmer que les ports requis sont ouverts<\/a><\/li><\/ul><\/li><li><a href=\"#3-postgre-sql-installation\">3. Installation de PostgreSQL<\/a><ul><li><a href=\"#step-1-install-postgre-sql-on-all-3-postgre-sql-nodes\">\u00c9tape 1 \u2014 Installer PostgreSQL sur les 3 n\u0153uds PostgreSQL<\/a><\/li><li><a href=\"#step-2-stop-and-disable-the-postgre-sql-service\">\u00c9tape 2 \u2014 Arr\u00eatez et d\u00e9sactivez le service PostgreSQL<\/a><\/li><\/ul><\/li><li><a href=\"#4-etcd-installation\">4. Installation d'etcd<\/a><ul><li><a href=\"#step-1-install-etcd-on-all-3-postgre-sql-nodes\">\u00c9tape 1 \u2014 Installer etcd sur les 3 n\u0153uds PostgreSQL<\/a><\/li><li><a href=\"#step-2-create-the-etcd-system-user\">\u00c9tape 2 \u2014 Cr\u00e9er l'utilisateur syst\u00e8me etcd<\/a><\/li><\/ul><\/li><li><a href=\"#5-tls-certificate-generation\">5. G\u00e9n\u00e9ration de certificats TLS<\/a><ul><li><a href=\"#step-1-create-the-working-directory\">\u00c9tape 1 \u2014 Cr\u00e9er le r\u00e9pertoire de travail<\/a><\/li><li><a href=\"#step-2-generate-the-certificate-authority\">\u00c9tape 2 \u2014 G\u00e9n\u00e9rer l'autorit\u00e9 de certification<\/a><\/li><li><a href=\"#step-3-generate-per-node-etcd-certificates\">\u00c9tape 3 \u2014 G\u00e9n\u00e9rer les certificats etcd par n\u0153ud<\/a><\/li><li><a href=\"#step-4-generate-the-postgre-sql-server-certificate\">\u00c9tape 4 \u2014 G\u00e9n\u00e9rer le certificat du serveur PostgreSQL<\/a><\/li><li><a href=\"#step-5-distribute-certificates-to-postgres-02-and-postgres-03\">\u00c9tape 5 \u2014 Distribuer les certificats \u00e0 postgres-02 et postgres-03<\/a><\/li><li><a href=\"#step-6-install-certificates-on-each-postgre-sql-node\">\u00c9tape 6 \u2014 Installer les certificats sur chaque n\u0153ud PostgreSQL<\/a><\/li><li><a href=\"#step-7-create-the-combined-pem-file-for-patroni\">\u00c9tape\u00a07\u00a0\u2014 Cr\u00e9er le fichier PEM combin\u00e9 pour Patroni<\/a><\/li><\/ul><\/li><li><a href=\"#6-etcd-configuration\">6. Configuration d'etcd<\/a><ul><li><a href=\"#step-1-create-the-etcd-data-directory\">\u00c9tape 1 \u2014 Cr\u00e9er le r\u00e9pertoire de donn\u00e9es etcd<\/a><\/li><li><a href=\"#step-2-create-the-etcd-environment-file-on-each-node\">\u00c9tape 2 \u2014 Cr\u00e9ez le fichier d'environnement etcd sur chaque n\u0153ud<\/a><\/li><li><a href=\"#step-3-create-the-etcd-systemd-service-file\">\u00c9tape 3 \u2014 Cr\u00e9er le fichier de service systemd pour etcd<\/a><\/li><li><a href=\"#step-4-start-etcd-on-all-3-nodes\">\u00c9tape 4 \u2014 D\u00e9marrer etcd sur les 3 n\u0153uds<\/a><\/li><li><a href=\"#step-5-verify-etcd-cluster-health\">\u00c9tape 5 \u2014 V\u00e9rifiez l'int\u00e9grit\u00e9 du cluster etcd<\/a><\/li><\/ul><\/li><li><a href=\"#7-patroni-installation-and-configuration\">7. Installation et configuration de Patroni<\/a><ul><li><a href=\"#step-1-install-patroni\">\u00c9tape 1 \u2014 Installer Patroni<\/a><\/li><li><a href=\"#step-2-create-patroni-yml-on-each-node\">\u00c9tape 2 \u2014 Cr\u00e9er patroni.yml sur chaque n\u0153ud<\/a><\/li><\/ul><\/li><li><a href=\"#8-starting-the-cluster\">8. D\u00e9marrage du Cluster<\/a><ul><li><a href=\"#step-1-start-patroni-on-postgres-01-first\">\u00c9tape 1 \u2014 D\u00e9marrer Patroni sur postgres-01 en premier<\/a><\/li><li><a href=\"#step-2-verify-postgres-01-is-the-leader\">\u00c9tape 2 \u2014 V\u00e9rifier que postgres-01 est le leader<\/a><\/li><li><a href=\"#step-3-start-patroni-on-postgres-02-and-postgres-03\">\u00c9tape 3 \u2014 D\u00e9marrer Patroni sur postgres-02 et postgres-03<\/a><\/li><li><a href=\"#step-4-change-initial-cluster-state-to-existing-on-all-nodes\">\u00c9tape 4 \u2014 Changer initial-cluster-state en existing sur tous les n\u0153uds<\/a><\/li><\/ul><\/li><li><a href=\"#9-ha-proxy-setup\">9. Configuration HAProxy<\/a><ul><li><a href=\"#step-1-install-ha-proxy\">\u00c9tape 1 \u2014 Installer HAProxy<\/a><\/li><li><a href=\"#step-2-configure-ha-proxy\">\u00c9tape 2 \u2014 Configurer HAProxy<\/a><\/li><li><a href=\"#step-3-validate-config-and-reload-ha-proxy\">\u00c9tape 3 \u2014 Valider la configuration et recharger HAProxy<\/a><\/li><\/ul><\/li><li><a href=\"#10-keepalived-setup\">10. Configuration de Keepalived<\/a><ul><li><a href=\"#step-1-install-keepalived\">\u00c9tape 1 \u2014 Installer keepalived<\/a><\/li><li><a href=\"#step-2-create-the-ha-proxy-health-check-script\">\u00c9tape 2 \u2014 Cr\u00e9er le script de v\u00e9rification de l'\u00e9tat d'HAProxy<\/a><\/li><li><a href=\"#step-3-configure-keepalived\">\u00c9tape 3 \u2014 Configurer keepalived<\/a><\/li><li><a href=\"#step-4-start-keepalived\">\u00c9tape 4 \u2014 D\u00e9marrer keepalived<\/a><\/li><\/ul><\/li><li><a href=\"#11-set-the-postgres-superuser-password\">11. D\u00e9finir le mot de passe du superutilisateur PostgreSQL<\/a><\/li><li><a href=\"#12-verify-the-full-stack\">12. V\u00e9rifier la pile compl\u00e8te<\/a><\/li><li><a href=\"#13-switchover-planned\">13. Basculement (Planifi\u00e9)<\/a><\/li><li><a href=\"#14-failover-unplanned\">14. Basculement (non planifi\u00e9)<\/a><ul><li><a href=\"#simulate-a-primary-failure\">Simuler une panne primaire<\/a><\/li><li><a href=\"#manual-failover-use-only-when-automatic-failover-has-not-triggered\">Basculement manuel \u2014 \u00e0 n'utiliser que lorsque le basculement automatique n'a pas \u00e9t\u00e9 d\u00e9clench\u00e9<\/a><\/li><\/ul><\/li><li><a href=\"#15-day-to-day-operations\">15. Op\u00e9rations quotidiennes<\/a><ul><li><a href=\"#check-cluster-status\">V\u00e9rifier l'\u00e9tat du cluster<\/a><\/li><li><a href=\"#pause-and-resume-automatic-failover\">Mettre en pause et reprendre le basculement automatique<\/a><\/li><li><a href=\"#restart-postgre-sql-on-a-node\">Red\u00e9marrer PostgreSQL sur un n\u0153ud<\/a><\/li><li><a href=\"#reload-configuration\">Recharger la configuration<\/a><\/li><li><a href=\"#edit-cluster-dcs-configuration\">Modifier la configuration du cluster DCS<\/a><\/li><li><a href=\"#reinitialise-a-failed-standby\">R\u00e9initialiser un standby d\u00e9faillant<\/a><\/li><\/ul><\/li><li><a href=\"#16-synchronous-mode-zero-data-loss\">16. Mode Synchrone (Perte de donn\u00e9es nulle)<\/a><\/li><li><a href=\"#17-monitoring\">17. Surveillance<\/a><ul><li><a href=\"#patroni-rest-api\">API REST Patroni<\/a><\/li><li><a href=\"#replication-lag\">D\u00e9calage de r\u00e9plication<\/a><\/li><\/ul><\/li><li><a href=\"#18-full-reset\">18. R\u00e9initialisation compl\u00e8te<\/a><ul><li><a href=\"#step-1-stop-patroni-and-etcd-on-all-postgre-sql-nodes\">\u00c9tape 1 \u2014 Arr\u00eatez Patroni et etcd sur tous les n\u0153uds PostgreSQL<\/a><\/li><li><a href=\"#step-2-wipe-etcd-and-postgre-sql-data-directories\">\u00c9tape 2 \u2014 Effacer les r\u00e9pertoires de donn\u00e9es etcd et PostgreSQL<\/a><\/li><li><a href=\"#step-3-recreate-directories-with-correct-ownership\">\u00c9tape 3 \u2014 Recr\u00e9er les r\u00e9pertoires avec la propri\u00e9t\u00e9 correcte<\/a><\/li><li><a href=\"#step-4-restore-acl-permissions-on-etcd-certificates\">\u00c9tape 4 \u2014 Restaurer les autorisations ACL sur les certificats etcd<\/a><\/li><li><a href=\"#step-5-reset-etcd-initial-cluster-state-to-new\">\u00c9tape 5 \u2014 R\u00e9initialiser ETCD_INITIAL_CLUSTER_STATE \u00e0 \u201cnew\u201d<\/a><\/li><li><a href=\"#step-6-start-etcd-on-all-nodes\">\u00c9tape 6 \u2014 D\u00e9marrer etcd sur tous les n\u0153uds<\/a><\/li><li><a href=\"#step-7-start-patroni-on-all-nodes\">\u00c9tape 7 \u2014 Lancer Patroni sur tous les n\u0153uds<\/a><\/li><\/ul><\/li><li><a href=\"#19-common-issues\">19. Probl\u00e8mes courants<\/a><\/li><li><a href=\"#20-key-commands-reference\">20. R\u00e9f\u00e9rence des commandes cl\u00e9s<\/a><\/li><\/ul><\/nav><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"postgre-sql-high-availability-with-patroni\">Alta disponibilidad de PostgreSQL con Patroni<\/h2>\n\n\n\n<p>Couvre l'architecture de Patroni, la configuration TLS, le basculement (failover), le changement de r\u00f4le (switchover) et les op\u00e9rations quotidiennes.<\/p>\n\n\n\n<p><strong>Environnement :<\/strong> Six serveurs au total. etcd s'ex\u00e9cute sur les m\u00eames n\u0153uds que PostgreSQL \u2014 pas de serveurs etcd d\u00e9di\u00e9s.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>R\u00f4le<\/th><th>Nom d'h\u00f4te<\/th><th>CI<\/th><\/tr><\/thead><tbody><tr><td>N\u0153ud HAProxy 1<\/td><td>haproxy-01<\/td><td>192.168.0.200<\/td><\/tr><tr><td>N\u0153ud HAProxy 2<\/td><td>haproxy-02<\/td><td>192.168.0.201<\/td><\/tr><tr><td>N\u0153ud HAProxy 3<\/td><td>haproxy-03<\/td><td>192.168.0.202<\/td><\/tr><tr><td>PostgreSQL + etcd + Patroni 1<\/td><td>postgres-01<\/td><td>192.168.0.203<\/td><\/tr><tr><td>PostgreSQL + etcd + Patroni 2<\/td><td>postgres-02<\/td><td>192.168.0.204<\/td><\/tr><tr><td>PostgreSQL + etcd + Patroni 3<\/td><td>postgres-03<\/td><td>192.168.0.205<\/td><\/tr><tr><td>Adresse IP virtuelle (VIP)<\/td><td>\u2014<\/td><td>192.168.0.210<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"1-architecture\">1. Architecture<\/h2>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n          +----------+   +----------+   +----------+\n          |  etcd    |   |  etcd    |   |  etcd    |\n          | Patroni  |   | Patroni  |   | Patroni  |\n          |   +PG    |   |   +PG    |   |   +PG    |\n          | node1    |   | node2    |   | node3    |\n          | PRIMARY  |   | STANDBY  |   | STANDBY  |\n          +----------+   +----------+   +----------+\n                \\              |              \/\n                 \\             |             \/\n          +----------+   +----------+   +----------+\n          | HAProxy  |   | HAProxy  |   | HAProxy  |\n          |  node1   |   |  node2   |   |  node3   |\n          | (MASTER) |   | (BACKUP) |   | (BACKUP) |\n          +----------+   +----------+   +----------+\n                \\              |              \/\n                 \\             |             \/\n              &#91;keepalived VIP: 192.168.0.210:5432]\n                              |\n                        Applications\n<\/pre><\/div>\n\n\n<ul class=\"wp-block-list\">\n<li><strong>etcd<\/strong>: magasin cl\u00e9-valeur distribu\u00e9 co-localis\u00e9 sur chaque n\u0153ud PostgreSQL. Maintient l'\u00e9tat du cluster (leader actuel, liste des membres). N\u00e9cessite un nombre impair de n\u0153uds pour le quorum \u2014 3 n\u0153uds tol\u00e8rent 1 panne, 5 n\u0153uds tol\u00e8rent 2.<\/li>\n\n\n\n<li><strong>Patroni<\/strong>: d\u00e9mon sur chaque n\u0153ud PostgreSQL. G\u00e8re la r\u00e9plication, surveille la sant\u00e9 et coordonne le basculement via etcd.<\/li>\n\n\n\n<li><strong>HAProxy<\/strong>: trois n\u0153uds d\u00e9di\u00e9s acheminent les connexions de l'application vers le principal actuel en consultant l'API REST de Patroni.<\/li>\n\n\n\n<li><strong>keepalived<\/strong>: g\u00e8re le VIP \u00e0 l'aide de VRRP. Un n\u0153ud HAProxy d\u00e9tient le VIP \u00e0 la fois. Si ce n\u0153ud tombe en panne, le VIP est automatiquement transf\u00e9r\u00e9 au n\u0153ud HAProxy suivant.<\/li>\n\n\n\n<li><strong>Toute communication est chiffr\u00e9e TLS<\/strong>: trafic de pairs etcd, trafic client etcd, API REST Patroni et connexions PostgreSQL.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"2-prerequisites\">2. Pr\u00e9requis<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"step-1-set-the-correct-timezone-on-all-nodes\">\u00c9tape 1 \u2014 R\u00e9glez le fuseau horaire correct sur tous les n\u0153uds<\/h3>\n\n\n\n<p>Ex\u00e9cutez sur les 6 serveurs (postgres-01\/02\/03 et haproxy-01\/02\/03). <\/p>\n\n\n\n<p>Les serveurs sont r\u00e9gl\u00e9s sur UTC par d\u00e9faut \u2014 d\u00e9finissez votre fuseau horaire local avant toute autre chose. <\/p>\n\n\n\n<p>Des horodatages incorrects ou discordants provoquent de la confusion dans les journaux et la validation des certificats.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nsudo timedatectl set-timezone Europe\/Madrid\ntimedatectl\n# Expected: Time zone: Europe\/Madrid (CET\/CEST, +0100\/+0200)\n# NTP service should show: active\n# System clock synchronized: yes\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"step-2-confirm-required-ports-are-open\">\u00c9tape 2 \u2014 Confirmer que les ports requis sont ouverts<\/h3>\n\n\n\n<p>Avant de commencer, confirmez que les ports suivants sont ouverts :<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Source<\/th><th>Destination<\/th><th>Port<\/th><th>Objectif<\/th><\/tr><\/thead><tbody><tr><td>N\u0153uds PostgreSQL<\/td><td>N\u0153uds PostgreSQL<\/td><td>2379<\/td><td>Client etcd (Patroni \u2192 etcd)<\/td><\/tr><tr><td>N\u0153uds PostgreSQL<\/td><td>N\u0153uds PostgreSQL<\/td><td>2380<\/td><td>communication entre pairs etcd<\/td><\/tr><tr><td>N\u0153uds PostgreSQL<\/td><td>N\u0153uds PostgreSQL<\/td><td>5432<\/td><td>R\u00e9plication PostgreSQL<\/td><\/tr><tr><td>N\u0153uds PostgreSQL<\/td><td>N\u0153uds PostgreSQL<\/td><td>8008<\/td><td>API REST Patroni<\/td><\/tr><tr><td>N\u0153uds HAProxy<\/td><td>N\u0153uds PostgreSQL<\/td><td>8008<\/td><td>V\u00e9rification de l'\u00e9tat de sant\u00e9 HAProxy<\/td><\/tr><tr><td>N\u0153uds HAProxy<\/td><td>N\u0153uds HAProxy<\/td><td>112\/VRRP<\/td><td>\u00c9lection VIP keepalived<\/td><\/tr><tr><td>Applications<\/td><td>VIP<\/td><td>5432<\/td><td>Connexions clients<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"3-postgre-sql-installation\">3. Installation de PostgreSQL<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"step-1-install-postgre-sql-on-all-3-postgre-sql-nodes\">\u00c9tape 1 \u2014 Installer PostgreSQL sur les 3 n\u0153uds PostgreSQL<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01, postgres-02, postgres-03\nsudo apt update\nsudo apt install -y postgresql-common\n# postgresql-common: provides the PGDG repository setup script\n\nsudo \/usr\/share\/postgresql-common\/pgdg\/apt.postgresql.org.sh\n# This script adds the official PostgreSQL apt repository (postgresql.org)\n# and imports its GPG key \u2014 ensures you get the latest PostgreSQL version,\n# not the older version bundled with Ubuntu\n\nsudo apt update\nsudo apt install -y postgresql-18 postgresql-contrib-18\n# Install version 18 explicitly \u2014 the generic &quot;postgresql&quot; meta-package installs Ubuntu&#039;s\n# default bundled version (16) in addition to the PGDG version, leaving two versions installed\n# Always specify the version number to avoid this\n# postgresql-contrib-18: additional modules including pg_rewind, which Patroni uses\n# to resync the old primary after a failover without a full base backup\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"step-2-stop-and-disable-the-postgre-sql-service\">\u00c9tape 2 \u2014 Arr\u00eatez et d\u00e9sactivez le service PostgreSQL<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01, postgres-02, postgres-03\nsudo systemctl stop postgresql\n# Patroni manages PostgreSQL startup entirely\n# If PostgreSQL is already running when Patroni starts, Patroni will fail with:\n# &quot;postmaster is already running&quot;\n\nsudo systemctl disable postgresql\n# Prevents PostgreSQL from starting automatically on boot\n# Patroni&#039;s own systemd service starts PostgreSQL when the node joins the cluster\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"4-etcd-installation\">4. Installation d'etcd<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"step-1-install-etcd-on-all-3-postgre-sql-nodes\">\u00c9tape 1 \u2014 Installer etcd sur les 3 n\u0153uds PostgreSQL<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01, postgres-02, postgres-03\nsudo apt-get install -y wget curl\n\nwget https:\/\/github.com\/etcd-io\/etcd\/releases\/download\/v3.6.10\/etcd-v3.6.10-linux-amd64.tar.gz\n# Download the etcd binary directly from GitHub releases\n# The apt package is often outdated \u2014 always install from the official releases\n# Check https:\/\/github.com\/etcd-io\/etcd\/releases for the latest stable version\n\ntar xvf etcd-v3.6.10-linux-amd64.tar.gz\n# xvf: extract (x), verbose (v), from file (f)\n\nsudo mv etcd-v3.6.10-linux-amd64\/etcd \/usr\/local\/bin\/\nsudo mv etcd-v3.6.10-linux-amd64\/etcdctl \/usr\/local\/bin\/\n# etcd: the etcd server binary\n# etcdctl: the etcd client CLI \u2014 used for health checks and inspecting cluster state\n\n# Verify the installation\netcd --version\n# Expected: etcd Version: 3.6.10\n# If &quot;etcd: command not found&quot;: \/usr\/local\/bin is not in PATH \u2014 run: export PATH=$PATH:\/usr\/local\/bin\n\netcdctl version\n# Expected: etcdctl version: 3.6.10\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"step-2-create-the-etcd-system-user\">\u00c9tape 2 \u2014 Cr\u00e9er l'utilisateur syst\u00e8me etcd<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01, postgres-02, postgres-03\nsudo useradd --system --home \/var\/lib\/etcd --shell \/bin\/false etcd\n\n# --system: creates a system account with no login shell by default\n# --home \/var\/lib\/etcd: etcd stores its data here\n# --shell \/bin\/false: prevents interactive login \u2014 etcd runs as a daemon only\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"5-tls-certificate-generation\">5. G\u00e9n\u00e9ration de certificats TLS<\/h2>\n\n\n\n<p>Tous les certificats sont g\u00e9n\u00e9r\u00e9s une seule fois sur postgres-01 puis distribu\u00e9s aux autres n\u0153uds.<\/p>\n\n\n\n<p>La cl\u00e9 priv\u00e9e du certificat\n\nLa cl\u00e9 priv\u00e9e du CA<code>ca.cl\u00e9<\/code>) reste sur postgres-01 apr\u00e8s la fin de la distribution \u2014 ne le copiez pas sur d'autres n\u0153uds.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"step-1-create-the-working-directory\">\u00c9tape 1 \u2014 Cr\u00e9er le r\u00e9pertoire de travail<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01\nmkdir ~\/certs &amp;&amp; cd ~\/certs\n# All certificate files are created here before being copied to each node\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"step-2-generate-the-certificate-authority\">\u00c9tape 2 \u2014 G\u00e9n\u00e9rer l'autorit\u00e9 de certification<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01\nopenssl genrsa -out ca.key 2048\n# genrsa: generate an RSA private key; 2048: key length in bits\n\nopenssl req -x509 -new -nodes -key ca.key -subj &quot;\/CN=etcd-ca&quot; -days 7300 -out ca.crt\n# req -x509: create a self-signed certificate (not a signing request)\n# -new -nodes: new certificate, no passphrase on the private key\n# -subj &quot;\/CN=etcd-ca&quot;: the certificate&#039;s Common Name \u2014 identifies this as the cluster CA\n# -days 7300: valid for 20 years\n# ca.crt: distributed to every node as the root of trust\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"step-3-generate-per-node-etcd-certificates\">\u00c9tape 3 \u2014 G\u00e9n\u00e9rer les certificats etcd par n\u0153ud<\/h3>\n\n\n\n<p>Chaque n\u0153ud obtient son propre certificat avec son adresse IP comme nom alternatif du sujet (SAN). La v\u00e9rification du nom d'h\u00f4te TLS exige que l'adresse IP du serveur apparaisse dans le SAN \u2013 sans cela, les connexions \u00e9choueront.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01 \u2014 generate all three node certificates here, then distribute\n\n# Certificate for postgres-01 (192.168.0.203)\nopenssl genrsa -out etcd-node1.key 2048\n\ncat &gt; temp.cnf &lt;&lt;EOF\n&#91; req ]\ndistinguished_name = req_distinguished_name\nreq_extensions = v3_req\n&#91; req_distinguished_name ]\n&#91; v3_req ]\nsubjectAltName = @alt_names\n&#91; alt_names ]\nIP.1 = 192.168.0.203\nIP.2 = 127.0.0.1\nEOF\n\n# temp.cnf: OpenSSL config that adds the node IP as a SAN\n# IP.2 = 127.0.0.1: allows etcdctl to connect locally without specifying a remote address\n\nopenssl req -new -key etcd-node1.key -out etcd-node1.csr \\\n  -subj &quot;\/CN=etcd-node1&quot; \\\n  -config temp.cnf\n# req -new: generate a certificate signing request (CSR)\n# -subj: the certificate&#039;s identity \u2014 CN identifies the node in logs\n\nopenssl x509 -req -in etcd-node1.csr -CA ca.crt -CAkey ca.key \\\n  -CAcreateserial -out etcd-node1.crt -days 7300 \\\n  -sha256 -extensions v3_req -extfile temp.cnf\n# x509 -req: sign the CSR with the CA to produce a certificate\n# -CAcreateserial: creates ca.srl to track serial numbers across certificates\n# -extensions v3_req -extfile temp.cnf: embed the SANs into the signed certificate\n\nopenssl x509 -in etcd-node1.crt -text -noout | grep -A1 &quot;Subject Alternative Name&quot;\n# Verify the SAN was embedded \u2014 Expected: IP Address:192.168.0.203, IP Address:127.0.0.1\n# If the SAN is missing: the -extensions and -extfile flags were not applied correctly\n\nrm temp.cnf\n\n# Certificate for postgres-02 (192.168.0.204)\nopenssl genrsa -out etcd-node2.key 2048\n\ncat &gt; temp.cnf &lt;&lt;EOF\n&#91; req ]\ndistinguished_name = req_distinguished_name\nreq_extensions = v3_req\n&#91; req_distinguished_name ]\n&#91; v3_req ]\nsubjectAltName = @alt_names\n&#91; alt_names ]\nIP.1 = 192.168.0.204\nIP.2 = 127.0.0.1\nEOF\n\nopenssl req -new -key etcd-node2.key -out etcd-node2.csr \\\n  -subj &quot;\/CN=etcd-node2&quot; -config temp.cnf\n\nopenssl x509 -req -in etcd-node2.csr -CA ca.crt -CAkey ca.key \\\n  -CAcreateserial -out etcd-node2.crt -days 7300 \\\n  -sha256 -extensions v3_req -extfile temp.cnf\n\nopenssl x509 -in etcd-node2.crt -text -noout | grep -A1 &quot;Subject Alternative Name&quot;\n# Expected: IP Address:192.168.0.204, IP Address:127.0.0.1\nrm temp.cnf\n\n\n# Certificate for postgres-03 (192.168.0.205)\nopenssl genrsa -out etcd-node3.key 2048\n\ncat &gt; temp.cnf &lt;&lt;EOF\n&#91; req ]\ndistinguished_name = req_distinguished_name\nreq_extensions = v3_req\n&#91; req_distinguished_name ]\n&#91; v3_req ]\nsubjectAltName = @alt_names\n&#91; alt_names ]\nIP.1 = 192.168.0.205\nIP.2 = 127.0.0.1\nEOF\n\nopenssl req -new -key etcd-node3.key -out etcd-node3.csr \\\n  -subj &quot;\/CN=etcd-node3&quot; -config temp.cnf\n\nopenssl x509 -req -in etcd-node3.csr -CA ca.crt -CAkey ca.key \\\n  -CAcreateserial -out etcd-node3.crt -days 7300 \\\n  -sha256 -extensions v3_req -extfile temp.cnf\n\nopenssl x509 -in etcd-node3.crt -text -noout | grep -A1 &quot;Subject Alternative Name&quot;\n# Expected: IP Address:192.168.0.205, IP Address:127.0.0.1\nrm temp.cnf\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"step-4-generate-the-postgre-sql-server-certificate\">\u00c9tape 4 \u2014 G\u00e9n\u00e9rer le certificat du serveur PostgreSQL<\/h3>\n\n\n\n<p>Un certificat partag\u00e9 couvre tous les n\u0153uds PostgreSQL. <\/p>\n\n\n\n<p>Il est utilis\u00e9 \u00e0 la fois pour les connexions PostgreSQL et pour l'API REST de Patroni.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01\nopenssl genrsa -out server.key 2048\n\nopenssl req -new -key server.key -out server.req\n# You will be prompted for certificate details \u2014 the Common Name is not critical\n# since connections are verified by IP SAN, not CN\n# Warning &quot;No -copy_extensions given&quot; is harmless \u2014 the server cert does not need SANs\n\nopenssl req -x509 -key server.key -in server.req -out server.crt -days 7300\n# Self-signed server certificate \u2014 signed directly with server.key, not the CA\n# Patroni and PostgreSQL use this certificate to identify themselves to clients\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"step-5-distribute-certificates-to-postgres-02-and-postgres-03\">\u00c9tape 5 \u2014 Distribuer les certificats \u00e0 postgres-02 et postgres-03<\/h3>\n\n\n\n<p>postgres-01 garde ses propres certificats dans ~\/certs \u2014 aucun scp n'est n\u00e9cessaire pour lui.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01\nscp ~\/certs\/ca.crt ~\/certs\/etcd-node2.crt ~\/certs\/etcd-node2.key \\\n  ~\/certs\/server.crt ~\/certs\/server.key fernando@192.168.0.204:\/tmp\/\n\nscp ~\/certs\/ca.crt ~\/certs\/etcd-node3.crt ~\/certs\/etcd-node3.key \\\n  ~\/certs\/server.crt ~\/certs\/server.key fernando@192.168.0.205:\/tmp\/\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"step-6-install-certificates-on-each-postgre-sql-node\">\u00c9tape 6 \u2014 Installer les certificats sur chaque n\u0153ud PostgreSQL<\/h3>\n\n\n\n<p>Tous les certificats r\u00e9sident dans <code>\/etc\/etcd\/certs\/<\/code> sur chaque n\u0153ud. <\/p>\n\n\n\n<p>Le r\u00e9pertoire appartient \u00e0 <code>etcd:etcd<\/code> alors le d\u00e9mon etcd peut lire ses certificats. <\/p>\n\n\n\n<p>Le <code>PostgreSQL<\/code> l'utilisateur obtient un acc\u00e8s en lecture via ACL afin que Patroni puisse se connecter \u00e0 etcd.<\/p>\n\n\n\n<p><strong>Important :<\/strong> d\u00e9finir les permissions de fichier avant de verrouiller le r\u00e9pertoire. <\/p>\n\n\n\n<p>Apr\u00e8s <code>chmod 700<\/code> le shell ne peut pas d\u00e9velopper les jokers \u00e0 l'int\u00e9rieur du r\u00e9pertoire en tant qu'utilisateur non root \u2014 utilisez des noms de fichiers explicites.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01, postgres-02, postgres-03\nsudo mkdir -p \/etc\/etcd\/certs\nsudo apt-get install -y acl\n# acl: provides setfacl \u2014 needed to grant postgres user access without changing ownership\n<\/pre><\/div>\n\n\n<p><strong>Sur postgres-01<\/strong> \u2014 copier depuis ~\/certs (les fichiers n'ont jamais \u00e9t\u00e9 dans \/tmp sur ce n\u0153ud) :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nsudo cp ~\/certs\/ca.crt \/etc\/etcd\/certs\/\nsudo cp ~\/certs\/etcd-node1.crt \/etc\/etcd\/certs\/\nsudo cp ~\/certs\/etcd-node1.key \/etc\/etcd\/certs\/\nsudo cp ~\/certs\/server.crt \/etc\/etcd\/certs\/\nsudo cp ~\/certs\/server.key \/etc\/etcd\/certs\/\n<\/pre><\/div>\n\n\n<p><strong>Sur postgres-02<\/strong> \u2014 d\u00e9placer depuis \/tmp :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nsudo mv \/tmp\/ca.crt \/etc\/etcd\/certs\/\nsudo mv \/tmp\/etcd-node2.crt \/etc\/etcd\/certs\/\nsudo mv \/tmp\/etcd-node2.key \/etc\/etcd\/certs\/\nsudo mv \/tmp\/server.crt \/etc\/etcd\/certs\/\nsudo mv \/tmp\/server.key \/etc\/etcd\/certs\/\n<\/pre><\/div>\n\n\n<p><strong>Sur postgres-03<\/strong> \u2014 d\u00e9placer depuis \/tmp :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nsudo mv \/tmp\/ca.crt \/etc\/etcd\/certs\/\nsudo mv \/tmp\/etcd-node3.crt \/etc\/etcd\/certs\/\nsudo mv \/tmp\/etcd-node3.key \/etc\/etcd\/certs\/\nsudo mv \/tmp\/server.crt \/etc\/etcd\/certs\/\nsudo mv \/tmp\/server.key \/etc\/etcd\/certs\/\n<\/pre><\/div>\n\n\n<p><strong>Sur les trois n\u0153uds<\/strong> \u2014 d\u00e9finissez les permissions puis verrouillez le r\u00e9pertoire :<\/p>\n\n\n\n<p>les certificats etcd doivent appartenir \u00e0 <code>etcd<\/code> \u2014 le d\u00e9mon etcd s'ex\u00e9cute en tant que cet utilisateur. <\/p>\n\n\n\n<p>Les certificats de serveur doivent appartenir \u00e0 <code>PostgreSQL<\/code> \u2014 PostgreSQL s'assure que sa cl\u00e9 priv\u00e9e SSL appartient \u00e0 l'utilisateur de la base de donn\u00e9es ou \u00e0 root. <\/p>\n\n\n\n<p>Utilisation <code>etcd<\/code> La propri\u00e9t\u00e9 entra\u00eenera le refus de d\u00e9marrage de PostgreSQL avec le message : \u201c le fichier de cl\u00e9 priv\u00e9e doit appartenir \u00e0 l'utilisateur de la base de donn\u00e9es ou \u00e0 root \u201d.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# Set file permissions first \u2014 must happen before chmod 700 on the directory\n# After chmod 700, the shell cannot expand globs as a non-root user\n\n# etcd certs: owned by etcd\n# On postgres-01:\nsudo chown etcd:etcd \/etc\/etcd\/certs\/etcd-node1.crt \/etc\/etcd\/certs\/etcd-node1.key \/etc\/etcd\/certs\/ca.crt\nsudo chmod 600 \/etc\/etcd\/certs\/etcd-node1.key\nsudo chmod 644 \/etc\/etcd\/certs\/etcd-node1.crt \/etc\/etcd\/certs\/ca.crt\n\n# On postgres-02:\nsudo chown etcd:etcd \/etc\/etcd\/certs\/etcd-node2.crt \/etc\/etcd\/certs\/etcd-node2.key \/etc\/etcd\/certs\/ca.crt\nsudo chmod 600 \/etc\/etcd\/certs\/etcd-node2.key\nsudo chmod 644 \/etc\/etcd\/certs\/etcd-node2.crt \/etc\/etcd\/certs\/ca.crt\n\n# On postgres-03:\nsudo chown etcd:etcd \/etc\/etcd\/certs\/etcd-node3.crt \/etc\/etcd\/certs\/etcd-node3.key \/etc\/etcd\/certs\/ca.crt\nsudo chmod 600 \/etc\/etcd\/certs\/etcd-node3.key\nsudo chmod 644 \/etc\/etcd\/certs\/etcd-node3.crt \/etc\/etcd\/certs\/ca.crt\n\n# Server certs: owned by postgres (all three nodes \u2014 same files on each)\nsudo chown postgres:postgres \/etc\/etcd\/certs\/server.crt \/etc\/etcd\/certs\/server.key\nsudo chmod 600 \/etc\/etcd\/certs\/server.key\nsudo chmod 644 \/etc\/etcd\/certs\/server.crt\n\n# Lock the directory \u2014 run this last, after all file permissions are set\nsudo chown etcd:etcd \/etc\/etcd\/certs\nsudo chmod 700 \/etc\/etcd\/certs\n\n# Grant the postgres user read access to the directory and all files inside it\n# Patroni needs to read the etcd certs to connect with TLS\nsudo setfacl -R -m u:postgres:rX \/etc\/etcd\/certs\n# -R: apply recursively to all files; rX: read + execute on directories (to traverse)\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"step-7-create-the-combined-pem-file-for-patroni\">\u00c9tape\u00a07\u00a0\u2014 Cr\u00e9er le fichier PEM combin\u00e9 pour Patroni<\/h3>\n\n\n\n<p>Patroni's <code>restapi.certfile<\/code> attend un fichier unique contenant \u00e0 la fois le certificat et la cl\u00e9 priv\u00e9e.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01, postgres-02, postgres-03\nsudo sh -c &#039;cat \/etc\/etcd\/certs\/server.crt \/etc\/etcd\/certs\/server.key \\\n  &gt; \/etc\/etcd\/certs\/server.pem&#039;\n# Concatenates certificate then key into one file\nsudo chown postgres:postgres \/etc\/etcd\/certs\/server.pem\nsudo chmod 600 \/etc\/etcd\/certs\/server.pem\n# server.pem contains the private key \u2014 PostgreSQL requires 0600 and postgres ownership\n\n# Verify the PEM file is valid\nsudo openssl x509 -in \/etc\/etcd\/certs\/server.pem -text -noout\n# Expected: certificate details including validity dates and subject\n# If &quot;unable to load certificate&quot;: the PEM file is malformed \u2014 recreate it\n\n# Verify final permissions on all cert files\nsudo ls -la \/etc\/etcd\/certs\/\n# Expected output (postgres-01 shown \u2014 node number differs on 02\/03):\n#   drwx------+  2 etcd     etcd      ca.crt etcd-node1.crt etcd-node1.key server.crt server.key server.pem\n#   -rw-r--r--+  1 etcd     etcd      ca.crt\n#   -rw-r--r--+  1 etcd     etcd      etcd-node1.crt\n#   -rw-------+  1 etcd     etcd      etcd-node1.key\n#   -rw-r--r--+  1 postgres postgres  server.crt\n#   -rw-------+  1 postgres postgres  server.key   \u2190 must be 0600, postgres-owned\n#   -rw-------+  1 postgres postgres  server.pem   \u2190 must be 0600, postgres-owned\n# PostgreSQL will refuse to start if server.key or server.pem is group- or world-readable\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"6-etcd-configuration\">6. Configuration d'etcd<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"step-1-create-the-etcd-data-directory\">\u00c9tape 1 \u2014 Cr\u00e9er le r\u00e9pertoire de donn\u00e9es etcd<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01, postgres-02, postgres-03\nsudo mkdir -p \/var\/lib\/etcd\nsudo chown -R etcd:etcd \/var\/lib\/etcd\n# etcd stores its WAL and snapshot data here \u2014 must be owned by the etcd user\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"step-2-create-the-etcd-environment-file-on-each-node\">\u00c9tape 2 \u2014 Cr\u00e9ez le fichier d'environnement etcd sur chaque n\u0153ud<\/h3>\n\n\n\n<p>etcd est configur\u00e9 via des variables d'environnement charg\u00e9es par le service systemd. <\/p>\n\n\n\n<p>Seules les valeurs sp\u00e9cifiques aux n\u0153uds diff\u00e8rent entre les n\u0153uds.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# \/etc\/etcd\/etcd.env \u2014 postgres-01 (192.168.0.203)\n\nETCD_NAME=&quot;postgresql-01&quot;\n# ETCD_NAME: unique identifier for this member within the cluster\n\nETCD_DATA_DIR=&quot;\/var\/lib\/etcd&quot;\n# ETCD_DATA_DIR: where etcd stores its WAL and snapshots\n\nETCD_INITIAL_CLUSTER=&quot;postgresql-01=https:\/\/192.168.0.203:2380,postgresql-02=https:\/\/192.168.0.204:2380,postgresql-03=https:\/\/192.168.0.205:2380&quot;\n# ETCD_INITIAL_CLUSTER: all members at bootstrap time \u2014 must be identical on all three nodes\n\nETCD_INITIAL_CLUSTER_STATE=&quot;new&quot;\n# new: this is a fresh cluster bootstrap\n# Important: change to &quot;existing&quot; after the cluster is running (see section 9 step 3)\n\nETCD_INITIAL_CLUSTER_TOKEN=&quot;etcd-cluster&quot;\n# ETCD_INITIAL_CLUSTER_TOKEN: prevents nodes from accidentally joining the wrong cluster\n\nETCD_INITIAL_ADVERTISE_PEER_URLS=&quot;https:\/\/192.168.0.203:2380&quot;\n# ETCD_INITIAL_ADVERTISE_PEER_URLS: address this node advertises to other etcd members for peer traffic\n\nETCD_LISTEN_PEER_URLS=&quot;https:\/\/0.0.0.0:2380&quot;\n# ETCD_LISTEN_PEER_URLS: address etcd listens on for peer connections from other etcd members\n\nETCD_LISTEN_CLIENT_URLS=&quot;https:\/\/0.0.0.0:2379&quot;\n# ETCD_LISTEN_CLIENT_URLS: address etcd listens on for client connections (Patroni connects here)\n\nETCD_ADVERTISE_CLIENT_URLS=&quot;https:\/\/192.168.0.203:2379&quot;\n# ETCD_ADVERTISE_CLIENT_URLS: address this node advertises to clients \u2014 must be reachable from Patroni\n\n# TLS for client connections (Patroni \u2192 etcd)\nETCD_CLIENT_CERT_AUTH=&quot;true&quot;\n# ETCD_CLIENT_CERT_AUTH: require clients to present a valid certificate (mutual TLS)\nETCD_TRUSTED_CA_FILE=&quot;\/etc\/etcd\/certs\/ca.crt&quot;\n# ETCD_TRUSTED_CA_FILE: CA certificate used to verify client certificates\nETCD_CERT_FILE=&quot;\/etc\/etcd\/certs\/etcd-node1.crt&quot;\n# ETCD_CERT_FILE: certificate presented to clients connecting to this node\nETCD_KEY_FILE=&quot;\/etc\/etcd\/certs\/etcd-node1.key&quot;\n# ETCD_KEY_FILE: private key for the above certificate\n\n# TLS for peer connections (etcd node \u2194 etcd node)\nETCD_PEER_CLIENT_CERT_AUTH=&quot;true&quot;\n# ETCD_PEER_CLIENT_CERT_AUTH: require peer nodes to present a valid certificate\nETCD_PEER_TRUSTED_CA_FILE=&quot;\/etc\/etcd\/certs\/ca.crt&quot;\nETCD_PEER_CERT_FILE=&quot;\/etc\/etcd\/certs\/etcd-node1.crt&quot;\nETCD_PEER_KEY_FILE=&quot;\/etc\/etcd\/certs\/etcd-node1.key&quot;\n<\/pre><\/div>\n\n\n<p>Pour postgres-02 (192.168.0.204) : changer <code>ETCD_NAME<\/code> \u00e0 <code>postgresql-02<\/code>, les deux adresses IP \u00e0 <code>192.168.0.204<\/code>, et les noms de fichiers de certificat\/cl\u00e9 \u00e0 <code>etcd-node2.crt<\/code> \/ <code>etcd-node2.cl\u00e9<\/code>. <\/p>\n\n\n\n<p>Pour postgres-03 (192.168.0.205) : changer <code>ETCD_NAME<\/code> \u00e0 <code>postgresql-03<\/code>, les deux adresses IP \u00e0 <code>192.168.0.205<\/code>, et les noms de fichiers de certificat\/cl\u00e9 \u00e0 <code>etcd-node3.crt<\/code> \/ <code>etcd-node3.cl\u00e9<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"step-3-create-the-etcd-systemd-service-file\">\u00c9tape 3 \u2014 Cr\u00e9er le fichier de service systemd pour etcd<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01, postgres-02, postgres-03\n# Create \/etc\/systemd\/system\/etcd.service with the following content:\n<\/pre><\/div>\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n&#91;Unit]\nDescription=etcd key-value store\nDocumentation=https:\/\/github.com\/etcd-io\/etcd\nAfter=network-online.target\nWants=network-online.target\n\n&#91;Service]\nType=notify\n# Type=notify: systemd waits for etcd to send a readiness signal before marking it as started\nWorkingDirectory=\/var\/lib\/etcd\nEnvironmentFile=\/etc\/etcd\/etcd.env\n# EnvironmentFile: loads all ETCD_* variables from the file created in step 2\nExecStart=\/usr\/local\/bin\/etcd\nRestart=always\n# Restart=always: systemd restarts etcd if it exits for any reason\nRestartSec=10s\nLimitNOFILE=40000\n# LimitNOFILE: raise the open file descriptor limit \u2014 etcd opens many files under load\nUser=etcd\nGroup=etcd\n\n&#91;Install]\nWantedBy=multi-user.target\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"step-4-start-etcd-on-all-3-nodes\">\u00c9tape 4 \u2014 D\u00e9marrer etcd sur les 3 n\u0153uds<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01, postgres-02, postgres-03\nsudo systemctl daemon-reload\n# daemon-reload: required after creating or modifying a systemd unit file\n\nsudo systemctl enable etcd\n# enable: start etcd automatically on boot\n\nsudo systemctl start etcd\n# start: start the etcd service now\n\nsudo systemctl status etcd\n# Expected: Active: active (running)\n# If &quot;Active: failed&quot;: check journalctl -xeu etcd.service for details\n# Common causes:\n#   - cert not found: verify paths in etcd.env match files in \/etc\/etcd\/certs\/\n#   - permission denied on key: run &quot;sudo chown etcd:etcd \/etc\/etcd\/certs\/*.key&quot;\n#   - port in use: run &quot;ss -tlnp | grep 237&quot; to find what is on ports 2379\/2380\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"step-5-verify-etcd-cluster-health\">\u00c9tape 5 \u2014 V\u00e9rifiez l'int\u00e9grit\u00e9 du cluster etcd<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01\netcdctl endpoint health\n# Expected:\n#   127.0.0.1:2379 is healthy: successfully committed proposal: took = 2.3ms\n# This checks only the local node \u2014 the full cluster check is below\n\n# Full cluster health check with TLS credentials\nsudo etcdctl \\\n  --endpoints=https:\/\/192.168.0.203:2379,https:\/\/192.168.0.204:2379,https:\/\/192.168.0.205:2379 \\\n  --cacert=\/etc\/etcd\/certs\/ca.crt \\\n  --cert=\/etc\/etcd\/certs\/etcd-node1.crt \\\n  --key=\/etc\/etcd\/certs\/etcd-node1.key \\\n  endpoint health\n# --cacert: CA certificate to verify the server certificates\n# --cert \/ --key: client certificate and key for mutual TLS\n# Expected:\n#   https:\/\/192.168.0.203:2379 is healthy: successfully committed proposal: took = 2.3ms\n#   https:\/\/192.168.0.204:2379 is healthy: successfully committed proposal: took = 2.1ms\n#   https:\/\/192.168.0.205:2379 is healthy: successfully committed proposal: took = 2.4ms\n# If any node is unhealthy: check journalctl -u etcd on that node\n# If all nodes unhealthy: check firewall on port 2380 between nodes\n\n# Check leader election \u2014 one node should be the leader\nsudo etcdctl \\\n  --endpoints=https:\/\/192.168.0.203:2379,https:\/\/192.168.0.204:2379,https:\/\/192.168.0.205:2379 \\\n  --cacert=\/etc\/etcd\/certs\/ca.crt \\\n  --cert=\/etc\/etcd\/certs\/etcd-node1.crt \\\n  --key=\/etc\/etcd\/certs\/etcd-node1.key \\\n  endpoint status --write-out=table\n# Expected: a table with one node showing IS LEADER = true\n# If no leader: quorum is not established \u2014 verify all three nodes are running\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"7-patroni-installation-and-configuration\">7. Installation et configuration de Patroni<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"step-1-install-patroni\">\u00c9tape 1 \u2014 Installer Patroni<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01, postgres-02, postgres-03\nsudo apt install -y patroni\n# On Ubuntu 22.04+, the apt package includes the etcd v3 client library\n# If your distro&#039;s package does not include it: pip install patroni&#91;etcd3]\n# &#91;etcd3]: selects the etcd v3 API backend \u2014 required for etcd 3.5+\n# The older &#91;etcd] flag uses the deprecated v2 HTTP API\n\nsudo mkdir -p \/etc\/patroni\/\n# Patroni reads its configuration from a YAML file in this directory\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"step-2-create-patroni-yml-on-each-node\">\u00c9tape 2 \u2014 Cr\u00e9er patroni.yml sur chaque n\u0153ud<\/h3>\n\n\n\n<p>Seulement <code>nom<\/code>, <code>restapi.adresse_de_connexion<\/code>, <code>postgresql.connect_address<\/code>, et les chemins des certificats\/cl\u00e9s etcd diff\u00e8rent entre les n\u0153uds.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# \/etc\/patroni\/config.yml \u2014 postgres-01 (192.168.0.203)\n\nscope: postgresql-cluster\n# scope: cluster name \u2014 must be identical on all Patroni nodes\n# Used as the key prefix in etcd to separate multiple Patroni clusters\n\nnamespace: \/service\/\n# namespace: etcd key prefix \u2014 all cluster state is stored under \/service\/postgresql-cluster\/\n\nname: postgresql-01\n# name: unique name for this node within the cluster\n\netcd3:\n  hosts: 192.168.0.203:2379,192.168.0.204:2379,192.168.0.205:2379\n  # etcd3: use the etcd v3 API (gRPC) \u2014 required for etcd 3.5+\n  # The older &quot;etcd:&quot; key uses the v2 HTTP API which is deprecated\n  protocol: https\n  # protocol: https \u2014 Patroni connects to etcd over TLS\n  cacert: \/etc\/etcd\/certs\/ca.crt\n  # cacert: CA certificate to verify the etcd server certificates\n  cert: \/etc\/etcd\/certs\/etcd-node1.crt\n  # cert: client certificate presented to etcd for mutual TLS\n  key: \/etc\/etcd\/certs\/etcd-node1.key\n  # key: private key for the client certificate\n\nrestapi:\n  listen: 0.0.0.0:8008\n  # listen: Patroni&#039;s REST API listens on all interfaces on port 8008\n  # HAProxy queries this API to determine which node is the current primary\n  connect_address: 192.168.0.203:8008\n  # connect_address: the address other nodes use to reach this node&#039;s REST API \u2014 must be the actual IP\n  certfile: \/etc\/etcd\/certs\/server.pem\n  # certfile: combined cert+key PEM file \u2014 enables TLS on the REST API\n  # HAProxy uses check-ssl when querying \/primary \u2014 the API must serve a certificate\n\nbootstrap:\n  dcs:\n    ttl: 30\n    # ttl: leader lease duration in seconds\n    # If the primary does not renew within ttl seconds, it is considered failed\n    # and a standby is promoted. Lower = faster failover; higher = more tolerance for slow networks.\n    loop_wait: 10\n    # loop_wait: how often Patroni checks cluster health (seconds)\n    retry_timeout: 10\n    # retry_timeout: how long Patroni retries a failed etcd or PostgreSQL operation before giving up\n    maximum_lag_on_failover: 1048576\n    # maximum_lag_on_failover: standbys more than 1MB behind the primary will not be promoted\n    # Prevents promoting a very stale standby that would cause significant data loss\n    postgresql:\n      parameters:\n        ssl: &#039;on&#039;\n        # ssl: enable TLS on PostgreSQL connections\n        ssl_cert_file: \/etc\/etcd\/certs\/server.crt\n        ssl_key_file: \/etc\/etcd\/certs\/server.key\n      pg_hba:\n        # pg_hba: Patroni writes this pg_hba.conf on bootstrap\n        # hostssl: TLS is required \u2014 plain connections are rejected\n        - hostssl replication replicator 127.0.0.1\/32 md5\n        - hostssl replication replicator 192.168.0.203\/32 md5\n        - hostssl replication replicator 192.168.0.204\/32 md5\n        - hostssl replication replicator 192.168.0.205\/32 md5\n        # Replication connections from all three PostgreSQL nodes\n        - hostssl all all 127.0.0.1\/32 md5\n        - hostssl all all 0.0.0.0\/0 md5\n        # Application connections \u2014 TLS required, password authentication\n\n  initdb:\n    - encoding: UTF8\n    - data-checksums\n    # data-checksums: enables page-level checksums \u2014 required for pg_rewind\n    # Detects corrupted blocks; slight write overhead (typically under 2%)\n\npostgresql:\n  listen: 0.0.0.0:5432\n  connect_address: 192.168.0.203:5432\n  # connect_address: this node&#039;s actual IP \u2014 used by standbys to connect for replication\n  data_dir: \/var\/lib\/postgresql\/data\n  # data_dir: PostgreSQL data directory \u2014 Patroni manages this directory entirely\n  bin_dir: \/usr\/lib\/postgresql\/18\/bin\n  # bin_dir: directory containing pg_ctl, pg_basebackup, pg_rewind, initdb\n  # Adjust the version number to match your PostgreSQL installation\n  authentication:\n    superuser:\n      username: postgres\n      password: strongpassword\n      # Patroni uses these credentials for internal management connections\n      # Change before production use\n    replication:\n      username: replicator\n      password: replpassword\n      # Patroni creates this role automatically during bootstrap\n      # Change before production use\n  parameters:\n    max_connections: 100\n    shared_buffers: 256MB\n    # shared_buffers: PostgreSQL&#039;s main memory cache \u2014 typically 25% of total RAM\n    # 256MB is a conservative default; increase based on available memory\n\ntags:\n  nofailover: false\n  # nofailover: set to true on a node you never want automatically promoted (e.g. a DR standby)\n  noloadbalance: false\n  # noloadbalance: set to true to exclude this node from read replica routing\n  clonefrom: false\n  nosync: false\n\nctl:\n  insecure: true\n  # insecure: skip TLS certificate verification when patronictl calls the Patroni REST API\n  # Required for switchover and failover commands \u2014 without it patronictl fails with an SSL error\n  # Read-only commands (patronictl list) work without this because they use etcd, not the REST API\n  # Do NOT add cacert or certfile here \u2014 a server certfile causes a bad TLS handshake\n<\/pre><\/div>\n\n\n<p>Pour postgres-02 (192.168.0.204) : changer <code>nom<\/code> \u00e0 <code>postgresql-02<\/code>, les deux <code>adresse_connexion<\/code> valeurs \u00e0 <code>192.168.0.204<\/code>, et cl\u00e9\/certifikat etcd \u00e0 <code>etcd-node2.crt<\/code> \/ <code>etcd-node2.cl\u00e9<\/code>. <\/p>\n\n\n\n<p>Pour postgres-03 (192.168.0.205) : changer <code>nom<\/code> \u00e0 <code>postgresql-03<\/code>, les deux <code>adresse_connexion<\/code> valeurs \u00e0 <code>192.168.0.205<\/code>, et cl\u00e9\/certifikat etcd \u00e0 <code>etcd-node3.crt<\/code> \/ <code>etcd-node3.cl\u00e9<\/code>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"8-starting-the-cluster\">8. D\u00e9marrage du Cluster<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"step-1-start-patroni-on-postgres-01-first\">\u00c9tape 1 \u2014 D\u00e9marrer Patroni sur postgres-01 en premier<\/h3>\n\n\n\n<p>Le primaire vis\u00e9 doit commencer en premier. <\/p>\n\n\n\n<p>Si un standby d\u00e9marre avant qu'un primaire n'existe dans etcd, il attend \u2014 cela ne provoque pas d'erreur. D\u00e9marrer d'abord le primaire \u00e9vite une course d'\u00e9lection de leader \u00e0 trois.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01\nsudo systemctl enable patroni\n# enable: start Patroni automatically on boot\nsudo systemctl restart patroni\n# Patroni initialises the PostgreSQL data directory (initdb), starts PostgreSQL,\n# acquires the leader lease in etcd, and configures itself as primary\n\njournalctl -u patroni -f\n# -f: follow \u2014 stream new log lines as they appear\n# Watch for: &quot;promoted self to leader&quot; \u2014 confirms postgres-01 is the primary\n# Press Ctrl+C to stop following once confirmed\n# If &quot;could not connect to etcd&quot;: verify etcd is running and TLS certs are correct\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"step-2-verify-postgres-01-is-the-leader\">\u00c9tape 2 \u2014 V\u00e9rifier que postgres-01 est le leader<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01\n# patronictl reads ca.crt from \/etc\/etcd\/certs\/ \u2014 that directory is mode 700.\n# Run with sudo, or it will fail with an SSL certificate load error.\nsudo patronictl -c \/etc\/patroni\/config.yml list\n# Expected:\n# + Cluster: postgresql-cluster +----+-----------+\n# | Member        | Host              | Role   | State   | TL | Lag in MB |\n# +---------------+-------------------+--------+---------+----+-----------+\n# | postgresql-01 | 192.168.0.203    | Leader | running |  1 |           |\n# +---------------+-------------------+--------+---------+----+-----------+\n# If State is &quot;start failed&quot;: check journalctl -u patroni for the PostgreSQL error\n# If no Leader after 30 seconds: etcd health check (section 7 step 5)\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"step-3-start-patroni-on-postgres-02-and-postgres-03\">\u00c9tape 3 \u2014 D\u00e9marrer Patroni sur postgres-02 et postgres-03<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-02 and postgres-03\nsudo systemctl enable patroni &amp;&amp; sudo systemctl restart patroni\n# Patroni detects the existing primary in etcd, runs pg_basebackup from postgres-01,\n# and starts PostgreSQL as a streaming standby\n\n# Verify all three nodes\nsudo patronictl -c \/etc\/patroni\/config.yml list\n# Expected:\n# + Cluster: postgresql-cluster -----+----+-----------+\n# | Member        | Host           | Role    | State   | TL | Lag in MB |\n# +---------------+----------------+---------+---------+----+-----------+\n# | postgresql-01 | 192.168.0.203 | Leader  | running |  1 |           |\n# | postgresql-02 | 192.168.0.204 | Replica | running |  1 |         0 |\n# | postgresql-03 | 192.168.0.205 | Replica | running |  1 |         0 |\n# +---------------+----------------+---------+---------+----+-----------+\n# Lag in MB = 0: standbys are fully caught up with the primary\n# If a node shows &quot;stopped&quot;: check journalctl -u patroni on that node\n# If pg_basebackup failed: verify port 5432 is open between nodes\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"step-4-change-initial-cluster-state-to-existing-on-all-nodes\">\u00c9tape 4 \u2014 Changer initial-cluster-state en existing sur tous les n\u0153uds<\/h3>\n\n\n\n<p>Apr\u00e8s un amor\u00e7age r\u00e9ussi, <code>\u00e9tat-initial-du-cluster<\/code> doit \u00eatre chang\u00e9 de <code>nouveau<\/code> \u00e0 <code>existant<\/code>. <\/p>\n\n\n\n<p>Ceci emp\u00eache un n\u0153ud d'amorcer accidentellement un nouveau cluster s'il est red\u00e9marr\u00e9 ult\u00e9rieurement en isolation.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01, postgres-02, postgres-03\n# Edit \/etc\/etcd\/etcd.env and change:\n#   ETCD_INITIAL_CLUSTER_STATE=&quot;new&quot;\n# to:\n#   ETCD_INITIAL_CLUSTER_STATE=&quot;existing&quot;\n\nsudo systemctl restart etcd\n# Restart to apply the change\n# Expected: etcd rejoins the existing cluster cleanly\n# Verify: run the endpoint health command from section 7 step 5\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"9-ha-proxy-setup\">9. Configuration HAProxy<\/h2>\n\n\n\n<p>HAProxy achemine toutes les connexions d'application vers le primaire actuel en interrogeant l'API REST de Patroni. <\/p>\n\n\n\n<p>Seuls les appels principaux renvoient HTTP 200 <code>\/principal<\/code> \u2014 Les sauvegardes renvoient le HTTP 503. <\/p>\n\n\n\n<p>Trois n\u0153uds HAProxy assurent la redondance ; keepalived (section 11) d\u00e9place le VIP entre eux.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"step-1-install-ha-proxy\">\u00c9tape 1 \u2014 Installer HAProxy<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On haproxy-01, haproxy-02, haproxy-03\nsudo apt install -y haproxy\n\nhaproxy -v\n# Expected: HAProxy version 2.x.x\n# If an older version: add the HAProxy apt repository for the latest version\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"step-2-configure-ha-proxy\">\u00c9tape 2 \u2014 Configurer HAProxy<\/h3>\n\n\n\n<p>La configuration est identique sur les trois n\u0153uds HAProxy.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# \/etc\/haproxy\/haproxy.cfg\n\nfrontend postgres_frontend\n    bind *:5432\n    mode tcp\n    # mode tcp: HAProxy forwards raw TCP \u2014 PostgreSQL is not an HTTP protocol\n    timeout client 30s\n    # timeout client belongs in the frontend only \u2014 HAProxy will warn and ignore it in a backend\n    default_backend postgres_backend\n\nbackend postgres_backend\n    mode tcp\n    option tcp-check\n    option httpchk GET \/primary\n    # httpchk: HAProxy queries this endpoint on each node&#039;s Patroni REST API\n    # Only the current primary returns HTTP 200 on \/primary; standbys return 503\n    # This is how HAProxy knows which node to route write traffic to\n    http-check expect status 200\n    timeout connect 5s\n    # timeout connect and timeout server belong in the backend only\n    timeout server 30s\n    server postgresql-01 192.168.0.203:5432 port 8008 check check-ssl verify none\n    server postgresql-02 192.168.0.204:5432 port 8008 check check-ssl verify none\n    server postgresql-03 192.168.0.205:5432 port 8008 check check-ssl verify none\n    # port 8008: HAProxy checks the Patroni REST API port, not the PostgreSQL port\n    # check-ssl: use TLS when querying the REST API (Patroni REST API has TLS enabled)\n    # verify none: skip certificate CN verification \u2014 acceptable in a private cluster\n    # where nodes are identified by IP\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"step-3-validate-config-and-reload-ha-proxy\">\u00c9tape 3 \u2014 Valider la configuration et recharger HAProxy<\/h3>\n\n\n\n<p>Toujours valider la configuration avant de la recharger. <\/p>\n\n\n\n<p>HAProxy refusera de recharger si la configuration contient des erreurs.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On haproxy-01, haproxy-02, haproxy-03\nsudo haproxy -c -f \/etc\/haproxy\/haproxy.cfg\n# -c: check config only, do not start\n# -f: path to config file\n# Expected: Configuration file is valid\n# If you see warnings about timeout client\/server in the wrong section,\n# check that timeout client is in the frontend and timeout connect\/server are in the backend\n<\/pre><\/div>\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On haproxy-01, haproxy-02, haproxy-03\nsudo systemctl reload haproxy\n# reload: applies the new configuration without dropping existing connections\n# Expected: no output on the terminal; confirm success in the logs below\n<\/pre><\/div>\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nsudo journalctl -u haproxy --since &quot;1 minute ago&quot;\n# Expected lines (timestamps will differ):\n#   systemd&#91;1]: Reloading haproxy.service - HAProxy Load Balancer...\n#   systemd&#91;1]: Reloaded haproxy.service - HAProxy Load Balancer.\n# If you see &quot;Failed&quot;: check sudo haproxy -c output first\n<\/pre><\/div>\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nsudo tail -f \/var\/log\/syslog | grep haproxy\n# Watch the HAProxy logs to confirm it is checking the PostgreSQL nodes\n# Expected: repeated health check lines; one node should show &quot;UP&quot; (the primary)\n# If all nodes show &quot;DOWN&quot;: HAProxy cannot reach port 8008 \u2014 check firewall\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"10-keepalived-setup\">10. Configuration de Keepalived<\/h2>\n\n\n\n<p>keepalived g\u00e8re la VIP \u00e0 l'aide de VRRP. <\/p>\n\n\n\n<p>Un n\u0153ud HAProxy d\u00e9tient la VIP (le MA\u00ceTRE). <\/p>\n\n\n\n<p>Si la v\u00e9rification de l'\u00e9tat du MASTER \u00e9choue ou si le n\u0153ud tombe en panne, keepalived sur un n\u0153ud BACKUP revendique la VIP. <\/p>\n\n\n\n<p>Les applications se connectent toujours au VIP \u2014 les d\u00e9faillances des n\u0153uds HAProxy sont transparentes.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"step-1-install-keepalived\">\u00c9tape 1 \u2014 Installer keepalived<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On haproxy-01, haproxy-02, haproxy-03\nsudo apt update &amp;&amp; sudo apt install -y keepalived\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"step-2-create-the-ha-proxy-health-check-script\">\u00c9tape 2 \u2014 Cr\u00e9er le script de v\u00e9rification de l'\u00e9tat d'HAProxy<\/h3>\n\n\n\n<p>keepalived ex\u00e9cute ce script toutes les 2 secondes. <\/p>\n\n\n\n<p>Un code de sortie non nul indique \u00e0 keepalived que ce n\u0153ud ne doit pas d\u00e9tenir la VIP.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On haproxy-01, haproxy-02, haproxy-03\n# Create \/etc\/keepalived\/check_haproxy.sh with the following content:\n<\/pre><\/div>\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n#!\/bin\/bash\n\nPORT=5432\n\nif ! pidof haproxy &gt; \/dev\/null; then\n    # pidof haproxy: checks whether the HAProxy process is running\n    echo &quot;HAProxy is not running&quot;\n    exit 1\n    # exit 1: non-zero tells keepalived this node is unhealthy \u2014 VIP moves to a BACKUP\nfi\n\nif ! ss -ltn | grep -q &quot;:${PORT}&quot;; then\n    # ss -ltn: list listening TCP sockets; grep checks whether port 5432 is bound\n    echo &quot;HAProxy is not listening on port ${PORT}&quot;\n    exit 2\nfi\n\nexit 0\n# exit 0: tells keepalived this node is healthy and should keep (or receive) the VIP\n<\/pre><\/div>\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On haproxy-01, haproxy-02, haproxy-03\nsudo useradd -r -s \/bin\/false keepalived_script\n# -r: system account; -s \/bin\/false: no interactive login\n# keepalived runs health check scripts as this user when enable_script_security is active\n\nsudo chmod +x \/etc\/keepalived\/check_haproxy.sh\nsudo chown keepalived_script:keepalived_script \/etc\/keepalived\/check_haproxy.sh\nsudo chmod 700 \/etc\/keepalived\/check_haproxy.sh\n# 700: owner execute-only \u2014 keepalived requires the script not be writable by other users\n# when enable_script_security is active (configured in keepalived.conf below)\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"step-3-configure-keepalived\">\u00c9tape 3 \u2014 Configurer keepalived<\/h3>\n\n\n\n<p>Chaque n\u0153ud est diff\u00e9rent <code>\u00e9tat<\/code> et <code>priorit\u00e9<\/code>. <\/p>\n\n\n\n<p>Le MA\u00ceTRE (priorit\u00e9 100) d\u00e9tient initialement le VIP. <\/p>\n\n\n\n<p>S'il \u00e9choue, la SAUVEGARDE avec la priorit\u00e9 la plus \u00e9lev\u00e9e suivante (90) prend le t\u00e9moin VIP.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# \/etc\/keepalived\/keepalived.conf \u2014 haproxy-01 (MASTER)\n\nglobal_defs {\n    enable_script_security\n    # enable_script_security: prevents keepalived from running scripts owned by root\n    # or writable by anyone \u2014 prevents privilege escalation through health check scripts\n    script_user keepalived_script\n    # script_user: the OS user keepalived uses to execute health check scripts\n}\n\nvrrp_script check_haproxy {\n    script &quot;\/etc\/keepalived\/check_haproxy.sh&quot;\n    interval 2\n    # interval: run the health check every 2 seconds\n    fall 3\n    # fall: mark this node as failed after 3 consecutive failing checks (6 seconds total)\n    rise 2\n    # rise: mark this node as recovered after 2 consecutive passing checks (4 seconds)\n}\n\nvrrp_instance VI_1 {\n    state MASTER\n    # MASTER: this node holds the VIP initially on startup\n    interface enp0s3\n    # interface: the network interface where the VIP is assigned\n    # Run &quot;ip link show&quot; to find your interface name \u2014 may be ens3, enp0s3, eth0, etc.\n    # In this lab the interface is enp0s3 (VirtualBox default)\n    virtual_router_id 51\n    # virtual_router_id: identifies this VRRP group \u2014 must be identical on all three nodes\n    priority 100\n    # priority: the node with the highest priority wins the VIP election\n    # haproxy-01 wins by default (100 &gt; 90 &gt; 80)\n    advert_int 1\n    # advert_int: send VRRP advertisements every 1 second\n    authentication {\n        auth_type PASS\n        auth_pass changeme123\n        # auth_pass: shared password between all keepalived nodes \u2014 change before production use\n    }\n    virtual_ipaddress {\n        192.168.0.210\n        # The VIP \u2014 applications connect to this address on port 5432\n    }\n    track_script {\n        check_haproxy\n        # track_script: tie this VRRP instance to the HAProxy health check\n        # If the script exits non-zero, this node&#039;s effective priority drops\n        # below the BACKUPs and the VIP moves\n    }\n}\n<\/pre><\/div>\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# \/etc\/keepalived\/keepalived.conf \u2014 haproxy-02 (BACKUP, second priority)\n# Identical to haproxy-01 except:\n#   state BACKUP\n#   priority 90\n<\/pre><\/div>\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# \/etc\/keepalived\/keepalived.conf \u2014 haproxy-03 (BACKUP, lowest priority)\n# Identical to haproxy-01 except:\n#   state BACKUP\n#   priority 80\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"step-4-start-keepalived\">\u00c9tape 4 \u2014 D\u00e9marrer keepalived<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On haproxy-01, haproxy-02, haproxy-03\nsudo systemctl enable --now keepalived\n<\/pre><\/div>\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nsudo journalctl -u keepalived -f\n# Watch the keepalived logs \u2014 expected to see VRRP state transitions\n# haproxy-01 should log: &quot;(VI_1) Entering BACKUP STATE&quot; then &quot;(VI_1) Entering MASTER STATE&quot;\n#   (briefly enters BACKUP on startup, wins election after ~4 seconds due to priority 100)\n# haproxy-02 and haproxy-03 should log: &quot;(VI_1) Entering BACKUP STATE&quot;\n# Press Ctrl+C to stop following\n#\n# NOTE: &quot;Truncating auth_pass to 8 characters&quot; is a warning, not an error.\n# keepalived only uses the first 8 characters of auth_pass regardless of what you set.\n# This is fine as long as all nodes use the same password string \u2014 they will all truncate identically.\n<\/pre><\/div>\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# Verify the VIP is active on haproxy-01\nping -c 3 192.168.0.210\n# Expected: 3 packets transmitted, 3 received\n# If 0 received: VIP is not assigned to any node \u2014 check keepalived logs on all three HAProxy nodes\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"11-set-the-postgres-superuser-password\">11. D\u00e9finir le mot de passe du superutilisateur PostgreSQL<\/h2>\n\n\n\n<p>Patroni g\u00e8re son propre <code>pg_hba.conf<\/code> \u00e0 <code>\/var\/lib\/postgresql\/data\/pg_hba.conf<\/code> \u2014 pas l'emplacement du package par d\u00e9faut \u00e0 <code>\/etc\/postgresql\/18\/main\/pg_hba.conf<\/code>. <\/p>\n\n\n\n<p>Le fichier de Patroni contient uniquement <code>h\u00f4te ssl<\/code> entr\u00e9es et aucune entr\u00e9e de socket Unix locale. <\/p>\n\n\n\n<p>Cela signifie <code>sudo -u postgres psql<\/code> \u00e9chouera jusqu'\u00e0 ce qu'une entr\u00e9e locale soit ajout\u00e9e.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01 \u2014 check Patroni&#039;s actual pg_hba.conf\nsudo cat \/var\/lib\/postgresql\/data\/pg_hba.conf\n# Expected: &quot;Do not edit this file manually! It will be overwritten by Patroni!&quot;\n# followed by hostssl entries only \u2014 no local socket entry\n<\/pre><\/div>\n\n\n<p>Ajoutez une entr\u00e9e locale temporaire pour permettre \u00e0 l'utilisateur PostgreSQL de se connecter via socket :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01\necho &quot;local all postgres peer&quot; | sudo tee -a \/var\/lib\/postgresql\/data\/pg_hba.conf\n<\/pre><\/div>\n\n\n<p>Recharger la configuration \u00e0 l'aide de pg_ctl \u2014 pg_reload_conf() ne peut pas \u00eatre utilis\u00e9 car il n'y a pas encore de connexion socket :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01\nsudo -u postgres \/usr\/lib\/postgresql\/18\/bin\/pg_ctl reload -D \/var\/lib\/postgresql\/data\n# Expected: server signaled\n<\/pre><\/div>\n\n\n<p>D\u00e9finir le mot de passe :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01\nsudo -u postgres psql -c &quot;ALTER USER postgres PASSWORD &#039;strongpassword&#039;;&quot;\n# Expected: ALTER ROLE\n# IMPORTANT: this password must match postgresql.authentication.superuser.password in config.yml\n# If they differ, Patroni cannot connect to its local PostgreSQL instance and will report\n# unknown LSN \u2014 the node will appear stuck after a switchover or restart\n# The password is now stored in the database \u2014 it will survive Patroni overwriting pg_hba.conf\n<\/pre><\/div>\n\n\n<p>Patroni \u00e9crasera <code>pg_hba.conf<\/code> lors de son prochain cycle et de supprimer l'entr\u00e9e locale \u2014 cela est attendu et normal. <\/p>\n\n\n\n<p>Le mot de passe persiste quoi qu'il arrive dans le catalogue de la base de donn\u00e9es.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"12-verify-the-full-stack\">12. V\u00e9rifier la pile compl\u00e8te<\/h2>\n\n\n\n<p>Ex\u00e9cutez ces v\u00e9rifications dans l'ordre, une fois toutes les \u00e9tapes de configuration termin\u00e9es.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# Check 1: etcd cluster is healthy (run on postgres-01)\nsudo etcdctl \\\n  --endpoints=https:\/\/192.168.0.203:2379,https:\/\/192.168.0.204:2379,https:\/\/192.168.0.205:2379 \\\n  --cacert=\/etc\/etcd\/certs\/ca.crt \\\n  --cert=\/etc\/etcd\/certs\/etcd-node1.crt \\\n  --key=\/etc\/etcd\/certs\/etcd-node1.key \\\n  endpoint health\n# Expected: all three nodes report &quot;is healthy&quot;\n\n# Check 2: Patroni cluster has a leader and two replicas (run on any PostgreSQL node)\nsudo patronictl -c \/etc\/patroni\/config.yml list\n# Expected: one Leader, two Replica, all State = running, Lag = 0\n\n# Check 3: VIP is responding\nping -c 3 192.168.0.210\n# Expected: 3 packets received\n# If 0 received: VIP not assigned \u2014 check keepalived on all HAProxy nodes\n\n# Check 4: PostgreSQL is reachable through the VIP\npsql -h 192.168.0.210 -U postgres -c &quot;SELECT inet_server_addr(), pg_is_in_recovery();&quot;\n# Expected: inet_server_addr = 192.168.0.203 (the primary), pg_is_in_recovery = f\n# pg_is_in_recovery = f: confirms this is the primary (standbys return t)\n# If connection refused: HAProxy is not routing \u2014 check haproxy status and \/primary endpoint\n\n# Check 5: replication is streaming\npsql -h 192.168.0.210 -U postgres -c &quot;SELECT client_addr, replay_lag FROM pg_stat_replication;&quot;\n# Expected: two rows (one per standby), replay_lag = 00:00:00 or NULL (caught up)\n# If no rows: standbys are not streaming \u2014 check Patroni logs on standby nodes\n\n# Check 6: insert data on primary, read it from both standbys\npsql -h 192.168.0.210 -U postgres -c &quot;CREATE TABLE test (id serial, val text);&quot;\npsql -h 192.168.0.210 -U postgres -c &quot;INSERT INTO test (val) VALUES (&#039;replication works&#039;);&quot;\npsql -h 192.168.0.204 -U postgres -c &quot;SELECT * FROM test;&quot;\n# Expected: one row with val = &#039;replication works&#039;\n# Connects directly to postgres-02 (a standby) to confirm the data replicated\n# If &quot;relation does not exist&quot;: replication is not running \u2014 check pg_stat_replication\npsql -h 192.168.0.205 -U postgres -c &quot;SELECT * FROM test;&quot;\n# Expected: same row \u2014 confirms postgres-03 is also replicating\npsql -h 192.168.0.210 -U postgres -c &quot;DROP TABLE test;&quot;\n# Clean up the test table\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"13-switchover-planned\">13. Basculement (Planifi\u00e9)<\/h2>\n\n\n\n<p>Une bascule d\u00e9place le r\u00f4le principal vers une r\u00e9plique de secours sp\u00e9cifique sans aucune perte de donn\u00e9es. <\/p>\n\n\n\n<p>Patroni attend que le candidat de secours soit compl\u00e8tement synchronis\u00e9 avant de promouvoir.<\/p>\n\n\n\n<p><strong>Pr\u00e9requis \u2014 <code>ctl :<\/code> section dans config.yml :<\/strong> Le <code>ctl :<\/code> la section doit \u00eatre pr\u00e9sente avec <code>non s\u00e9curis\u00e9 : vrai<\/code> avant d'ex\u00e9cuter le basculement. <\/p>\n\n\n\n<p>Sans cela, patronictl ne peut pas s'authentifier aupr\u00e8s de l'API REST de Patroni et la bascule \u00e9chouera avec une erreur SSL. <\/p>\n\n\n\n<p>Commandes en lecture seule comme <code>patronictl lister<\/code> ils n'exigent pas l'API REST (ils utilisent etcd) \u2014 la section manquante n'est donc pas \u00e9vidente avant de tenter une op\u00e9ration d'\u00e9criture.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# Verify the ctl section is present on all PostgreSQL nodes before attempting switchover\nsudo tail -5 \/etc\/patroni\/config.yml\n# Expected:\n#   ctl:\n#     insecure: true\n# If missing: add it and reload patroni (sudo systemctl reload patroni)\n# NOTE: do not add cacert or certfile to the ctl section \u2014 only insecure: true\n# Adding a server certfile causes a bad TLS handshake and switchover still fails\n<\/pre><\/div>\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On any PostgreSQL node\nsudo patronictl -c \/etc\/patroni\/config.yml switchover postgresql-cluster \\\n  --leader postgresql-01 \\\n  # --leader: the current primary being demoted\n  # NOTE: older Patroni versions used --master here \u2014 newer versions use --leader\n  --candidate postgresql-02\n  # --candidate: the standby being promoted\n# patronictl will show the current topology and ask for confirmation \u2014 press Enter for &quot;now&quot;, then y\n\n# Patroni performs these steps automatically:\n# 1. Pauses writes on the primary (checkpoint)\n# 2. Waits for the candidate to confirm it has applied all WAL\n# 3. Demotes the current primary to standby\n# 4. Promotes the candidate to primary\n# 5. Reconfigures the old primary as a standby using pg_rewind\n<\/pre><\/div>\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nsudo patronictl -c \/etc\/patroni\/config.yml list\n# Expected: postgresql-02 now shows Role = Leader on a new timeline; postgresql-01 shows Role = Replica\n# postgresql-01 may briefly show &quot;stopped&quot; \u2014 this is pg_rewind running; wait 10 seconds and recheck\n# If postgresql-01 stays &quot;stopped&quot;: pg_rewind failed\n#   Fix: sudo patronictl -c \/etc\/patroni\/config.yml reinit postgresql-cluster postgresql-01\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"14-failover-unplanned\">14. Basculement (non planifi\u00e9)<\/h2>\n\n\n\n<p>Lorsque le primaire \u00e9choue, Patroni le d\u00e9tecte automatiquement :<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Le primaire ne parvient pas \u00e0 renouveler son bail de leader dans etcd dans les <code>ttl<\/code> secondes (30 par d\u00e9faut)<\/li>\n\n\n\n<li>Patroni sur les n\u0153uds restants organise une \u00e9lection dans etcd<\/li>\n\n\n\n<li>Le veille \u00e0 moins de d\u00e9calage qui se trouve \u00e0 l'int\u00e9rieur <code>latence_maximale_en_cas_de_basculement<\/code> est \u00e9lu et promu<\/li>\n\n\n\n<li>Les autres solutions de secours se reconnectent au nouveau primaire \u00e0 l'aide de <code>pg_rewind<\/code><\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"simulate-a-primary-failure\">Simuler une panne primaire<\/h3>\n\n\n\n<p>Arr\u00eatez Patroni sur le responsable actuel pour simuler un crash. <\/p>\n\n\n\n<p>Patroni g\u00e8re PostgreSQL \u2014 arr\u00eater Patroni arr\u00eate \u00e9galement PostgreSQL sur ce n\u0153ud.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01 (the current primary)\nsudo systemctl stop patroni\n# This simulates a primary crash \u2014 PostgreSQL stops and the leader lease expires\n<\/pre><\/div>\n\n\n<p>Sur postgres-02 ou postgres-03, observez le basculement automatique\u00a0:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-02 or postgres-03\nwatch -n2 &quot;sudo patronictl -c \/etc\/patroni\/config.yml list&quot;\n# Expected sequence over ~30 seconds (ttl):\n# 1. postgresql-01 disappears or shows &quot;stopped&quot;\n# 2. One of the remaining nodes shows Leader on a new timeline\n# 3. The other remaining node shows Replica streaming\n# Press Ctrl+C when the new leader is confirmed\n<\/pre><\/div>\n\n\n<p>Ramener postgres-01 :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01\nsudo systemctl start patroni\n<\/pre><\/div>\n\n\n<p>V\u00e9rifiez le cluster \u2014 postgres-01 devrait se rattacher en tant que r\u00e9plique :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nsudo patronictl -c \/etc\/patroni\/config.yml list\n# Expected: postgres-01 shows Replica streaming, Lag = 0\n# If postgres-01 shows &quot;start failed&quot;: check sudo journalctl -u patroni -n 30 --no-pager\n<\/pre><\/div>\n\n\n<p>Rebasculer sur postgres-01 comme primaire quand vous serez pr\u00eat :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nsudo patronictl -c \/etc\/patroni\/config.yml switchover postgresql-cluster \\\n  --leader &lt;new-leader&gt; \\\n  --candidate postgresql-01\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"manual-failover-use-only-when-automatic-failover-has-not-triggered\">Basculement manuel \u2014 \u00e0 n'utiliser que lorsque le basculement automatique n'a pas \u00e9t\u00e9 d\u00e9clench\u00e9<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nsudo patronictl -c \/etc\/patroni\/config.yml failover postgresql-cluster \\\n  --leader postgresql-01 \\\n  --candidate postgresql-02 \\\n  --force\n  # --force: skip the confirmation prompt\n  # Use only when the primary is confirmed down\n  # Without --force, patronictl prompts you to confirm before proceeding\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"15-day-to-day-operations\">15. Op\u00e9rations quotidiennes<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"check-cluster-status\">V\u00e9rifier l'\u00e9tat du cluster<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nsudo patronictl -c \/etc\/patroni\/config.yml list\n# Shows all members, roles (Leader\/Replica), state (running\/stopped\/start failed),\n# timeline, and replication lag in MB\n# Run this first whenever diagnosing any cluster issue\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"pause-and-resume-automatic-failover\">Mettre en pause et reprendre le basculement automatique<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# Pause \u2014 Patroni will not promote any standby while paused\n# Use during planned maintenance to prevent accidental failover\nsudo patronictl -c \/etc\/patroni\/config.yml pause postgresql-cluster\n\n# Resume \u2014 restore automatic failover\nsudo patronictl -c \/etc\/patroni\/config.yml resume postgresql-cluster\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"restart-postgre-sql-on-a-node\">Red\u00e9marrer PostgreSQL sur un n\u0153ud<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# Always restart PostgreSQL through patronictl \u2014 never directly via systemctl\n# Running &quot;systemctl restart postgresql&quot; bypasses Patroni and causes inconsistent state\nsudo patronictl -c \/etc\/patroni\/config.yml restart postgresql-cluster postgresql-02\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"reload-configuration\">Recharger la configuration<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# Apply postgresql.conf changes across all nodes without a restart\nsudo patronictl -c \/etc\/patroni\/config.yml reload postgresql-cluster\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"edit-cluster-dcs-configuration\">Modifier la configuration du cluster DCS<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# Edit settings stored in etcd (ttl, maximum_lag_on_failover, synchronous_mode, etc.)\nsudo patronictl -c \/etc\/patroni\/config.yml edit-config postgresql-cluster\n# Opens the current configuration in your $EDITOR\n# Changes take effect immediately after saving \u2014 no restart required\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"reinitialise-a-failed-standby\">R\u00e9initialiser un standby d\u00e9faillant<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# If pg_rewind fails after a failover, wipe and rebuild the standby from the primary\nsudo patronictl -c \/etc\/patroni\/config.yml reinit postgresql-cluster postgresql-03\n# Wipes data_dir on postgresql-03 and runs pg_basebackup from the current primary\n# Wipes the standby and rebuilds it from the current primary\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"16-synchronous-mode-zero-data-loss\">16. Mode Synchrone (Perte de donn\u00e9es nulle)<\/h2>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nsudo patronictl -c \/etc\/patroni\/config.yml edit-config postgresql-cluster\n<\/pre><\/div>\n\n\n<p>Ajouter ou mettre \u00e0 jour :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nsynchronous_mode: true\n# true: commits on the primary wait for at least one standby to confirm before returning\n# Patroni sets synchronous_standby_names automatically \u2014 no manual configuration needed\n\nsynchronous_mode_strict: false\n# false (default): if no synchronous standby is available, the primary continues writing\n# true: if no synchronous standby is available, the primary stops accepting writes entirely\n#       use true only when zero data loss is mandatory and availability can be sacrificed\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"17-monitoring\">17. Surveillance<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"patroni-rest-api\">API REST Patroni<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# Check HTTP status code only \u2014 this is what HAProxy checks internally\ncurl -k -o \/dev\/null -w &quot;%{http_code}\\n&quot; https:\/\/192.168.0.203:8008\/primary\n# Expected on the primary: 200\n# Expected on a replica: 503\n# -k: skip certificate verification; -o \/dev\/null: discard body; -w: print status code only\n\ncurl -k -o \/dev\/null -w &quot;%{http_code}\\n&quot; https:\/\/192.168.0.203:8008\/replica\n# Expected on a replica: 200\n# Expected on the primary: 503\n\n# Check full JSON response (useful for debugging)\ncurl -k https:\/\/192.168.0.203:8008\/primary\n# Response includes: role, server_version, timeline, replication state of each standby\n\ncurl -k https:\/\/192.168.0.203:8008\/cluster | python3 -m json.tool\n# Full cluster status in formatted JSON\n# Expected: all members listed with roles, state, and lag\n\ncurl -k https:\/\/192.168.0.203:8008\/health\n# Expected: HTTP 200 if the node is running normally\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"replication-lag\">D\u00e9calage de r\u00e9plication<\/h3>\n\n\n\n<p>Se connecter directement au n\u0153ud principal \u2014 <code>pg_stat_replication<\/code> n'a que des lignes sur le primaire, pas sur les r\u00e9pliques. <\/p>\n\n\n\n<p>HAProxy achemine \u00e9galement le port 5432 du VIP vers le primaire, donc l'un ou l'autre fonctionne.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# Option 1 \u2014 direct to primary\npsql -h 192.168.0.203 -U postgres -c &quot;SELECT client_addr, replay_lag, sync_state FROM pg_stat_replication;&quot;\n\n# Option 2 \u2014 through VIP (HAProxy routes all connections on port 5432 to the primary)\npsql -h 192.168.0.210 -p 5432 -U postgres -c &quot;SELECT client_addr, replay_lag, sync_state FROM pg_stat_replication;&quot;\n<\/pre><\/div>\n\n\n<p>R\u00e9sultat attendu :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n  client_addr  | replay_lag | sync_state\n---------------+------------+------------\n 192.168.0.204 |            | async\n 192.168.0.205 |            | async\n(2 rows)\n<\/pre><\/div>\n\n\n<ul class=\"wp-block-list\">\n<li>Une ligne par veille connect\u00e9e<\/li>\n\n\n\n<li><code>replay_lag = NUL<\/code> (vide) : la veille est compl\u00e8tement \u00e0 jour - normale au repos<\/li>\n\n\n\n<li><code>latence_rejeu<\/code> En croissance : le standby est \u00e0 la tra\u00eene \u2014 v\u00e9rifiez le r\u00e9seau et les journaux du standby Patroni<\/li>\n\n\n\n<li>0 lignes : aucun standby en cours - v\u00e9rifiez <code>patronictl lister<\/code> pour confirmer les \u00e9tats des r\u00e9pliques ; si les r\u00e9pliques s'affichent comme en cours d'ex\u00e9cution, v\u00e9rifiez <code>primary_conninfo<\/code> en <code>postgresql.auto.conf<\/code> en veille<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"18-full-reset\">18. R\u00e9initialisation compl\u00e8te<\/h2>\n\n\n\n<p>Utilisez cette proc\u00e9dure pour reconstruire enti\u00e8rement le cluster \u00e0 partir de z\u00e9ro \u2014 par exemple, apr\u00e8s une d\u00e9faillance du laboratoire, une mauvaise configuration irr\u00e9cup\u00e9rable ou pour r\u00e9ex\u00e9cuter le laboratoire \u00e0 partir de la section 9. Les certificats TLS sont conserv\u00e9s sur tous les n\u0153uds. <\/p>\n\n\n\n<p>Seul l'\u00e9tat du cluster etcd et les donn\u00e9es PostgreSQL sont effac\u00e9s.<\/p>\n\n\n\n<p>Ex\u00e9cutez toutes les commandes de cette section sur <strong>postgres-01, postgres-02, and postgres-03<\/strong> sauf indication contraire.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"step-1-stop-patroni-and-etcd-on-all-postgre-sql-nodes\">\u00c9tape 1 \u2014 Arr\u00eatez Patroni et etcd sur tous les n\u0153uds PostgreSQL<\/h3>\n\n\n\n<p>Patroni doit s'arr\u00eater avant etcd afin qu'il puisse lib\u00e9rer proprement son verrou de leader. <\/p>\n\n\n\n<p>Si etcd est arr\u00eat\u00e9 en premier, Patroni perd sa connexion DCS et peut se bloquer.<\/p>\n\n\n\n<p>Sur postgres-01 :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01\nsudo systemctl stop patroni\n# Stops PostgreSQL gracefully via Patroni \u2014 do not use systemctl stop postgresql directly\n\nsudo systemctl stop etcd\n# Stops the etcd member on this node\n<\/pre><\/div>\n\n\n<p>R\u00e9p\u00e9tez sur postgres-02 et postgres-03 avant de continuer.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"step-2-wipe-etcd-and-postgre-sql-data-directories\">\u00c9tape 2 \u2014 Effacer les r\u00e9pertoires de donn\u00e9es etcd et PostgreSQL<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01, postgres-02, postgres-03\nsudo rm -rf \/var\/lib\/etcd\/\n# Removes all etcd WAL and snapshot data \u2014 the etcd cluster will re-bootstrap from scratch\n\nsudo rm -rf \/var\/lib\/postgresql\/data\/\n# Removes all PostgreSQL data files \u2014 Patroni will re-initialise via pg_basebackup on standbys\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"step-3-recreate-directories-with-correct-ownership\">\u00c9tape 3 \u2014 Recr\u00e9er les r\u00e9pertoires avec la propri\u00e9t\u00e9 correcte<\/h3>\n\n\n\n<p><code>rm -rf<\/code> supprime le r\u00e9pertoire lui-m\u00eame, pas seulement son contenu. <\/p>\n\n\n\n<p>Les r\u00e9pertoires doivent \u00eatre recr\u00e9\u00e9s avant que etcd et Patroni ne puissent y \u00e9crire.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01, postgres-02, postgres-03\nsudo mkdir -p \/var\/lib\/etcd\/\nsudo chown etcd:etcd \/var\/lib\/etcd\/\n# etcd runs as the etcd user \u2014 it must own its data directory\n\nsudo mkdir -p \/var\/lib\/postgresql\/data\nsudo chown postgres:postgres \/var\/lib\/postgresql\/data\n# Patroni runs as the postgres user \u2014 it must own the PostgreSQL data directory\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"step-4-restore-acl-permissions-on-etcd-certificates\">\u00c9tape 4 \u2014 Restaurer les autorisations ACL sur les certificats etcd<\/h3>\n\n\n\n<p><code>rm -rf<\/code> sur le r\u00e9pertoire des donn\u00e9es n'affecte pas <code>\/etc\/etcd\/certs\/<\/code>, mais si vous avez \u00e9galement effac\u00e9 le r\u00e9pertoire certs lors du d\u00e9pannage, le <code>PostgreSQL<\/code> l'utilisateur aura perdu l'acc\u00e8s en lecture aux certificats etcd. <\/p>\n\n\n\n<p>Ex\u00e9cutez cette \u00e9tape pour r\u00e9tablir ces autorisations.<\/p>\n\n\n\n<p>Sur postgres-01 :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01\nsudo setfacl -m u:postgres:r \/etc\/etcd\/certs\/ca.crt\nsudo setfacl -m u:postgres:r \/etc\/etcd\/certs\/etcd-node1.crt\nsudo setfacl -m u:postgres:r \/etc\/etcd\/certs\/etcd-node1.key\n# Grants the postgres OS user read access to the etcd TLS files\n# Required because Patroni (running as postgres) connects to etcd over TLS\n<\/pre><\/div>\n\n\n<p>Sur postgres-02 :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-02\nsudo setfacl -m u:postgres:r \/etc\/etcd\/certs\/ca.crt\nsudo setfacl -m u:postgres:r \/etc\/etcd\/certs\/etcd-node2.crt\nsudo setfacl -m u:postgres:r \/etc\/etcd\/certs\/etcd-node2.key\n<\/pre><\/div>\n\n\n<p>Sur postgres-03 :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-03\nsudo setfacl -m u:postgres:r \/etc\/etcd\/certs\/ca.crt\nsudo setfacl -m u:postgres:r \/etc\/etcd\/certs\/etcd-node3.crt\nsudo setfacl -m u:postgres:r \/etc\/etcd\/certs\/etcd-node3.key\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"step-5-reset-etcd-initial-cluster-state-to-new\">\u00c9tape 5 \u2014 R\u00e9initialiser ETCD_INITIAL_CLUSTER_STATE \u00e0 \u201cnew\u201d<\/h3>\n\n\n\n<p>Apr\u00e8s le premier amor\u00e7age, <code>etcd.env<\/code> sur tous les n\u0153uds a \u00e9t\u00e9 chang\u00e9 en <code>existant<\/code>. <\/p>\n\n\n\n<p>Pour une r\u00e9initialisation compl\u00e8te, il doit \u00eatre remis \u00e0 <code>nouveau<\/code> donc etcd traite ceci comme une nouvelle formation de cluster.<\/p>\n\n\n\n<p>Sur chaque n\u0153ud, sauvegarder et modifier <code>\/etc\/etcd\/etcd.env<\/code>:<\/p>\n\n\n\n<p>Sur postgres-01 :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01\nsudo cp \/etc\/etcd\/etcd.env \/etc\/etcd\/etcd.env.$(date +%Y%m%d)\nsudo vi \/etc\/etcd\/etcd.env\n<\/pre><\/div>\n\n\n<p>Garder:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nETCD_INITIAL_CLUSTER_STATE=&quot;existing&quot;\n<\/pre><\/div>\n\n\n<p>\u00c0 :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nETCD_INITIAL_CLUSTER_STATE=&quot;new&quot;\n<\/pre><\/div>\n\n\n<p>R\u00e9p\u00e9tez sur postgres-02 et postgres-03.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"step-6-start-etcd-on-all-nodes\">\u00c9tape 6 \u2014 D\u00e9marrer etcd sur tous les n\u0153uds<\/h3>\n\n\n\n<p>etcd doit \u00eatre en cours d'ex\u00e9cution sur les trois n\u0153uds avant que Patroni ne d\u00e9marre. <\/p>\n\n\n\n<p>D\u00e9marrez etcd sur les trois n\u0153uds avant de passer \u00e0 l'\u00e9tape 7.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01, postgres-02, postgres-03\nsudo systemctl start etcd\n<\/pre><\/div>\n\n\n<p>V\u00e9rifiez que les trois membres sont sains avant de d\u00e9marrer Patroni :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01\nETCDCTL_API=3 etcdctl \\\n  --cacert=\/etc\/etcd\/certs\/ca.crt \\\n  --cert=\/etc\/etcd\/certs\/etcd-node1.crt \\\n  --key=\/etc\/etcd\/certs\/etcd-node1.key \\\n  --endpoints=https:\/\/192.168.0.203:2379,https:\/\/192.168.0.204:2379,https:\/\/192.168.0.205:2379 \\\n  endpoint health\n# Expected: all three endpoints report &quot;is healthy&quot;\n# If any node is unhealthy: check journalctl -u etcd on that node before starting Patroni\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"step-7-start-patroni-on-all-nodes\">\u00c9tape 7 \u2014 Lancer Patroni sur tous les n\u0153uds<\/h3>\n\n\n\n<p>D\u00e9marrez d'abord Patroni sur postgres-01. <\/p>\n\n\n\n<p>Patroni sur postgres-01 va initialiser un nouveau ma\u00eetre PostgreSQL. <\/p>\n\n\n\n<p>Ce n'est qu'apr\u00e8s que postgres-01 soit en cours d'ex\u00e9cution et soit indiqu\u00e9 comme Leader que postgres-02 et postgres-03 devraient \u00eatre d\u00e9marr\u00e9s \u2014 ils rejoindront en tant que r\u00e9pliques via pg_basebackup.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01 \u2014 start first\nsudo systemctl start patroni\n<\/pre><\/div>\n\n\n<p>Attendez que postgres-01 apparaisse comme Leader :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01\npatronictl -c \/etc\/patroni\/config.yml list\n# Expected: postgresql-01 shows as Leader, state running\n# Wait for this before starting postgres-02 and postgres-03\n<\/pre><\/div>\n\n\n<p>Puis lancez Patroni sur les n\u0153uds restants :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-02\nsudo systemctl start patroni\n<\/pre><\/div>\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-03\nsudo systemctl start patroni\n<\/pre><\/div>\n\n\n<p>V\u00e9rification finale :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code\" data-no-translation=\"\"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# On postgres-01\npatronictl -c \/etc\/patroni\/config.yml list\n# Expected:\n# + Cluster: postgres-cluster --------+----+-----------+\n# | Member        | Host            | Role    | State   | TL | Lag in MB |\n# +---------------+-----------------+---------+---------+----+-----------+\n# | postgresql-01 | 192.168.0.203:5432 | Leader | running |  1 |           |\n# | postgresql-02 | 192.168.0.204:5432 | Replica | running |  1 |         0 |\n# | postgresql-03 | 192.168.0.205:5432 | Replica | running |  1 |         0 |\n# +---------------+-----------------+---------+---------+----+-----------+\n# TL 1: fresh cluster, timeline resets to 1 on full reset\n# If a node shows &quot;start failed&quot;: check journalctl -u patroni on that node\n<\/pre><\/div>\n\n\n<p>Continuez \u00e0 partir de la section 9, \u00e9tape 3 pour rev\u00e9rifier le cluster et l'ensemble <code>ETCD_INITIAL_CLUSTER_STATE=existing<\/code>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"19-common-issues\">19. Probl\u00e8mes courants<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Probl\u00e8me<\/th><th>Cause<\/th><th>R\u00e9parer<\/th><\/tr><\/thead><tbody><tr><td>Aucun dirigeant \u00e9lu<\/td><td>perte de quorum etcd<\/td><td>Restaurer etcd ; v\u00e9rifier le port 2380 entre les n\u0153uds PostgreSQL<\/td><\/tr><tr><td>Patroni ne peut pas se connecter \u00e0 etcd<\/td><td>Mauvaise configuration TLS<\/td><td>V\u00e9rifier les chemins de cacert\/cert\/key dans config.yml ; v\u00e9rifier les permissions setfacl<\/td><\/tr><tr><td>N\u0153ud bloqu\u00e9 dans <code>d\u00e9marrage \u00e9chou\u00e9<\/code><\/td><td>PostgreSQL ne d\u00e9marre pas<\/td><td>V\u00e9rifier journalctl -u patroni; corriger la configuration, puis <code>patronictl reinit<\/code><\/td><\/tr><tr><td>En attente de transmission<\/td><td>Identifiants incorrects ou pg_hba<\/td><td>V\u00e9rifiez primary_conninfo dans postgresql.auto.conf; v\u00e9rifiez le r\u00e9plicateur dans pg_hba<\/td><\/tr><tr><td><code>pg_rewind<\/code> \u00e9choue apr\u00e8s basculement<\/td><td><code>wal_log_hints<\/code> non activ\u00e9<\/td><td>Activer wal\\_log\\_hints = on avant l'initialisation du cluster ; utiliser <code>patronictl reinit<\/code> en cas de secours<\/td><\/tr><tr><td>Le VIP ne r\u00e9pond pas<\/td><td>keepalived ne fonctionne pas<\/td><td>V\u00e9rifier le statut de systemctl keepalived ; v\u00e9rifier journalctl -u keepalived sur tous les n\u0153uds HAProxy<\/td><\/tr><tr><td>Routage HAProxy vers un n\u0153ud incorrect<\/td><td>API REST Patroni inaccessible<\/td><td>V\u00e9rifier que le port 8008 est ouvert ; v\u00e9rifier que Patroni fonctionne sur tous les n\u0153uds PostgreSQL<\/td><\/tr><tr><td>Le cluster etcd se r\u00e9forme au red\u00e9marrage<\/td><td>\u00e9tat initial du cluster toujours \u201c nouveau \u201d<\/td><td>Modifier \u201cexisting\u201d dans etcd.env sur tous les n\u0153uds et red\u00e9marrer etcd<\/td><\/tr><tr><td>PostgreSQL d\u00e9marre en dehors de Patroni<\/td><td><code>systemctl start postgresql<\/code> ex\u00e9cuter directement<\/td><td>Arr\u00eater PostgreSQL ; red\u00e9marrer via <code>patronictl restart<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"20-key-commands-reference\">20. R\u00e9f\u00e9rence des commandes cl\u00e9s<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Commande<\/th><th>Description<\/th><\/tr><\/thead><tbody><tr><td><code>patronictl lister<\/code><\/td><td>Afficher tous les membres du cluster, les r\u00f4les, l'\u00e9tat et le d\u00e9calage de r\u00e9plication<\/td><\/tr><tr><td><code>patronictl basculement<\/code><\/td><td>Commutation planifi\u00e9e vers un n\u0153ud sp\u00e9cifique \u2014 perte de donn\u00e9es nulle<\/td><\/tr><tr><td><code>patronictl failover --force<\/code><\/td><td>Basculement forc\u00e9 \u2014 \u00e0 n'utiliser que lorsque le primaire est confirm\u00e9 comme \u00e9tant hors service<\/td><\/tr><tr><td><code>patronictl pause<\/code><\/td><td>D\u00e9sactiver le basculement automatique \u2014 \u00e0 utiliser lors de la maintenance planifi\u00e9e<\/td><\/tr><tr><td><code>patronictl reprendre<\/code><\/td><td>R\u00e9activer le basculement automatique<\/td><\/tr><tr><td><code>patronictl restart<\/code><\/td><td>Red\u00e9marrer PostgreSQL sur un n\u0153ud via Patroni \u2014 n'utilisez jamais systemctl directement<\/td><\/tr><tr><td><code>patronictl reload<\/code><\/td><td>Recharger postgresql.conf sur tous les n\u0153uds du cluster<\/td><\/tr><tr><td><code>patronictl reinit<\/code><\/td><td>Effacer et reconstruire une r\u00e9plique de secours \u00e0 partir de la primaire actuelle<\/td><\/tr><tr><td><code>patronictl edit-config<\/code><\/td><td>Modifier la configuration du cluster DCS stock\u00e9e dans etcd<\/td><\/tr><tr><td><code>curl -k https:\/\/:8008\/primary<\/code><\/td><td>HTTP 200 si ce n\u0153ud est actuellement le principal<\/td><\/tr><tr><td><code>curl -k https:\/\/:8008\/r\u00e9plique<\/code><\/td><td>HTTP 200 si ce n\u0153ud est actuellement une r\u00e9plique<\/td><\/tr><tr><td><code>curl -k https:\/\/:8008\/cluster<\/code><\/td><td>Full cluster status in JSON<\/td><\/tr><tr><td><code>etcdctl endpoint health<\/code><\/td><td>V\u00e9rifier l'\u00e9tat de tous les membres du cluster etcd<\/td><\/tr><tr><td><code>ip addr show enp0s3<\/code><\/td><td>Confirmer quel n\u0153ud HAProxy d\u00e9tient actuellement le VIP (interface par d\u00e9faut VirtualBox)<\/td><\/tr><\/tbody><\/table><\/figure>","protected":false},"excerpt":{"rendered":"<p>Guide de laboratoire \u00e9tape par \u00e9tape pour construire un cluster PostgreSQL HA \u00e0 6 n\u0153uds avec Patroni, etcd, HAProxy et keepalived. Couvre la configuration TLS, le basculement (failover), le basculement planifi\u00e9 (switchover), la surveillance et les op\u00e9rations quotidiennes.<\/p>","protected":false},"author":1,"featured_media":6695,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_focus_keyword":"","rank_math_title":"","rank_math_description":"","rank_math_robots":"","rank_math_og_title":"","rank_math_og_description":"","jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[126],"tags":[145,144],"class_list":["post-6685","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-postgresql","tag-high-availability","tag-patroni"],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/rootfan.com\/wp-content\/uploads\/pexels-photo-36043291-1.jpeg?fit=1880%2C1253&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/rootfan.com\/fr\/wp-json\/wp\/v2\/posts\/6685","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rootfan.com\/fr\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rootfan.com\/fr\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rootfan.com\/fr\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rootfan.com\/fr\/wp-json\/wp\/v2\/comments?post=6685"}],"version-history":[{"count":13,"href":"https:\/\/rootfan.com\/fr\/wp-json\/wp\/v2\/posts\/6685\/revisions"}],"predecessor-version":[{"id":6702,"href":"https:\/\/rootfan.com\/fr\/wp-json\/wp\/v2\/posts\/6685\/revisions\/6702"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rootfan.com\/fr\/wp-json\/wp\/v2\/media\/6695"}],"wp:attachment":[{"href":"https:\/\/rootfan.com\/fr\/wp-json\/wp\/v2\/media?parent=6685"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rootfan.com\/fr\/wp-json\/wp\/v2\/categories?post=6685"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rootfan.com\/fr\/wp-json\/wp\/v2\/tags?post=6685"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}