{"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-08T00:25:38","modified_gmt":"2026-04-07T22:25:38","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\n<pre class=\"wp-block-code\"><code>          +----------+ +----------+ +----------+\n          | etcd | | etcd | | etcd | | etcd | | etcd\n          | Patroni | | Patroni | | Patroni | | Patroni | | Patroni\n          | +PG | | +PG | | +PG | | +PG |\n          | | node1 | | node2 | | node3 |\n          | PRIMAIRE | | STANDBY | | STANDBY |\n          +----------+ +----------+ +----------+\n                \\ | \/\n                 \n          +----------+ +----------+ +----------+\n          HAProxy | | HAProxy | | HAProxy | HAProxy | HAProxy | HAProxy | HAProxy | HAProxy | HAProxy\n          | node1 | | node2 | | node3 |\n          | (MA\u00ceTRE) | | (SAUVEGARDE) | (SAUVEGARDE) | (MA\u00ceTRE) | (MA\u00ceTRE) | (MA\u00ceTRE) | (MA\u00ceTRE) | (MA\u00ceTRE) | (MA\u00ceTRE) | (MA\u00ceTRE)\n          +----------+ +----------+ +----------+\n                \\ | \/\n                 \\ | \/\n              [keepalived VIP : 192.168.0.210:5432]\n                              \n                        Applications<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code>sudo timedatectl set-timezone Europe\/Madrid\ntimedatectl\n# Attendu : Fuseau horaire : Europe\/Madrid (CET\/CEST, +0100\/+0200)\n# Service NTP devrait indiquer : actif\n# Horloge syst\u00e8me synchronis\u00e9e : oui<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur postgres-01, postgres-02, postgres-03\nsudo apt update\nsudo apt install -y postgresql-common\n# postgresql-common : fournit le script de configuration du d\u00e9p\u00f4t PGDG\n\nsudo \/usr\/share\/postgresql-common\/pgdg\/apt.postgresql.org.sh\n# Ce script ajoute le d\u00e9p\u00f4t apt officiel de PostgreSQL (postgresql.org)\n# et importe sa cl\u00e9 GPG \u2014 garantit que vous obtenez la derni\u00e8re version de PostgreSQL,\n# et non la version plus ancienne int\u00e9gr\u00e9e \u00e0 Ubuntu\n\nsudo apt update\nsudo apt install -y postgresql-18 postgresql-contrib-18\n# Installe la version 18 explicitement \u2014 le m\u00e9ta-paquet g\u00e9n\u00e9rique \"postgresql\" installe la version\n# par d\u00e9faut int\u00e9gr\u00e9e \u00e0 Ubuntu (16) en plus de la version PGDG, laissant deux versions install\u00e9es\n# Sp\u00e9cifiez toujours le num\u00e9ro de version pour \u00e9viter cela\n# postgresql-contrib-18 : modules suppl\u00e9mentaires, y compris pg_rewind, que Patroni utilise\n# pour resynchroniser l'ancien primaire apr\u00e8s un basculement sans sauvegarde de base compl\u00e8te<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur postgres-01, postgres-02, postgres-03\nsudo systemctl stop postgresql\n# Patroni g\u00e8re enti\u00e8rement le d\u00e9marrage de PostgreSQL\n# Si PostgreSQL est d\u00e9j\u00e0 en cours d'ex\u00e9cution lorsque Patroni d\u00e9marre, Patroni \u00e9chouera avec :\n# \" postmaster est d\u00e9j\u00e0 en cours d'ex\u00e9cution \"\n\nsudo systemctl disable postgresql\n# Emp\u00eache PostgreSQL de d\u00e9marrer automatiquement au d\u00e9marrage\n# Le propre service systemd de Patroni d\u00e9marre PostgreSQL lorsque le n\u0153ud rejoint le cluster<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur 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# T\u00e9l\u00e9charger le binaire etcd directement depuis les releases GitHub\n# Le paquet apt est souvent obsol\u00e8te \u2014 toujours installer depuis les releases officielles\n# V\u00e9rifier sur https:\/\/github.com\/etcd-io\/etcd\/releases la derni\u00e8re version stable\n\ntar xvf etcd-v3.6.10-linux-amd64.tar.gz\n# xvf : extraire (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 : le binaire serveur etcd\n# etcdctl : l'interface en ligne de commande client etcd \u2014 utilis\u00e9e pour les v\u00e9rifications d'int\u00e9grit\u00e9 et l'inspection de l'\u00e9tat du cluster\n\n# V\u00e9rifier l'installation\netcd --version\n# Attendu : etcd Version : 3.6.10\n# Si \"etcd: command not found\" : \/usr\/local\/bin n'est pas dans le PATH \u2014 ex\u00e9cuter : export PATH=$PATH:\/usr\/local\/bin\n\netcdctl version\n# Attendu : etcdctl version : 3.6.10<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur postgres-01, postgres-02, postgres-03\nsudo useradd --system --home \/var\/lib\/etcd --shell \/bin\/false etcd\n\n# --system : cr\u00e9e un compte syst\u00e8me sans shell de connexion par d\u00e9faut\n# --home \/var\/lib\/etcd : etcd y stocke ses donn\u00e9es\n# --shell \/bin\/false : emp\u00eache la connexion interactive \u2014 etcd s'ex\u00e9cute uniquement en tant que d\u00e9mon<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur postgres-01\nmkdir ~\/certs &amp;&amp; cd ~\/certs\n# Tous les fichiers de certificat sont cr\u00e9\u00e9s ici avant d'\u00eatre copi\u00e9s sur chaque n\u0153ud<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur postgres-01\nopenssl genrsa -out ca.key 2048\n# genrsa: g\u00e9n\u00e8re une cl\u00e9 priv\u00e9e RSA ; 2048 : longueur de la cl\u00e9 en bits\n\nopenssl req -x509 -new -nodes -key ca.key -subj \"\/CN=etcd-ca\" -days 7300 -out ca.crt\n# req -x509 : cr\u00e9e un certificat auto-sign\u00e9 (pas une demande de signature)\n# -new -nodes : nouveau certificat, pas de phrase de passe sur la cl\u00e9 priv\u00e9e\n# -subj \"\/CN=etcd-ca\" : le nom commun (Common Name) du certificat \u2014 l'identifie comme le CA du cluster\n# -days 7300 : valide pendant 20 ans\n# ca.crt : distribu\u00e9 \u00e0 chaque n\u0153ud comme racine de confiance<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># 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 \"\/CN=etcd-node1\" \\\n  -config temp.cnf\n# req -new: generate a certificate signing request (CSR)\n# -subj: the certificate'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 \"Subject Alternative Name\"\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 \"\/CN=etcd-node2\" -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 \"Subject Alternative Name\"\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 \"\/CN=etcd-node3\" -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 \"Subject Alternative Name\"\n# Expected: IP Address:192.168.0.205, IP Address:127.0.0.1\nrm temp.cnf<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur postgres-01\nopenssl genrsa -out server.key 2048\n\nopenssl req -new -key server.key -out server.req\n# Il vous sera demand\u00e9 les d\u00e9tails du certificat \u2014 le nom commun n'est pas critique\n# car les connexions sont v\u00e9rifi\u00e9es par IP SAN, pas par CN\n# L'avertissement \"No -copy_extensions given\" est inoffensif \u2014 le certificat du serveur n'a pas besoin de SAN\nopenssl req -x509 -key server.key -in server.req -out server.crt -days 7300\n# Certificat serveur auto-sign\u00e9 \u2014 sign\u00e9 directement avec server.key, pas avec l'AC\n# Patroni et PostgreSQL utilisent ce certificat pour s'identifier aupr\u00e8s des clients<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur 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\/<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur postgres-01, postgres-02, postgres-03\nsudo mkdir -p \/etc\/etcd\/certs\nsudo apt-get install -y acl\n# acl : fournit setfacl \u2014 n\u00e9cessaire pour accorder l'acc\u00e8s \u00e0 l'utilisateur postgres sans modifier la propri\u00e9t\u00e9<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code>sudo 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\/<\/code><\/pre>\n\n\n\n<p><strong>Sur postgres-02<\/strong> \u2014 d\u00e9placer depuis \/tmp :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo 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\/<\/code><\/pre>\n\n\n\n<p><strong>Sur postgres-03<\/strong> \u2014 d\u00e9placer depuis \/tmp :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo 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\/<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># D\u00e9finir d'abord les permissions des fichiers - doit se faire avant chmod 700 sur le r\u00e9pertoire\n# Apr\u00e8s chmod 700, le shell ne peut pas d\u00e9velopper les globs en tant qu'utilisateur non-root\n\n# etcd certs : owned by etcd\n# Sur 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# Sur 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# Sur 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 - 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# Verrouillez le r\u00e9pertoire - ex\u00e9cutez cette op\u00e9ration en dernier, apr\u00e8s avoir d\u00e9fini toutes les autorisations de fichiers\nsudo chown etcd:etcd \/etc\/etcd\/certs\nsudo chmod 700 \/etc\/etcd\/certs\n\n# Accorder \u00e0 l'utilisateur postgres l'acc\u00e8s en lecture au r\u00e9pertoire et \u00e0 tous les fichiers qu'il contient\n# Patroni doit lire les certificats etcd pour se connecter avec TLS\nsudo setfacl -R -m u:postgres:rX \/etc\/etcd\/certs\n# -R : appliquer r\u00e9cursivement \u00e0 tous les fichiers ; rX : lire + ex\u00e9cuter sur les r\u00e9pertoires (\u00e0 traverser)<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur postgres-01, postgres-02, postgres-03\nsudo sh -c 'cat \/etc\/etcd\/certs\/server.crt \/etc\/etcd\/certs\/server.key \\N- &gt; \/etc\/etcd\/certs\/server.pem'\".\n  &gt; \/etc\/etcd\/certs\/server.pem\"\n# Concat\u00e9nation du certificat et de la cl\u00e9 en un seul fichier\nsudo chown postgres:postgres \/etc\/etcd\/certs\/server.pem\nsudo chmod 600 \/etc\/etcd\/certs\/server.pem\n# server.pem contient la cl\u00e9 priv\u00e9e - PostgreSQL n\u00e9cessite 0600 et la propri\u00e9t\u00e9 de postgres\n\n# V\u00e9rifier que le fichier PEM est valide\nsudo openssl x509 -in \/etc\/etcd\/certs\/server.pem -text -noout\n# Attendu : d\u00e9tails du certificat, y compris les dates de validit\u00e9 et le sujet\n# Si \"unable to load certificate\" : le fichier PEM est malform\u00e9 - recr\u00e9ez-le.\n\n# V\u00e9rifier les permissions finales sur tous les fichiers cert.\nsudo ls -la \/etc\/etcd\/certs\/\n# R\u00e9sultats attendus (postgres-01 affich\u00e9 - le num\u00e9ro de n\u0153ud diff\u00e8re sur 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 doit \u00eatre 0600, propri\u00e9t\u00e9 de postgres\n# -rw-------+ 1 postgres postgres server.pem \u2190 doit \u00eatre 0600, propri\u00e9t\u00e9 de postgres\n# PostgreSQL refusera de d\u00e9marrer si server.key ou server.pem est lisible par le groupe ou le monde.<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code>Sur postgres-01, postgres-02, postgres-03\nsudo mkdir -p \/var\/lib\/etcd\nsudo chown -R etcd:etcd \/var\/lib\/etcd\n# etcd y stocke ses donn\u00e9es WAL et snapshot \u2014 doit \u00eatre la propri\u00e9t\u00e9 de l'utilisateur etcd<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># \/etc\/etcd\/etcd.env - postgres-01 (192.168.0.203)\n\nETCD_NAME=\"postgresql-01\"\n# ETCD_NAME : identifiant unique de ce membre au sein du cluster\n\nETCD_DATA_DIR=\"\/var\/lib\/etcd\"\n# ETCD_DATA_DIR : endroit o\u00f9 etcd stocke ses WAL et ses instantan\u00e9s.\n\nETCD_INITIAL_CLUSTER=\"postgresql-01=https:\/\/192.168.0.203:2380,postgresql-02=https:\/\/192.168.0.204:2380,postgresql-03=https:\/\/192.168.0.205:2380\"\n# ETCD_INITIAL_CLUSTER : tous les membres au moment du d\u00e9marrage - doit \u00eatre identique sur les trois n\u0153uds\n\nETCD_INITIAL_CLUSTER_STATE=\"new\"\n# new : il s'agit d'un nouveau d\u00e9marrage de cluster\n# Important : passer \u00e0 \"existing\" apr\u00e8s le d\u00e9marrage de la grappe (voir section 9, \u00e9tape 3).\n\nETCD_INITIAL_CLUSTER_TOKEN=\"etcd-cluster\"\n# ETCD_INITIAL_CLUSTER_TOKEN : emp\u00eache les n\u0153uds de rejoindre accidentellement le mauvais cluster\n\nETCD_INITIAL_ADVERTISE_PEER_URLS=\"https:\/\/192.168.0.203:2380\"\n# ETCD_INITIAL_ADVERTISE_PEER_URLS : adresse que ce n\u0153ud annonce aux autres membres du syst\u00e8me etcd pour le trafic entre pairs.\n\nETCD_LISTEN_PEER_URLS=\"https:\/\/0.0.0.0:2380\"\n# ETCD_LISTEN_PEER_URLS : adresse sur laquelle etcd \u00e9coute les connexions de pairs provenant d'autres membres etcd\n\nETCD_LISTEN_CLIENT_URLS=\"https:\/\/0.0.0.0:2379\"\n# ETCD_LISTEN_CLIENT_URLS : adresse sur laquelle etcd \u00e9coute les connexions des clients (Patroni se connecte ici)\n\nETCD_ADVERTISE_CLIENT_URLS=\"https:\/\/192.168.0.203:2379\"\n# ETCD_ADVERTISE_CLIENT_URLS : adresse que ce noeud annonce aux clients - doit \u00eatre accessible depuis Patroni\n\n# TLS pour les connexions clients (Patroni \u2192 etcd)\nETCD_CLIENT_CERT_AUTH=\"true\"\n# ETCD_CLIENT_CERT_AUTH : demande aux clients de pr\u00e9senter un certificat valide (TLS mutuel)\nETCD_TRUSTED_CA_FILE=\"\/etc\/etcd\/certs\/ca.crt\"\n# ETCD_TRUSTED_CA_FILE : Certificat de l'autorit\u00e9 de certification utilis\u00e9 pour v\u00e9rifier les certificats des clients\nETCD_CERT_FILE=\"\/etc\/etcd\/certs\/etcd-node1.crt\"\n# ETCD_CERT_FILE : certificat pr\u00e9sent\u00e9 aux clients se connectant \u00e0 ce n\u0153ud\nETCD_KEY_FILE=\"\/etc\/etcd\/certs\/etcd-node1.key\"\n# ETCD_KEY_FILE : cl\u00e9 priv\u00e9e pour le certificat ci-dessus\n\n# TLS pour les connexions entre pairs (n\u0153ud etcd \u2194 n\u0153ud etcd)\nETCD_PEER_CLIENT_CERT_AUTH=\"true\"\n# ETCD_PEER_CLIENT_CERT_AUTH : demande aux n\u0153uds homologues de pr\u00e9senter un certificat valide\nETCD_PEER_TRUSTED_CA_FILE=\"\/etc\/etcd\/certs\/ca.crt\"\nETCD_PEER_CERT_FILE=\"\/etc\/etcd\/certs\/etcd-node1.crt\"\nETCD_PEER_KEY_FILE=\"\/etc\/etcd\/certs\/etcd-node1.key\"<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur postgres-01, postgres-02, postgres-03\n# Cr\u00e9ez \/etc\/systemd\/system\/etcd.service avec le contenu suivant :<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>[Unit]\nDescription=Magasin cl\u00e9-valeur etcd\nDocumentation=https:\/\/github.com\/etcd-io\/etcd\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=notify\n# Type=notify : systemd attend qu'etcd envoie un signal de pr\u00e9paration avant de le marquer comme d\u00e9marr\u00e9\nWorkingDirectory=\/var\/lib\/etcd\nEnvironmentFile=\/etc\/etcd\/etcd.env\n# EnvironmentFile : charge toutes les variables ETCD_* du fichier cr\u00e9\u00e9 \u00e0 l'\u00e9tape 2\nExecStart=\/usr\/local\/bin\/etcd\nRestart=always\n# Restart=always : systemd red\u00e9marre etcd s'il se termine pour une raison quelconque\nRestartSec=10s\nLimitNOFILE=40000\n# LimitNOFILE : augmente la limite des descripteurs de fichiers ouverts \u2014 etcd ouvre de nombreux fichiers en cas de forte charge\nUser=etcd\nGroup=etcd\n\n[Install]\nWantedBy=multi-user.target<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur postgres-01, postgres-02, postgres-03\nsudo systemctl daemon-reload\n# daemon-reload : requis apr\u00e8s la cr\u00e9ation ou la modification d'un fichier d'unit\u00e9 systemd\n\nsudo systemctl enable etcd\n# enable : d\u00e9marre etcd automatiquement au d\u00e9marrage (boot)\n\nsudo systemctl start etcd\n# start : d\u00e9marre le service etcd maintenant\n\nsudo systemctl status etcd\n# Attendu : Active : active (running)\n# Si \"Active : failed\" : v\u00e9rifiez journalctl -xeu etcd.service pour plus de d\u00e9tails\n# Causes courantes :\n#   - certificat non trouv\u00e9 : v\u00e9rifiez que les chemins dans etcd.env correspondent aux fichiers dans \/etc\/etcd\/certs\/\n#   - permission refus\u00e9e sur la cl\u00e9 : ex\u00e9cutez \"sudo chown etcd:etcd \/etc\/etcd\/certs\/*.key\"\n#   - port utilis\u00e9 : ex\u00e9cutez \"ss -tlnp | grep 237\" pour trouver ce qui utilise les ports 2379\/2380<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur postgres-01\netcdctl endpoint health\n# Attendu :\n# 127.0.0.1:2379 est sain : proposition valid\u00e9e avec succ\u00e8s : dur\u00e9e = 2.3ms\n# Ceci ne v\u00e9rifie que le noeud local - la v\u00e9rification compl\u00e8te du cluster est ci-dessous\n\n# V\u00e9rification compl\u00e8te de l'\u00e9tat de sant\u00e9 de la grappe avec les informations d'identification TLS\nsudo etcdctl \\N- --endpoints=\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\/ca.crt\n  --cert=\/etc\/etcd\/certs\/etcd-node1.crt \\N-key=\/etc\/etcd\/certs\/ca.crt\n  --key=\/etc\/etcd\/certs\/etcd-node1.key \\N--cert=\/etc\/etcd\/certs\/etcd-node1.crt\n  sant\u00e9 du point d'extr\u00e9mit\u00e9\n# --cacert : Certificat CA pour v\u00e9rifier les certificats du serveur\n# --cert \/ --key : certificat et cl\u00e9 du client pour TLS mutuel\n# Attendu :\n# https:\/\/192.168.0.203:2379 est sain : proposition engag\u00e9e avec succ\u00e8s : dur\u00e9e = 2.3ms\n# https:\/\/192.168.0.204:2379 est sain : proposition engag\u00e9e avec succ\u00e8s : dur\u00e9e = 2.1ms\n# https:\/\/192.168.0.205:2379 est en bonne sant\u00e9 : proposition valid\u00e9e avec succ\u00e8s : d\u00e9lai = 2.4ms\n# Si un n\u0153ud est en mauvaise sant\u00e9 : v\u00e9rifier journalctl -u etcd sur ce n\u0153ud\n# Si tous les n\u0153uds sont en mauvaise sant\u00e9 : v\u00e9rifier le pare-feu sur le port 2380 entre les n\u0153uds\n\n# V\u00e9rifier l'\u00e9lection du leader - un n\u0153ud doit \u00eatre le leader\nsudo etcdctl \\N\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\/ca.crt\n  --cert=\/etc\/etcd\/certs\/etcd-node1.crt \\N-key=\/etc\/etcd\/certs\/ca.crt\n  --key=\/etc\/etcd\/certs\/etcd-node1.key \\N--CE\n  endpoint status --write-out=table\n# Attendu : une table avec un n\u0153ud montrant IS LEADER = true\n# Si pas de leader : le quorum n'est pas \u00e9tabli - v\u00e9rifier que les trois n\u0153uds fonctionnent.<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur postgres-01, postgres-02, postgres-03\nsudo apt install -y patroni\n# Sur Ubuntu 22.04+, le paquet apt inclut la biblioth\u00e8que cliente etcd v3\n# Si le paquet de votre distribution ne l'inclut pas : pip install patroni[etcd3]\n# [etcd3] s\u00e9lectionne le backend de l'API etcd v3 \u2014 requis pour etcd 3.5+\n# L'ancien drapeau [etcd] utilise l'API HTTP d\u00e9pr\u00e9ci\u00e9e v2\n\nsudo mkdir -p \/etc\/patroni\/\n# Patroni lit sa configuration \u00e0 partir d'un fichier YAML dans ce r\u00e9pertoire<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># \/etc\/patroni\/config.yml - postgres-01 (192.168.0.203)\n\nscope : postgresql-cluster\n# scope : cluster name - doit \u00eatre identique sur tous les n\u0153uds Patroni\n# Utilis\u00e9 comme pr\u00e9fixe dans etcd pour s\u00e9parer plusieurs clusters Patroni\n\nespace de noms : \/service\/\n# namespace : etcd key prefix - tout l'\u00e9tat du cluster est stock\u00e9 sous \/service\/postgresql-cluster\/\n\nnom : postgresql-01\n# name : nom unique pour ce n\u0153ud au sein du cluster\n\netcd3 :\n  hosts : 192.168.0.203:2379,192.168.0.204:2379,192.168.0.205:2379\n  # etcd3 : utiliser l'API etcd v3 (gRPC) - requis pour etcd 3.5+\n  # L'ancienne cl\u00e9 \"etcd :\" utilise l'API HTTP v2 qui est obsol\u00e8te\n  protocole : https\n  Protocole # : https - Patroni se connecte \u00e0 etcd via TLS\n  cacert : \/etc\/etcd\/certs\/ca.crt\n  # cacert : Certificat CA pour v\u00e9rifier les certificats du serveur etcd\n  cert : \/etc\/etcd\/certs\/etcd-node1.crt\n  # cert : certificat client pr\u00e9sent\u00e9 \u00e0 etcd pour TLS mutuel\n  mutuel : \/etc\/etcd\/certs\/etcd-node1.key\n  # key : cl\u00e9 priv\u00e9e du certificat client\n\nrestapi :\n  listen : 0.0.0.0:8008\n  # listen : L'API REST de Patroni \u00e9coute sur toutes les interfaces sur le port 8008\n  # HAProxy interroge cette API pour d\u00e9terminer quel n\u0153ud est le n\u0153ud primaire actuel\n  connect_address : 192.168.0.203:8008\n  # connect_address : l'adresse que les autres n\u0153uds utilisent pour atteindre l'API REST de ce n\u0153ud - doit \u00eatre l'IP r\u00e9elle\n  certfile : \/etc\/etcd\/certs\/server.pem\n  # certfile : fichier PEM combin\u00e9 cert+key - active TLS sur l'API REST\n  # HAProxy utilise check-ssl lors de l'interrogation de \/primary - l'API doit servir un certificat\n\nbootstrap :\n  dcs :\n    ttl : 30\n    # ttl : dur\u00e9e du bail du leader en secondes\n    # Si le primaire n'est pas renouvel\u00e9 dans les secondes ttl, il est consid\u00e9r\u00e9 comme d\u00e9faillant.\n    # et un standby est promu. Plus bas = basculement plus rapide ; plus haut = plus de tol\u00e9rance pour les r\u00e9seaux lents.\n    loop_wait : 10\n    # loop_wait : fr\u00e9quence \u00e0 laquelle Patroni v\u00e9rifie la sant\u00e9 du cluster (secondes)\n    retry_timeout : 10\n    # retry_timeout : combien de temps Patroni retente une op\u00e9ration etcd ou PostgreSQL qui a \u00e9chou\u00e9 avant d'abandonner.\n    maximum_lag_on_failover : 1048576\n    # maximum_lag_on_failover : les serveurs en attente ayant plus de 1 Mo de retard sur le serveur primaire ne seront pas promus.\n    # Emp\u00eache la promotion d'un standby tr\u00e8s d\u00e9fectueux qui causerait une perte de donn\u00e9es importante.\n    postgresql :\n      parameters :\n        ssl : 'on'\n        # ssl : active TLS sur les connexions PostgreSQL\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 \u00e9crit ce pg_hba.conf au bootstrap\n        # hostssl : TLS est requis - les connexions simples sont rejet\u00e9es\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        # Connexions de r\u00e9plication depuis les trois n\u0153uds PostgreSQL\n        - hostssl all all 127.0.0.1\/32 md5\n        - hostssl all all 0.0.0.0\/0 md5\n        # Connexions d'application - TLS requis, authentification par mot de passe\n\n  initdb :\n    - encodage : UTF8\n    - donn\u00e9es de contr\u00f4le (data-checksums)\n    # data-checksums : active les sommes de contr\u00f4le au niveau de la page - requis pour pg_rewind\n    # d\u00e9tecte les blocs corrompus ; l\u00e9ger surco\u00fbt d'\u00e9criture (typiquement inf\u00e9rieur \u00e0 2%)\n\npostgresql :\n  listen : 0.0.0.0:5432\n  connect_address : 192.168.0.203:5432\n  # connect_address : l'adresse IP r\u00e9elle de ce n\u0153ud - utilis\u00e9e par les serveurs pour se connecter \u00e0 la r\u00e9plication\n  data_dir : \/var\/lib\/postgresql\/data\n  # data_dir : R\u00e9pertoire de donn\u00e9es PostgreSQL - Patroni g\u00e8re enti\u00e8rement ce r\u00e9pertoire\n  bin_dir : \/usr\/lib\/postgresql\/18\/bin\n  # bin_dir : r\u00e9pertoire contenant pg_ctl, pg_basebackup, pg_rewind, initdb\n  # Ajustez le num\u00e9ro de version pour qu'il corresponde \u00e0 votre installation PostgreSQL\n  authentification :\n    superuser :\n      nom d'utilisateur : postgres\n      mot de passe : strongpassword\n      # Patroni utilise ces identifiants pour les connexions de gestion interne\n      # Modifier avant l'utilisation en production\n    r\u00e9plication :\n      nom d'utilisateur : replicator\n      mot de passe : replpassword\n      # Patroni cr\u00e9e ce r\u00f4le automatiquement pendant le bootstrap\n      # Modifier avant la mise en production\n  param\u00e8tres :\n    max_connections : 100\n    shared_buffers : 256MB\n    # tampons_partag\u00e9s : Cache m\u00e9moire principal de PostgreSQL - typiquement 25% de RAM totale\n    # 256MB est une valeur conservatrice par d\u00e9faut ; augmenter en fonction de la m\u00e9moire disponible\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 : valeur vraie pour exclure ce n\u0153ud de l'acheminement des r\u00e9pliques en lecture\n  clonefrom : false\n  nosync : false\n\nctl :\n  insecure : true\n  # insecure : sauter la v\u00e9rification du certificat TLS lorsque patronictl appelle l'API REST de Patroni\n  # Requis pour les commandes de basculement et de basculement - sans lui, patronictl \u00e9choue avec une erreur SSL\n  # Les commandes en lecture seule (patronictl list) fonctionnent sans cela car elles utilisent etcd, et non l'API REST.\n  # N'ajoutez PAS cacert ou certfile ici - un certfile serveur provoque un mauvais handshake TLS<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur postgres-01\nsudo systemctl enable patroni\n# activer : d\u00e9marrer Patroni automatiquement au d\u00e9marrage\nsudo systemctl restart patroni\n# Patroni initialise le r\u00e9pertoire de donn\u00e9es PostgreSQL (initdb), d\u00e9marre PostgreSQL,\n# acquiert le bail de leader dans etcd, et se configure en tant que primaire\n\njournalctl -u patroni -f\n# -f : suivre \u2014 afficher les nouvelles lignes de log au fur et \u00e0 mesure de leur apparition\n# Rechercher : \" promu soi-m\u00eame au r\u00f4le de leader \" \u2014 confirme que postgres-01 est le primaire\n# Appuyer sur Ctrl+C pour arr\u00eater le suivi une fois la confirmation obtenue\n# Si \" impossible de se connecter \u00e0 etcd \" : v\u00e9rifier que etcd est en cours d'ex\u00e9cution et que les certificats TLS sont corrects<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur postgres-01\n# patronictl lit ca.crt depuis \/etc\/etcd\/certs\/ \u2014 ce r\u00e9pertoire est en mode 700.\n# Ex\u00e9cutez avec sudo, sinon cela \u00e9chouera avec une erreur de chargement de certificat SSL.\nsudo patronictl -c \/etc\/patroni\/config.yml list\n# Attendu :\n# + Cluster : postgresql-cluster +----+-----------+\n# | Membre | H\u00f4te | R\u00f4le | \u00c9tat | TL | Lag in MB |\n# +---------------+-------------------+--------+---------+----+-----------+\n# | postgresql-01 | 192.168.0.203 | Leader | running | 1 | |\n# +---------------+-------------------+--------+---------+----+-----------+\n# Si l'\u00e9tat est \"start failed\" : v\u00e9rifiez journalctl -u patroni pour l'erreur PostgreSQL\n# Si aucun Leader apr\u00e8s 30 secondes : v\u00e9rification de l'\u00e9tat d'etcd (section 7 \u00e9tape 5)<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur postgres-02 et postgres-03\nsudo systemctl enable patroni &amp;&amp; sudo systemctl restart patroni\n# Patroni d\u00e9tecte le primaire existant dans etcd, ex\u00e9cute pg_basebackup \u00e0 partir de postgres-01,\n# et d\u00e9marre PostgreSQL en tant que standby en streaming\n\n# V\u00e9rifier les trois noeuds\nsudo patronictl -c \/etc\/patroni\/config.yml list\n# Attendu :\n# + Cluster : postgresql-cluster -----+----+-----------+\n# | Membre | H\u00f4te | R\u00f4le | Etat | TL | Lag en MB |\n# +---------------+----------------+---------+---------+----+-----------+\n# | postgresql-01 | 192.168.0.203 | Leader | running | 1 | | |\n# | postgresql-02 | 192.168.0.204 | R\u00e9plique | en cours d'ex\u00e9cution | 1 | 0 |\n# | postgresql-03 | 192.168.0.205 | R\u00e9plique | en cours d'ex\u00e9cution | 1 | 0 |\n# +---------------+----------------+---------+---------+----+-----------+\n# Lag in MB = 0 : standbys are fully caught up with the primary\n# Si un n\u0153ud indique \"stopped\" : v\u00e9rifier journalctl -u patroni sur ce n\u0153ud\n# Si pg_basebackup a \u00e9chou\u00e9 : v\u00e9rifier que le port 5432 est ouvert entre les n\u0153uds<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur postgres-01, postgres-02, postgres-03\n# Modifiez \/etc\/etcd\/etcd.env et changez :\n#   ETCD_INITIAL_CLUSTER_STATE=\"new\"\n# en :\n#   ETCD_INITIAL_CLUSTER_STATE=\"existing\"\n\nsudo systemctl restart etcd\n# Red\u00e9marrez pour appliquer le changement\n# Attendu : etcd rejoint le cluster existant proprement\n# V\u00e9rifiez : ex\u00e9cutez la commande de sant\u00e9 des points d'extr\u00e9mit\u00e9 de la section 7, \u00e9tape 5<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur haproxy-01, haproxy-02, haproxy-03\nsudo apt install -y haproxy\n\nhaproxy -v\n# Attendu : HAProxy version 2.x.x\n# Si une version plus ancienne : ajoutez le d\u00e9p\u00f4t apt HAProxy pour la derni\u00e8re version<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># \/etc\/haproxy\/haproxy.cfg\n\nfrontend postgres_frontend\n    bind *:5432\n    mode tcp\n    # mode tcp : HAProxy transmet du TCP brut - PostgreSQL n'est pas un protocole HTTP\n    timeout client 30s\n    Le client timeout # n'a sa place que dans le frontend - HAProxy l'avertira et l'ignorera dans un 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 interroge ce point d'extr\u00e9mit\u00e9 sur l'API REST Patroni de chaque n\u0153ud.\n    # Seul le n\u0153ud principal actuel renvoie HTTP 200 sur \/primary ; les n\u0153uds secondaires renvoient 503\n    # C'est ainsi que HAProxy sait vers quel n\u0153ud acheminer le trafic d'\u00e9criture\n    http-check expect status 200\n    timeout connect 5s\n    # timeout connect et timeout server appartiennent au backend uniquement\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 v\u00e9rifie le port de l'API REST de Patroni, pas celui de PostgreSQL.\n    # check-ssl : utiliser TLS lors de l'interrogation de l'API REST (l'API REST de Patroni a TLS activ\u00e9)\n    # verify none : ne pas v\u00e9rifier le certificat CN - acceptable dans un cluster priv\u00e9\n    # o\u00f9 les n\u0153uds sont identifi\u00e9s par IP<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur haproxy-01, haproxy-02, haproxy-03\nsudo haproxy -c -f \/etc\/haproxy\/haproxy.cfg\n# -c : v\u00e9rifier uniquement la configuration, ne pas d\u00e9marrer\n# -f : chemin vers le fichier de configuration\n# Attendu : Le fichier de configuration est valide\n# Si vous voyez des avertissements \u00ab timeout client\/server in the wrong section \u00bb,\n# v\u00e9rifiez que \u00ab timeout client \u00bb est dans le frontend et \u00ab timeout connect\/server \u00bb sont dans le backend<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code># Sur haproxy-01, haproxy-02, haproxy-03\nsudo systemctl reload haproxy\n# reload\u00a0: applique la nouvelle configuration sans interrompre les connexions existantes\n# Attendu\u00a0: aucune sortie sur le terminal\u00a0; confirmer le succ\u00e8s dans les journaux ci-dessous<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo journalctl -u haproxy --since \"1 minute ago\"\n# Lignes attendues (les horodatages diff\u00e9reront) :\n#   systemd[1]: Rechargement de haproxy.service - HAProxy Load Balancer...\n#   systemd[1]: Recharg\u00e9 haproxy.service - HAProxy Load Balancer.\n# Si vous voyez \"\u00c9chec\" : v\u00e9rifiez d'abord la sortie de sudo haproxy -c<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo tail -f \/var\/log\/syslog | grep haproxy\n# Surveiller les logs HAProxy pour confirmer qu'il v\u00e9rifie les n\u0153uds PostgreSQL\n# Attendu : lignes r\u00e9p\u00e9t\u00e9es de contr\u00f4le de sant\u00e9 ; un n\u0153ud doit afficher \"UP\" (le primaire)\n# Si tous les n\u0153uds affichent \"DOWN\" : HAProxy ne peut pas atteindre le port 8008 \u2014 v\u00e9rifier le pare-feu<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur haproxy-01, haproxy-02, haproxy-03\nsudo apt update &amp;&amp; sudo apt install -y keepalived<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur haproxy-01, haproxy-02, haproxy-03\n# Cr\u00e9ez le fichier \/etc\/keepalived\/check_haproxy.sh avec le contenu suivant :<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/bin\/bash\n\nPORT=5432\n\nif ! pidof haproxy &gt; \/dev\/null; then\n    # pidof haproxy : v\u00e9rifie si le processus HAProxy est en cours d'ex\u00e9cution\n    echo \"HAProxy n'est pas en cours d'ex\u00e9cution\"\n    exit 1\n    # exit 1 : un code non nul indique \u00e0 keepalived que ce n\u0153ud n'est pas sain \u2014 la VIP est d\u00e9plac\u00e9e vers un n\u0153ud BACKUP\nfi\n\nif ! ss -ltn | grep -q \":${PORT}\"; then\n    # ss -ltn : liste les sockets TCP en \u00e9coute ; grep v\u00e9rifie si le port 5432 est li\u00e9\n    echo \"HAProxy n'\u00e9coute pas sur le port ${PORT}\"\n    exit 2\nfi\n\nexit 0\n# exit 0 : indique \u00e0 keepalived que ce n\u0153ud est sain et doit conserver (ou recevoir) la VIP<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code># Sur haproxy-01, haproxy-02, haproxy-03\nsudo useradd -r -s \/bin\/false keepalived_script\n# -r : compte syst\u00e8me ; -s \/bin\/false : pas de connexion interactive\n# keepalived ex\u00e9cute les scripts de v\u00e9rification de l'\u00e9tat en tant que cet utilisateur lorsque enable_script_security est activ\u00e9\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 : propri\u00e9taire ex\u00e9cutable uniquement \u2014 keepalived exige que le script ne soit pas modifiable par d'autres utilisateurs\n# lorsque enable_script_security est activ\u00e9 (configur\u00e9 ci-dessous dans keepalived.conf)<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># \/etc\/keepalived\/keepalived.conf - haproxy-01 (MASTER)\n\nglobal_defs {\n    enable_script_security\n    # enable_script_security : emp\u00eache keepalived d'ex\u00e9cuter des scripts appartenant \u00e0 root\n    # ou accessibles en \u00e9criture par n'importe qui - emp\u00eache l'escalade des privil\u00e8ges par le biais des scripts de contr\u00f4le de sant\u00e9\n    script_user keepalived_script\n    # script_user : l'utilisateur du syst\u00e8me d'exploitation que keepalived utilise pour ex\u00e9cuter les scripts de contr\u00f4le de sant\u00e9\n}\n\nvrrp_script check_haproxy {\n    script \"\/etc\/keepalived\/check_haproxy.sh\"\n    intervalle 2\n    # interval : ex\u00e9cuter le contr\u00f4le de sant\u00e9 toutes les 2 secondes\n    fall 3\n    # fall : marque ce noeud comme d\u00e9faillant apr\u00e8s 3 v\u00e9rifications cons\u00e9cutives (6 secondes au total)\n    rise 2\n    # rise : marque ce n\u0153ud comme r\u00e9tabli apr\u00e8s 2 v\u00e9rifications cons\u00e9cutives (4 secondes)\n}\n\nvrrp_instance VI_1 {\n    state MASTER\n    # MASTER : ce n\u0153ud d\u00e9tient le VIP initialement au d\u00e9marrage\n    interface enp0s3\n    # interface : l'interface r\u00e9seau \u00e0 laquelle le VIP est attribu\u00e9\n    # Ex\u00e9cutez \"ip link show\" pour trouver le nom de votre interface - il peut s'agir de ens3, enp0s3, eth0, etc.\n    # Dans ce laboratoire, l'interface est enp0s3 (valeur par d\u00e9faut de VirtualBox).\n    virtual_router_id 51\n    # virtual_router_id : identifie ce groupe VRRP - doit \u00eatre identique sur les trois n\u0153uds\n    priority 100\n    # priority : le n\u0153ud ayant la priorit\u00e9 la plus \u00e9lev\u00e9e remporte l'\u00e9lection VIP\n    # haproxy-01 gagne par d\u00e9faut (100 &gt; 90 &gt; 80)\n    advert_int 1\n    # advert_int : envoi d'annonces VRRP toutes les 1 seconde\n    authentification {\n        auth_type PASS\n        auth_pass changeme123\n        # auth_pass : mot de passe partag\u00e9 entre tous les n\u0153uds keepalived - changer avant l'utilisation en production\n    }\n    virtual_ipaddress {\n        192.168.0.210\n        # Le VIP - les applications se connectent \u00e0 cette adresse sur le port 5432\n    }\n    track_script {\n        check_haproxy\n        # track_script : lier cette instance VRRP au contr\u00f4le de sant\u00e9 de HAProxy\n        # Si le script sort avec une valeur non nulle, la priorit\u00e9 effective de ce n\u0153ud tombe\n        # en dessous des BACKUPs et des mouvements VIP\n    }\n}<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code># \/etc\/keepalived\/keepalived.conf \u2014 haproxy-02 (BACKUP, seconde priorit\u00e9)\n# Identique \u00e0 haproxy-01 sauf :\n#   state BACKUP\n#   priority 90<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code># \/etc\/keepalived\/keepalived.conf \u2014 haproxy-03 (BACKUP, priorit\u00e9 la plus basse)\n# Identique \u00e0 haproxy-01 sauf :\n#   state BACKUP\n#   priority 80<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"step-4-start-keepalived\">\u00c9tape 4 \u2014 D\u00e9marrer keepalived<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code># Sur haproxy-01, haproxy-02, haproxy-03\nsudo systemctl enable --now keepalived<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo journalctl -u keepalived -f\n# Observer les journaux de keepalived\u00a0\u2014\u00a0on s'attend \u00e0 voir des transitions d'\u00e9tat VRRP\n# haproxy-01 devrait afficher\u00a0: \"(VI_1) Entering BACKUP STATE\" puis \"(VI_1) Entering MASTER STATE\"\n# (entre bri\u00e8vement en mode BACKUP au d\u00e9marrage, remporte l'\u00e9lection apr\u00e8s environ 4 secondes en raison de la priorit\u00e9 100)\n# haproxy-02 et haproxy-03 devraient afficher\u00a0: \"(VI_1) Entering BACKUP STATE\"\n# Appuyez sur Ctrl+C pour arr\u00eater de suivre\n#\n# REMARQUE : \"Truncating auth_pass to 8 characters\" est un avertissement, pas une erreur.\n# keepalived n'utilise que les 8 premiers caract\u00e8res de auth_pass, quelle que soit la valeur que vous d\u00e9finissez.\n# C'est bon tant que tous les n\u0153uds utilisent la m\u00eame cha\u00eene de mots de passe\u00a0\u2014 ils seront tous tronqu\u00e9s de mani\u00e8re identique.<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code># V\u00e9rifier que la VIP est active sur haproxy-01\nping -c 3 192.168.0.210\n# Attendu : 3 paquets transmis, 3 re\u00e7us\n# Si 0 re\u00e7u : La VIP n'est attribu\u00e9e \u00e0 aucun n\u0153ud \u2014 v\u00e9rifier les journaux de keepalived sur les trois n\u0153uds HAProxy<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur postgres-01 \u2014 v\u00e9rifiez pg_hba.conf de Patroni\nsudo cat \/var\/lib\/postgresql\/data\/pg_hba.conf\n# Attendu : \" Ne modifiez pas ce fichier manuellement ! Il sera \u00e9cras\u00e9 par Patroni ! \"\n# suivi d'entr\u00e9es hostssl uniquement \u2014 aucune entr\u00e9e de socket local<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur postgres-01\necho \"local all postgres peer\" | sudo tee -a \/var\/lib\/postgresql\/data\/pg_hba.conf<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur postgres-01\nsudo -u postgres \/usr\/lib\/postgresql\/18\/bin\/pg_ctl reload -D \/var\/lib\/postgresql\/data\n# Attendu : serveur signal\u00e9<\/code><\/pre>\n\n\n\n<p>D\u00e9finir le mot de passe :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Sur postgres-01\nsudo -u postgres psql -c \"ALTER USER postgres PASSWORD 'motdepassefort';\"\n# Attendu\u00a0: ALTER ROLE\n# IMPORTANT\u00a0: ce mot de passe doit correspondre \u00e0 postgresql.authentication.superuser.password dans config.yml\n# S'ils diff\u00e8rent, Patroni ne pourra pas se connecter \u00e0 son instance PostgreSQL locale et signalera\n# LSN inconnu \u2014 le n\u0153ud semblera bloqu\u00e9 apr\u00e8s un basculement ou un red\u00e9marrage\n# Le mot de passe est maintenant stock\u00e9 dans la base de donn\u00e9es \u2014 il survivra \u00e0 l'\u00e9crasement de pg_hba.conf par Patroni<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Check 1 : etcd cluster is healthy (run on postgres-01)\nsudo etcdctl \\N\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\/etcd\/etcd\/etcd\/ca.crt\n  --cert=\/etc\/etcd\/certs\/etcd-node1.crt \\N-key=\/etc\/etcd\/certs\/ca.crt\n  --key=\/etc\/etcd\/certs\/etcd-node1.key \\N--cert=\/etc\/etcd\/certs\/etcd-node1.crt\n  sant\u00e9 du point d'extr\u00e9mit\u00e9\n# Attendu : les trois n\u0153uds signalent \"est sain\".\n\n# V\u00e9rification 2 : Le cluster Patroni a un leader et deux r\u00e9pliques (ex\u00e9cut\u00e9 sur n'importe quel noeud PostgreSQL)\nsudo patronictl -c \/etc\/patroni\/config.yml list\n# Attendu : un leader, deux r\u00e9pliques, tous State = running, Lag = 0\n\n# V\u00e9rification 3 : VIP r\u00e9pond\nping -c 3 192.168.0.210\n# Attendu : 3 paquets re\u00e7us\n# Si 0 re\u00e7u : VIP non assign\u00e9 - v\u00e9rifier keepalived sur tous les n\u0153uds HAProxy\n\n# V\u00e9rification 4 : PostgreSQL est accessible via le VIP\npsql -h 192.168.0.210 -U postgres -c \"SELECT inet_server_addr(), pg_is_in_recovery() ;\"\n# Attendu : inet_server_addr = 192.168.0.203 (le primaire), pg_is_in_recovery = f\n# pg_is_in_recovery = f : confirme qu'il s'agit bien de l'ordinateur primaire (la m\u00e9thode standard renvoie t)\n# Si la connexion est refus\u00e9e : HAProxy n'est pas en train de router - v\u00e9rifier l'\u00e9tat de haproxy et le point de terminaison \/primary\n\n# V\u00e9rification 5 : la r\u00e9plication est en cours\npsql -h 192.168.0.210 -U postgres -c \"SELECT client_addr, replay_lag FROM pg_stat_replication ;\"\n# Attendu : deux lignes (une par standby), replay_lag = 00:00:00 ou NULL (rattrap\u00e9)\n# Si pas de lignes : les standbys ne sont pas en streaming - v\u00e9rifier les logs Patroni sur les noeuds standby\n\n# V\u00e9rification 6 : ins\u00e9rer des donn\u00e9es sur le n\u0153ud primaire, les lire sur les deux n\u0153uds en attente\npsql -h 192.168.0.210 -U postgres -c \"CREATE TABLE test (id serial, val text) ;\"\npsql -h 192.168.0.210 -U postgres -c \"INSERT INTO test (val) VALUES ('replication works') ;\"\npsql -h 192.168.0.204 -U postgres -c \"SELECT * FROM test ;\"\n# Attendu : une ligne avec val = 'replication works'\n# Se connecte directement \u00e0 postgres-02 (un standby) pour confirmer les donn\u00e9es r\u00e9pliqu\u00e9es\n# Si \"relation does not exist\" : la r\u00e9plication ne fonctionne pas - v\u00e9rifier pg_stat_replication\npsql -h 192.168.0.205 -U postgres -c \"SELECT * FROM test ;\"\n# Attendu : m\u00eame ligne - confirme que postgres-03 est \u00e9galement r\u00e9pliqu\u00e9\npsql -h 192.168.0.210 -U postgres -c \"DROP TABLE test ;\"\n# Nettoyer la table test<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># V\u00e9rifiez que la section ctl est pr\u00e9sente sur tous les n\u0153uds PostgreSQL avant de tenter le basculement\nsudo tail -5 \/etc\/patroni\/config.yml\n# Attendu :\n#   ctl:\n#     insecure: true\n# En cas d'absence : ajoutez-la et rechargez patroni (sudo systemctl reload patroni)\n# REMARQUE : n'ajoutez pas cacert ou certfile \u00e0 la section ctl \u2014 seulement insecure: true\n# L'ajout d'un fichier de certificat serveur provoque une mauvaise n\u00e9gociation TLS et le basculement \u00e9choue toujours<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code># Sur n'importe quel n\u0153ud PostgreSQL\nsudo patronictl -c \/etc\/patroni\/config.yml switchover postgresql-cluster \\\n  --leader postgresql-01 \\\n  # --leader : le primaire actuel r\u00e9trograd\u00e9\n  # REMARQUE : les anciennes versions de Patroni utilisaient --master \u2014 les versions plus r\u00e9centes utilisent --leader\n  --candidate postgresql-02\n  # --candidate : le standby promu\n# patronictl affichera la topologie actuelle et demandera une confirmation \u2014 appuyez sur Entr\u00e9e pour \" maintenant \", puis sur y\n\n# Patroni effectue ces \u00e9tapes automatiquement :\n# 1. Met en pause les \u00e9critures sur le primaire (point de contr\u00f4le)\n# 2. Attend que le candidat confirme qu'il a appliqu\u00e9 toutes les WAL\n# 3. R\u00e9trograde le primaire actuel en standby\n# 4. Proscrit le candidat en primaire\n# 5. Reconfigure l'ancien primaire en standby en utilisant pg_rewind<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo patronictl -c \/etc\/patroni\/config.yml list\n# Attendu : postgresql-02 affiche maintenant R\u00f4le = Leader sur une nouvelle timeline ; postgresql-01 affiche R\u00f4le = R\u00e9plique\n# postgresql-01 peut bri\u00e8vement afficher \"stopped\" \u2014 c'est pg_rewind en cours d'ex\u00e9cution ; attendez 10 secondes et v\u00e9rifiez \u00e0 nouveau\n# Si postgresql-01 reste \"stopped\" : pg_rewind a \u00e9chou\u00e9\n#   Correction : sudo patronictl -c \/etc\/patroni\/config.yml reinit postgresql-cluster postgresql-01<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur postgres-01 (le primaire actuel)\nsudo systemctl stop patroni\n# Ceci simule un crash du primaire \u2014 PostgreSQL s'arr\u00eate et le bail du leader expire<\/code><\/pre>\n\n\n\n<p>Sur postgres-02 ou postgres-03, observez le basculement automatique\u00a0:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Sur postgres-02 ou postgres-03\nwatch -n2 \"sudo patronictl -c \/etc\/patroni\/config.yml list\"\n# S\u00e9quence attendue sur ~30 secondes (ttl) :\n# 1. postgresql-01 dispara\u00eet ou affiche \"stopped\"\n# 2. Un des n\u0153uds restants affiche Leader sur une nouvelle timeline\n# 3. L'autre n\u0153ud restant affiche Replica streaming\n# Appuyez sur Ctrl+C lorsque le nouveau leader est confirm\u00e9<\/code><\/pre>\n\n\n\n<p>Ramener postgres-01 :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Sur postgres-01\nsudo systemctl start patroni<\/code><\/pre>\n\n\n\n<p>V\u00e9rifiez le cluster \u2014 postgres-01 devrait se rattacher en tant que r\u00e9plique :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo patronictl -c \/etc\/patroni\/config.yml list\n# Attendu\u00a0: postgres-01 affiche Replica streaming, Lag = 0\n# Si postgres-01 affiche \"\u00a0start failed\u00a0\"\u00a0: v\u00e9rifiez sudo journalctl -u patroni -n 30 --no-pager<\/code><\/pre>\n\n\n\n<p>Rebasculer sur postgres-01 comme primaire quand vous serez pr\u00eat :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo patronictl -c \/etc\/patroni\/config.yml switchover postgresql-cluster \\\n  --leader  \\\n  --candidate postgresql-01<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code>sudo patronictl -c \/etc\/patroni\/config.yml failover postgresql-cluster \\\n  --leader postgresql-01 \\\n  --candidate postgresql-02 \\\n  --force\n  # --force : ignorer l'invite de confirmation\n  # \u00c0 n'utiliser que lorsque le primaire est confirm\u00e9 comme \u00e9tant hors service\n  # Sans --force, patronictl vous demande confirmation avant de continuer<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code>sudo patronictl -c \/etc\/patroni\/config.yml list\n# Affiche tous les membres, les r\u00f4les (Leader\/Replica), l'\u00e9tat (running\/stopped\/start failed),\n# la timeline et le d\u00e9calage de r\u00e9plication en Mo\n# Ex\u00e9cutez ceci en premier chaque fois que vous diagnostiquez un probl\u00e8me de cluster<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Pause \u2014 Patroni ne promouvra aucun standby pendant la pause\n# \u00c0 utiliser lors de la maintenance planifi\u00e9e pour \u00e9viter un basculement accidentel\nsudo patronictl -c \/etc\/patroni\/config.yml pause postgresql-cluster\n\n# Reprendre \u2014 restaurer le basculement automatique\nsudo patronictl -c \/etc\/patroni\/config.yml resume postgresql-cluster<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Red\u00e9marrez toujours PostgreSQL via patronictl \u2014 jamais directement via systemctl\n# L'ex\u00e9cution de \"systemctl restart postgresql\" contourne Patroni et provoque un \u00e9tat incoh\u00e9rent\nsudo patronictl -c \/etc\/patroni\/config.yml restart postgresql-cluster postgresql-02<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"reload-configuration\">Recharger la configuration<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code># Appliquer les changements de postgresql.conf sur tous les n\u0153uds sans red\u00e9marrage\nsudo patronictl -c \/etc\/patroni\/config.yml reload postgresql-cluster<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"edit-cluster-dcs-configuration\">Modifier la configuration du cluster DCS<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code># Modifier les param\u00e8tres stock\u00e9s dans etcd (ttl, maximum_lag_on_failover, synchronous_mode, etc.)\nsudo patronictl -c \/etc\/patroni\/config.yml edit-config postgresql-cluster\n# Ouvre la configuration actuelle dans votre $EDITOR\n# Les modifications prennent effet imm\u00e9diatement apr\u00e8s la sauvegarde \u2014 aucun red\u00e9marrage n\u00e9cessaire<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"reinitialise-a-failed-standby\">R\u00e9initialiser un standby d\u00e9faillant<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code># Si pg_rewind \u00e9choue apr\u00e8s un basculement, nettoyez et reconstruisez la r\u00e9plique \u00e0 partir du ma\u00eetre\nsudo patronictl -c \/etc\/patroni\/config.yml reinit postgresql-cluster postgresql-03\n# Nettoie data_dir sur postgresql-03 et ex\u00e9cute pg_basebackup \u00e0 partir du ma\u00eetre actuel\n# Nettoie la r\u00e9plique et la reconstruit \u00e0 partir du ma\u00eetre actuel<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code>sudo patronictl -c \/etc\/patroni\/config.yml edit-config postgresql-cluster<\/code><\/pre>\n\n\n\n<p>Ajouter ou mettre \u00e0 jour :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>synchronous_mode: true\n# true : les commits sur le primaire attendent qu'au moins un standby confirme avant de retourner\n# Patroni d\u00e9finit synchronous_standby_names automatiquement \u2014 aucune configuration manuelle n'est n\u00e9cessaire\n\nsynchronous_mode_strict: false\n# false (par d\u00e9faut)\u00a0: si aucun standby synchrone n'est disponible, le primaire continue d'\u00e9crire\n# true\u00a0: si aucun standby synchrone n'est disponible, le primaire arr\u00eate totalement d'accepter les \u00e9critures\n#       utilisez true uniquement lorsque la perte de donn\u00e9es nulle est obligatoire et que la disponibilit\u00e9 peut \u00eatre sacrifi\u00e9e<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># V\u00e9rifier le code d'\u00e9tat HTTP uniquement \u2014 c'est ce que HAProxy v\u00e9rifie en interne\ncurl -k -o \/dev\/null -w \"%{http_code}\\n\" https:\/\/192.168.0.203:8008\/primary\n# Attendu sur le primaire : 200\n# Attendu sur une r\u00e9plique : 503\n# -k : ignorer la v\u00e9rification du certificat ; -o \/dev\/null : ignorer le corps ; -w : afficher uniquement le code d'\u00e9tat\n\ncurl -k -o \/dev\/null -w \"%{http_code}\\n\" https:\/\/192.168.0.203:8008\/replica\n# Attendu sur une r\u00e9plique : 200\n# Attendu sur le primaire : 503\n\n# V\u00e9rifier la r\u00e9ponse JSON compl\u00e8te (utile pour le d\u00e9bogage)\ncurl -k https:\/\/192.168.0.203:8008\/primary\n# La r\u00e9ponse inclut : r\u00f4le, version du serveur, chronologie, \u00e9tat de r\u00e9plication de chaque r\u00e9plique\n\ncurl -k https:\/\/192.168.0.203:8008\/cluster | python3 -m json.tool\n# \u00c9tat complet du cluster au format JSON format\u00e9\n# Attendu : tous les membres list\u00e9s avec leurs r\u00f4les, leur \u00e9tat et leur d\u00e9calage\n\ncurl -k https:\/\/192.168.0.203:8008\/health\n# Attendu : HTTP 200 si le n\u0153ud fonctionne normalement<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Option 1 \u2014 direct vers le primaire\npsql -h 192.168.0.203 -U postgres -c \"SELECT client_addr, replay_lag, sync_state FROM pg_stat_replication;\"\n\n# Option 2 \u2014 via VIP (HAProxy redirige toutes les connexions sur le port 5432 vers le primaire)\npsql -h 192.168.0.210 -p 5432 -U postgres -c \"SELECT client_addr, replay_lag, sync_state FROM pg_stat_replication;\"<\/code><\/pre>\n\n\n\n<p>R\u00e9sultat attendu :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>  client_addr | replay_lag | sync_state\n------------+------------+------------\n192.168.0.204 |            | async\n192.168.0.205 |            | async\n(2 lignes)<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur postgres-01\nsudo systemctl stop patroni\n# Arr\u00eate PostgreSQL gracieusement via Patroni \u2014 n'utilisez pas systemctl stop postgresql directement\n\nsudo systemctl stop etcd\n# Arr\u00eate le membre etcd sur ce n\u0153ud<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur postgres-01, postgres-02, postgres-03\nsudo rm -rf \/var\/lib\/etcd\/\n# Supprime toutes les donn\u00e9es WAL et snapshot d'etcd \u2014 le cluster etcd red\u00e9marrera \u00e0 partir de z\u00e9ro\n\nsudo rm -rf \/var\/lib\/postgresql\/data\/\n# Supprime tous les fichiers de donn\u00e9es PostgreSQL \u2014 Patroni r\u00e9initialisera via pg_basebackup sur les r\u00e9pliques<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur postgres-01, postgres-02, postgres-03\nsudo mkdir -p \/var\/lib\/etcd\/\nsudo chown etcd:etcd \/var\/lib\/etcd\/\n# etcd s'ex\u00e9cute en tant qu'utilisateur etcd \u2014 il doit \u00eatre propri\u00e9taire de son r\u00e9pertoire de donn\u00e9es\n\nsudo mkdir -p \/var\/lib\/postgresql\/data\nsudo chown postgres:postgres \/var\/lib\/postgresql\/data\n# Patroni s'ex\u00e9cute en tant qu'utilisateur postgres \u2014 il doit \u00eatre propri\u00e9taire du r\u00e9pertoire de donn\u00e9es PostgreSQL<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur 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# Donne \u00e0 l'utilisateur OS postgres un acc\u00e8s en lecture aux fichiers TLS d'etcd\n# Requis car Patroni (ex\u00e9cut\u00e9 par postgres) se connecte \u00e0 etcd via TLS<\/code><\/pre>\n\n\n\n<p>Sur postgres-02 :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Sur 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<\/code><\/pre>\n\n\n\n<p>Sur postgres-03 :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Sur 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<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur postgres-01\nsudo cp \/etc\/etcd\/etcd.env \/etc\/etcd\/etcd.env.$(date +%Y%m%d)\nsudo vi \/etc\/etcd\/etcd.env<\/code><\/pre>\n\n\n\n<p>Garder:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ETCD_INITIAL_CLUSTER_STATE=\"existing\"<\/code><\/pre>\n\n\n\n<p>\u00c0 :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ETCD_INITIAL_CLUSTER_STATE=\"new\"<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur postgres-01, postgres-02, postgres-03\nsudo systemctl start etcd<\/code><\/pre>\n\n\n\n<p>V\u00e9rifiez que les trois membres sont sains avant de d\u00e9marrer Patroni :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Sur 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# Attendu : les trois points de terminaison indiquent \"is healthy\"\n# Si un n\u0153ud n'est pas sain : v\u00e9rifiez journalctl -u etcd sur ce n\u0153ud avant de d\u00e9marrer Patroni<\/code><\/pre>\n\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\n<pre class=\"wp-block-code\"><code># Sur postgres-01 \u2014 d\u00e9marrer en premier\nsudo systemctl start patroni<\/code><\/pre>\n\n\n\n<p>Attendez que postgres-01 apparaisse comme Leader :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Sur postgres-01\npatronictl -c \/etc\/patroni\/config.yml list\n# Attendu : postgresql-01 est indiqu\u00e9 comme Leader, \u00e9tat en cours d'ex\u00e9cution\n# Attendre cela avant de d\u00e9marrer postgres-02 et postgres-03<\/code><\/pre>\n\n\n\n<p>Puis lancez Patroni sur les n\u0153uds restants :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Sur postgres-02\nsudo systemctl start patroni<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code># Sur postgres-03\nsudo systemctl start patroni<\/code><\/pre>\n\n\n\n<p>V\u00e9rification finale :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Sur postgres-01\npatronictl -c \/etc\/patroni\/config.yml list\n# Attendu :\n# + Cluster : postgres-cluster --------+----+-----------+\n# | Membre        | H\u00f4te            | R\u00f4le    | \u00c9tat   | TL | Latence en Mo |\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 : cluster fra\u00eechement cr\u00e9\u00e9, la chronologie est r\u00e9initialis\u00e9e \u00e0 1 lors d'une r\u00e9initialisation compl\u00e8te\n# Si un n\u0153ud affiche \"start failed\" : v\u00e9rifiez journalctl -u patroni sur ce n\u0153ud<\/code><\/pre>\n\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":10,"href":"https:\/\/rootfan.com\/fr\/wp-json\/wp\/v2\/posts\/6685\/revisions"}],"predecessor-version":[{"id":6698,"href":"https:\/\/rootfan.com\/fr\/wp-json\/wp\/v2\/posts\/6685\/revisions\/6698"}],"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}]}}