PostgreSQL¶
Rôle¶
Backend de base de données relationnelle à haute disponibilité de l'infrastructure Indio. Héberge notamment la base zabbix, backend du cluster de supervision (voir Zabbix). Le cluster combine réplication physique via repmgr et bascule automatique de l'accès applicatif via une VIP keepalived — c'est le seul mécanisme de haute disponibilité de l'infra basé sur une IP flottante côté base de données, à contraster avec le cluster Zabbix Server, qui utilise une HA native sans VIP.
Architecture¶
- 2 nœuds Rocky Linux 9.6 (clones du template
tmpl-rocky96-hardened) sur le segmentseg-data(10.100.2.192/27) :INDDATA001(10.100.2.193) — primaireINDDATA002(10.100.2.194) — standby (réplication physique streaming)
- PostgreSQL 18 (
postgresql-18, dépôt PGDG), extensionrepmgr(shared_preload_libraries=repmgr) pour l'orchestration de la réplication et le failover automatique (failover='automatic'dansrepmgr.conf, avecpromote_command/follow_command). - VIP keepalived
10.100.2.195/27(VRRP router id 51) : elle ne suit pas une priorité statique par nœud, mais le primaire réellement actif. Le scriptpg-is-primary.sh(exécuté toutes les 2 secondes par levrrp_script chk_pg_primary, poids 50) interroge PostgreSQL viaSELECT NOT pg_is_in_recovery()pour déterminer quel nœud doit porter la VIP. C'est cette VIP que les clients applicatifs (ex. Zabbix Server) utilisent comme point d'entrée unique. - Accès réseau : port
5432/tcpouvert (firewalld, zonedrop) ; protocole VRRP autorisé en règle riche firewalld pour keepalived.pg_hba.confautorise explicitement la réplication entre les 2 nœuds, la baserepmgr, ainsi que les clients Zabbix (10.100.2.226/10.100.2.227, soit INDSUPV001/002) vers la basezabbix— aucun autre client applicatif n'est autorisé à ce jour (voir Points d'attention). - Au niveau réseau NSX, aucune règle DFW dédiée n'est nécessaire pour l'accès depuis
seg-supervision: la zone Admin (T1-Admin) reste en allow par défaut (voir NSX), contrairement aux zones DMZ/SOC.
Structure du dépôt¶
| Fichier/Dossier | Contenu |
|---|---|
main.tf |
2 VM (for_each), clone du template, IP via NetBox |
variables.tf |
vsphere_network = "seg-data", nodes = [INDDATA001, INDDATA002], vm_cpu = 2, vm_ram = 4096 |
versions.tf |
Providers hashicorp/vsphere (>= 2.6.0), e-breuninger/netbox (>= 5.0.0, < 6.0.0) |
ansible/site.yml |
Orchestrateur : postgres_primary → postgres_standby → postgres_keepalived |
ansible/inventory.ini |
Groupes primary/standby/postgres, IP + repmgr_node_id par hôte |
ansible/group_vars/postgres.yml |
Secrets vaultés (repmgr_password, pg_vrrp_auth_pass), zabbix_pg_clients, paramètres VIP |
ansible/roles/postgres_primary/ |
initdb, postgresql.conf, pg_hba.conf, rôle+base repmgr, repmgr.conf (node_id=1), sudoers, repmgr primary register |
ansible/roles/postgres_standby/ |
repmgr.conf (node_id=2), repmgr standby clone --force, pg_hba.conf zabbix, repmgr standby register |
ansible/roles/postgres_keepalived/ |
Paquet keepalived, SELinux, pg-is-primary.sh, keepalived.conf, règle VRRP firewalld |
flowchart TB
subgraph SEGDATA["seg-data — 10.100.2.192/27"]
D1["INDDATA001<br/>10.100.2.193<br/>PostgreSQL 18 + repmgrd"]
D2["INDDATA002<br/>10.100.2.194<br/>PostgreSQL 18 + repmgrd"]
end
D1 -- "réplication physique streaming" --> D2
K1["keepalived<br/>chk_pg_primary (2s)"] -. "SELECT NOT pg_is_in_recovery()" .-> D1
K2["keepalived<br/>chk_pg_primary (2s)"] -. "SELECT NOT pg_is_in_recovery()" .-> D2
K1 & K2 ==> VIP(("VIP 10.100.2.195/27<br/>VRRP id 51"))
VIP --> ZBX["Zabbix Server HA<br/>INDSUPV001 / INDSUPV002<br/>base zabbix"]
Séquence d'un failover réel (validée le 2026-07-07 dans les deux sens, 001→002 puis 002→001) :
sequenceDiagram
participant P as INDDATA001 (primaire)
participant S as INDDATA002 (standby)
participant K1 as keepalived (001)
participant K2 as keepalived (002)
participant C as Client (ex. Zabbix)
Note over P,S: Fonctionnement normal — réplication streaming
P->>S: WAL streaming
K1->>P: chk_pg_primary OK (toutes les 2s)
K2->>S: chk_pg_primary KO (standby)
Note over K1: VIP .195 portée par INDDATA001
Note over P: Panne (postgresql-18 + repmgr-18 arrêtés)
Note over S: repmgrd détecte l'absence du primaire (~50s observés)
S->>S: repmgr standby promote (automatic)
K2->>S: chk_pg_primary devient OK
K1->>P: chk_pg_primary devient KO
Note over K2: VIP .195 bascule vers INDDATA002 (~10-15s après promotion)
C->>S: reconnexion applicative via la VIP
Note over P: Réintégration manuelle requise (repmgr standby clone --force)
Provisioning Terraform¶
/root/postgres/main.tfclone les VM depuisdata.vsphere_virtual_machine.templateviafor_eachsur la variablenodes.- IP attribuées dynamiquement depuis NetBox (data source
netbox_ip_addresses, filtre surdns_nameen minuscules) — pas d'adresse en dur dans le code. lifecycle.ignore_changescouvreannotation,clone[0].template_uuid,clone[0].customize,disk[0].io_share_count— évite une recréation de VM sur dérive cosmétique post-clonage.- Variables principales (
variables.tf) :vsphere_network = "seg-data",vm_gateway = "10.100.2.222",vm_netmask = 27,vm_cpu = 2,vm_ram = 4096,nodes = [INDDATA001, INDDATA002]. - Providers :
hashicorp/vsphere(>= 2.6.0),e-breuninger/netbox(>= 5.0.0, < 6.0.0). - Identifiants vCenter/NetBox attendus dans
terraform.tfvars(non versionné,.gitignore) ; modèle disponible dansterraform.tfvars.example.
Configuration Ansible¶
Playbook /root/postgres/ansible/site.yml, 3 rôles appliqués dans l'ordre, chacun tagué (primary_setup/standby_setup/keepalived_setup) pour permettre de cibler une phase précise sans rejouer tout le playbook :
postgres_primary(hôteINDDATA001) :initdb(siPG_VERSIONabsent),listen_addresses='*',shared_preload_libraries='repmgr'(+ restart du service — cette directive n'est pas rechargeable à chaud), bloc gérépg_hba.conf(réplication + baserepmgrentre les 2 IP, puis un second bloc dédié pour les clients Zabbix), ouverture du port 5432, création du rôlerepmgr(superuser, mot de passe vaulté) et de la baserepmgr, sudoers NOPASSWD dédié (/etc/sudoers.d/repmgr-postgres, requis par repmgr pour démarrer/arrêter/recharger PostgreSQL lors d'un switchover), déploiement derepmgr.conf(node_id=1),repmgr primary register --force, démarrage derepmgrd.postgres_standby(hôteINDDATA002) : sudoers identique, déploiement derepmgr.conf(node_id=2), puis — uniquement si le cluster local n'est pas déjà initialisé —repmgr standby clone --forcedepuis le primaire (initialise entièrementdata_directorypar streaming, hérite dupg_hba.confdu primaire, y compris son bloc zabbix), blocpg_hba.confzabbix redéployé par idempotence, démarrage de PostgreSQL en recovery,repmgr standby register --force, démarrage derepmgrd.postgres_keepalived(groupepostgres, les 2 nœuds, rôle symétrique) : installation dekeepalived, activation du booléen SELinuxkeepalived_connect_any(moduleansible.posix.seboolean, persistant), déploiement du scriptpg-is-primary.shet dekeepalived.conf(VIP10.100.2.195/27,vrrp_script chk_pg_primarytoutes les 2s, poids 50,fall 2/rise 2), autorisation du protocole VRRP dans firewalld (règle riche).
repmgr.conf.j2 (identique structurellement sur les 2 nœuds, seuls node_id/node_name/node_ip varient) définit use_replication_slots=yes, failover='automatic', promote_command='repmgr standby promote ...', follow_command='repmgr standby follow ... --upstream-node-id=%n', et les 4 service_*_command (sudo systemctl {start,stop,restart,reload} postgresql-18) qui s'appuient sur le sudoers déployé par les rôles primary/standby.
Le mot de passe du rôle repmgr ({{ repmgr_password }}) et la passphrase VRRP ({{ pg_vrrp_auth_pass }}) sont stockés chiffrés par Ansible Vault dans group_vars/postgres.yml — aucune valeur en clair dans le dépôt (voir gestion des secrets).
Procédure manuelle¶
Réintégration d'un nœud après un vrai failover — pas encore automatisée en Ansible
Après un failover réel, l'ancien primaire (ou un standby resté périmé) doit être recloné manuellement depuis le nouveau primaire ; site.yml ne gère pas ce cas (voir Points d'attention ci-dessous sur le risque de split-brain si on le rejoue tel quel). Procédure, à exécuter sur le nœud à réintégrer :
systemctl stop repmgr-18 postgresql-18
rm -rf /var/lib/pgsql/18/data
sudo -u postgres mkdir -p /var/lib/pgsql/18/data && chmod 700 /var/lib/pgsql/18/data
# <repmgr_password> : valeur récupérée depuis Ansible Vault (group_vars/postgres.yml)
sudo -u postgres bash -c "PGPASSWORD='<repmgr_password>' repmgr -h <ip_nouveau_primaire> \
-U repmgr -d repmgr -f /etc/repmgr/18/repmgr.conf standby clone --force"
systemctl start postgresql-18
sudo -u postgres repmgr -f /etc/repmgr/18/repmgr.conf standby register --force
systemctl start repmgr-18
Validée dans les deux sens lors du test du 2026-07-07 (001→002 puis 002→001), cluster restauré dans sa topologie d'origine.
Procédure de déploiement¶
terraform apply(dans/root/postgres) : clone les 2 VM et attribue leurs IP via NetBox.ansible-playbook site.yml(dans/root/postgres/ansible) : applique dans l'ordrepostgres_primary→postgres_standby→postgres_keepalived— l'ordre est important, le standby devant cloner un primaire déjà initialisé.
Ne jamais rejouer site.yml en entier sur un cluster déjà bootstrappé
site.yml a 3 plays statiques (hosts: primary = toujours INDDATA001, hosts: standby = toujours INDDATA002) : après un vrai failover où les rôles réels se sont inversés, un rejeu complet réactive le nœud primary figé par l'inventaire (redémarre son PostgreSQL périmé, le réenregistre comme primaire) pendant que l'autre nœud, réellement primaire, refuse à raison le retraitement. Vérifier systématiquement repmgr cluster show avant tout rejeu, et cibler uniquement la phase concernée avec --tags (primary_setup/standby_setup/keepalived_setup) plutôt que de rejouer tout site.yml.
Contrôle de santé / Vérification¶
sudo -u postgres repmgr -f /etc/repmgr/18/repmgr.conf cluster show(sur l'un ou l'autre nœud) : affiche le rôle réel (primary/standby) et l'état (running) de chaque nœud — seule source de vérité fiable sur qui est réellement primaire.SELECT client_addr, state, sync_state FROM pg_stat_replication;sur le primaire : confirme le streaming actif vers le standby.ip -4 addr show | grep 10.100.2.195sur chaque nœud : identifie lequel porte actuellement la VIP.systemctl status postgresql-18 repmgr-18 keepalived: les 3 services doivent êtreactive (running)sur les 2 nœuds (keepalived tourne partout, PostgreSQL/repmgrd aussi — c'est leur rôle interne, primaire ou standby, qui diffère).- Test de connexion applicative via la VIP :
psql -h 10.100.2.195 -U zabbix -d zabbixdepuis un hôte autorisé (ex. INDSUPV001/002) doit aboutir quel que soit le nœud réellement primaire.
Points d'attention¶
- SELinux
keepalived_connect_any: sans ce booléen (activé explicitement par le rôlepostgres_keepalived), keepalived ne peut pas établir la connexion TCP sortante que fait sonvrrp_scriptvers le port PostgreSQL — le symptôme côté script est trompeur (psqlrenvoie un simple refus, facilement confondu avec un problèmepg_hba.conf) — le script de détection du primaire échouerait silencieusement et la VIP ne basculerait jamais correctement. - VIP dynamique, pas de priorité statique : les 2 nœuds partagent la même priorité keepalived de base (100) ; c'est le
vrrp_script chk_pg_primary(poids 50) qui fait réellement basculer la VIP selon l'état réel de réplication PostgreSQL. nopreemptserait un contresens ici : pour une VIP qui doit suivre dynamiquement « qui est primaire » (priorité pilotée par script), la préemption doit rester active (comportement par défaut, non modifié) — sinon un nœud redevenu primaire (priorité plus haute) ne reprendrait jamais la VIP tant que l'ancien primaire (devenu standby, toujours vivant) continue d'émettre ses advertisements VRRP.nopreemptn'a de sens que pour des rôles figés par admin (ex. HAProxy/Squid), pas pour une VIP qui suit un état applicatif changeant.- Authentification du script de détection :
pg-is-primary.shutilise une connexion TCP/mot de passe (rôlerepmgr) plutôt qu'un changement d'utilisateur système, car la directiveuserd'unvrrp_scriptkeepalived a un bug connu sur la version installée (2.2.8) qui empêche sa ré-exécution après le premier run. - Mot de passe VRRP limité à 8 caractères : limite du protocole VRRPv2 (
auth_type PASS) — keepalived tronque silencieusement (logTruncating auth_pass to 8 characters) sans échouer ; à générer sur 8 caractères maximum dès le départ pour ce secret précis. pg_hba.confn'autorise que les 2 nœuds entre eux et les clients Zabbix : aucun autre client applicatif ne peut se connecter tant qu'une règle dédiée n'est pas ajoutée dans le bloc géré Ansible (postgres_primary/tasks/main.yml) — une règle ajoutée à la main sur un seul nœud pour tester ne survit pas à un failover/reclone (le standby clone écrasepg_hba.confavec la version du primaire au moment du clone). Décision actuelle : ne rien ouvrir de plus tant qu'aucun besoin applicatif réel ne se présente ; ne pas exposer le rôlerepmgr(superuser) à un futur client applicatif — prévoir un rôle dédié non-superuser.- Fenêtre d'indisponibilité mesurée (test réel du 2026-07-07) : ~50s entre l'arrêt du primaire et la promotion automatique du standby, VIP bascule ~10-15s après. Non modifiés :
reconnect_attempts/reconnect_intervalpar défaut de repmgr. - Dépendance croisée : la base
zabbixde ce cluster est le backend du cluster Zabbix HA — toute intervention surpg_hba.confou sur la VIP impacte directement la supervision. - Secrets :
repmgr_passwordetpg_vrrp_auth_passchiffrés via Ansible Vault dansgroup_vars/postgres.yml— voir gestion des secrets.