
Ce chapitre est l'alternative souveraine au Chapitre 5, LBU + DNS round-robin. Au lieu de 3 LBU managés + DNS RR tiers (Cloudflare, NS1…), on déploie 3 VMs HAProxy auto-gérées en cluster Corosync/Pacemaker avec une EIP flottante pilotée par un agent OCF custom IMDS-first. Tout reste dans le compte OUTSCALE, aucune dépendance à un DNS externe. C'est plus complexe que le Chap 5, mais le pattern est 100 % souverain et le RTO de bascule est de ~5 secondes (vs 2-4 minutes pour DNS RR + healthcheck). Public visé : avancé, à l'aise avec Pacemaker. Choisir ce chapitre plutôt que le 5 quand la souveraineté pure est non négociable (SecNumCloud, OIV, NIS 2 strict).
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Provisionner 3 VMs HAProxy dans les subnets publics avec 2 EIPs par VM (egress + flottante).
- Coder un agent OCF custom Bash en stratégie IMDS-first : pas d'appel API en chemin nominal
monitor. - Bootstrap d'un compte EIM scopé strictement aux 3 actions Public IP, via SDK Python (les CLI ont un bug sur
--Document). - Configurer un cluster Corosync 3 nœuds en unicast cross-Net (UDP 5404/5405 via peering).
- Créer les ressources Pacemaker
osc-eip+haproxyavec colocation + ordre (l'EIP doit être attachée AVANT que HAProxy démarre). - Mesurer le RTO réel d'une bascule contrôlée (
pcs node standby). - Comprendre le piège deadlock EIP unique et savoir ce qu'on n'a pas codé pour ne pas s'y faire prendre en prod.
Prérequis
Section intitulée « Prérequis »- Les Chapitres 1 à 4 appliqués (3 frontends Nginx HTTPS opérationnels, c'est le backend du cluster HAProxy).
- Pacemaker 2.1+ packagé sur Ubuntu 24.04, pas de compilation, juste
apt install. osc-sdk-pythoncôté contrôleur Ansible (le bootstrap EIM ne peut pas se faire avecoapi-cli/osc-cli).- Le
LoginGraceTimedu bastion porté à 120 s dans le drop-in du Chap 3 (sinonapt install pacemakervia ProxyJump timeout côté bastion sshd). - Quelques notions de Corosync/Pacemaker (votes, ressources, contraintes, OCF), voir le guide Corosync/Pacemaker fondamentaux.
Pourquoi cette alternative au LBU + DNS RR
Section intitulée « Pourquoi cette alternative au LBU + DNS RR »Le Chap 5 fonctionne très bien en environnement commercial standard. Quand ne pas le prendre :
- Posture SecNumCloud / OIV / NIS 2 strict, la dépendance à un DNS tiers (Cloudflare, NS1) implique que les logs DNS transitent chez un tiers, parfois hors UE. Pour les contextes où l'audit doit montrer une chaîne 100 % souveraine, c'est rédhibitoire.
- RTO sub-minute non négociable, le DNS RR plafonne à
TTL + healthcheck interval, soit 2-4 minutes en pratique. Un cluster Pacemaker bien réglé tient un RTO de l'ordre de 5-30 secondes. - Maîtrise complète du plan de contrôle, vous voulez voir exactement qui prend la décision de bascule, quand et pourquoi. Pacemaker expose sa décision dans sa configuration et ses journaux, nœud par nœud ; un DNS managé tiers ne restitue que le résultat via son interface, sans le raisonnement qui l'a produit.
Le prix à payer : 3 VMs à patcher, un cluster Pacemaker à exploiter, un agent OCF custom à maintenir. Pas anodin. À mettre dans la balance avec la simplicité du Chap 5.
L'architecture en 1 vue
Section intitulée « L'architecture en 1 vue »Le schéma superpose deux plans distincts : celui que Terraform pose une fois pour toutes, et celui que Pacemaker déplace au fil des élections. Retenir cette séparation évite la confusion la plus fréquente entre les trois adresses publiques permanentes et l'unique adresse qui bouge.
Trois EIPs permanentes (une par HAProxy, allouées par Terraform et attachées en NIC primaire pour la sortie Internet apt/yum) + une EIP flottante (allouée hors Terraform, datasource, pilotée par l'agent OCF selon l'élection Pacemaker).
Pré-requis E, EIP flottante allouée hors Terraform
Section intitulée « Pré-requis E, EIP flottante allouée hors Terraform »Comme l'EIP du bastion (Chap 3), la flottante du LBU vit hors Terraform pour survivre aux cycles destroy/apply. Une seule allocation, taguée pour le datasource :
EIP_ID=$(oapi-cli CreatePublicIp | jq -r '.PublicIp.PublicIpId')oapi-cli CreateTags --ResourceIds "[\"$EIP_ID\"]" --Tags '[ {"Key":"Name","Value":"capstone-haproxy-floating-ip"}, {"Key":"project","Value":"capstone"}, {"Key":"env","Value":"lab"}, {"Key":"owner","Value":"stephane-robert"}, {"Key":"cost-center","Value":"formation"}, {"Key":"purpose","Value":"haproxy-floating-ip"}, {"Key":"managed-by","Value":"oapi-cli"}]'Le datasource outscale_public_ip du stack 30-haproxy-cluster/ filtre par purpose=haproxy-floating-ip + project=capstone (un seul bloc filter à valeurs multiples, cf. piège du Chap 3).
Pré-requis F, compte EIM scopé strictement
Section intitulée « Pré-requis F, compte EIM scopé strictement »C'est la discipline non négociable de ce chapitre : l'agent OCF qui tourne en root sur les 3 HAProxy reçoit des credentials qui peuvent appeler uniquement 3 actions OUTSCALE. Pas le compte principal, pas un compte « lab ouvert », un user EIM dédié avec une policy minimale.
# scripts/bootstrap-eim-haproxy-cluster.py (extrait)POLICY_DOCUMENT = json.dumps({ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["api:LinkPublicIp", "api:UnlinkPublicIp", "api:ReadPublicIps"], "Resource": "*", }],})Le script scripts/bootstrap-eim-haproxy-cluster.py est idempotent, il crée le user, la policy, l'attachement, et une seule access key (la secret key n'est lisible qu'à CreateAccessKey, perdue si on l'oublie). Le profil est écrit dans ~/.osc/config.json côté contrôleur sous le nom haproxy-cluster. Le playbook Ansible le déposera plus tard sur les 3 HAProxy en /root/.osc/config.json mode 0600.
Le code Terraform, 30-haproxy-cluster/
Section intitulée « Le code Terraform, 30-haproxy-cluster/ »Ce stack Terraform ne décrit que l'infrastructure statique : les trois VMs, leurs groupes de sécurité et les EIPs de sortie. Rien de ce qui bouge au runtime n'y figure, et c'est volontaire, puisque l'état mobile appartient au cluster. L'arborescence reprend le découpage habituel du capstone, un fichier par préoccupation.
Répertoireterraform/
Répertoire30-haproxy-cluster/
- provider.tf
- variables.tf
- locals.tf
- data.tf
- security-group.tf
- haproxy.tf
- outputs.tf
45 ressources créées :
- 3 SG (un par Net) pour les 3 HAProxy
- 3 SG rules SSH depuis le subnet bastion (pattern CIDR, SG-to-SG cross-Net non supporté, cf. Chap 4)
- 3 SG rules HTTPS depuis Internet (entrée publique de l'EIP flottante)
- 18 SG rules Corosync UDP (3 SG × 3 CIDRs publics × 2 ports : 5404 et 5405)
- 9 SG rules pcsd TCP 2224 (3 SG × 3 CIDRs publics), auth + commandes pcs
- 3 VMs HAProxy dans le subnet public de chaque Net
- 3 EIPs egress + 3
outscale_public_ip_link(une par HAProxy, attachées en permanence à la NIC primaire pour la sortie Internet, apt updates, propagation cluster, dépose creds…)
L'EIP flottante est référencée en datasource (filter sur purpose=haproxy-floating-ip) et n'est attachée par aucune ressource Terraform, c'est l'agent OCF qui la pilote au runtime.
Le playbook Ansible, 6 phases
Section intitulée « Le playbook Ansible, 6 phases »Le playbook est découpé en six plays successifs, chacun idempotent et rejouable isolément. L'ordre n'est pas cosmétique : les credentials doivent être déposés avant l'agent OCF, et l'agent avant la création des ressources Pacemaker qui vont l'invoquer.
# ansible/playbooks/deploy-haproxy-cluster.yml (vue d'ensemble)
- name: Phase 1 - HAProxy install + config TCP passthrough hosts: role_haproxy ...
- name: Phase 2 - Pacemaker/Corosync, install hosts: role_haproxy ...
- name: Phase 3 - Credentials osc-cli (profil haproxy-cluster, scopé EIM) hosts: role_haproxy ...
- name: Phase 4 - Agent OCF custom osc-eip, dépose hosts: role_haproxy ...
- name: Phase 5 - Cluster pacemaker, auth + setup + start hosts: role_haproxy ...
- name: Phase 6 - Cluster pacemaker, ressources hosts: role_haproxy ...Phase 1, HAProxy en TCP passthrough
Section intitulée « Phase 1, HAProxy en TCP passthrough »HAProxy est configuré en mode TCP de bout en bout : il relaie les octets sans jamais terminer la session TLS. Le certificat reste donc sur les Nginx, et aucune clé privée n'a besoin d'être copiée sur les trois nœuds du cluster.
frontend https-front bind *:443 mode tcp option tcplog default_backend nginx-passthrough
backend nginx-passthrough mode tcp balance roundrobin option ssl-hello-chk{% for net_key, fe in nginx_frontends.items() %} server nginx-{{ net_key }} {{ fe.private_ip }}:443 check inter 3s rise 2 fall 3{% endfor %}mode tcp partout, HAProxy ne déchiffre rien, passthrough TLS intégral vers les 3 Nginx. option ssl-hello-chk envoie un ClientHello en healthcheck pour valider qu'un Nginx parle bien TLS, sans déchiffrer la requête.
Crucial : state: stopped, enabled: false dans la phase 1. C'est Pacemaker qui démarrera HAProxy sur le nœud actif, pas systemd au boot. Sinon les 3 HAProxy démarrent simultanément et 2 d'entre eux ont des sockets en CLOSE_WAIT inutilisables.
Phase 2, Install Pacemaker + paquet PyPI osc-sdk
Section intitulée « Phase 2, Install Pacemaker + paquet PyPI osc-sdk »Cette phase installe d'abord les paquets système du cluster, puis séparément le SDK Python dont l'agent OCF a besoin pour appeler l'API.
- ansible.builtin.apt: name: - pacemaker - corosync - pcs - python3-pip # requis pour pip install osc-sdk state: present update_cache: true
- ansible.builtin.pip: name: osc-sdk # ← le paquet PyPI s'appelle osc-sdk, pas osc-cli ! extra_args: "--break-system-packages"L'erreur classique : pip install osc-cli retourne Could not find a version that satisfies the requirement osc-cli. Le binaire osc-cli est packagé sous le nom osc-sdk.
Phase 3, Dépose des credentials EIM scopés
Section intitulée « Phase 3, Dépose des credentials EIM scopés »Les credentials ne sont pas régénérés sur les machines : ils sont lus une seule fois sur le contrôleur Ansible, puis recopiés à l'identique. Les deux tâches ci-dessous réalisent ce transfert.
- ansible.builtin.set_fact: haproxy_creds: >- {{ (lookup('ansible.builtin.file', '~/.osc/config.json') | from_json)['haproxy-cluster'] }} run_once: true no_log: true
- ansible.builtin.copy: dest: /root/.osc/config.json content: "{{ {'haproxy-cluster': haproxy_creds} | to_nice_json }}" owner: root group: root mode: "0600" no_log: truelookup('ansible.builtin.file', ...) lit ~/.osc/config.json côté contrôleur Ansible (le fichier produit par le bootstrap Pré-requis F). On extrait uniquement le profil haproxy-cluster et on le dépose en mode 0600 sur chaque HAProxy. no_log: true mandatory sur les deux tâches sinon Ansible logge la secret key en clair dans -v.
Phase 4, Dépose de l'agent OCF custom
Section intitulée « Phase 4, Dépose de l'agent OCF custom »L'agent /usr/lib/ocf/resource.d/capstone/osc-eip est un script Bash en stratégie IMDS-first : le monitor (toutes les 30 s) ne fait aucun appel API, juste un curl http://169.254.169.254/latest/meta-data/public-ipv4 local en sub-ms. C'est le hot path qui doit survivre aux pannes API OAPI.
Le code complet et la justification détaillée vivent dans la page EIP flottante HA, expérimentation. Logique condensée :
| Action | Source primaire | Appel API ? |
|---|---|---|
monitor toutes les 30 s | IMDS public-ipv4 | 0 en chemin nominal |
start | IMDS first puis LinkPublicIp si l'EIP n'est pas déjà locale | 0 ou 1 |
stop | IMDS first puis UnlinkPublicIp si on porte effectivement l'EIP | 0 ou 1 |
meta-data / validate-all | Statique / IMDS | 0 |
Les appels API mutants (LinkPublicIp, UnlinkPublicIp) sont wrappés dans un retry à backoff exponentiel (1 s, 2 s, 4 s) pour absorber les drops API transitoires sans bloquer le cluster.
Phase 5, Setup du cluster
Section intitulée « Phase 5, Setup du cluster »L'authentification pcs host auth doit impérativement précéder le setup :
sans elle, les autres nœuds rejettent les commandes émises depuis le premier.
Les deux propriétés positionnées ensuite fixent le comportement du cluster face
au quorum, c'est-à-dire à la majorité de nœuds nécessaire pour décider.
# Tournée run_once sur le 1er nœudpcs host auth 10.10.0.233 10.11.0.29 10.12.0.153 -u hacluster -p ${PASSWORD}pcs cluster setup capstone-haproxy --start --wait=60 --force \ 10.10.0.233 10.11.0.29 10.12.0.153pcs cluster enable --allpcs property set stonith-enabled=false # lab, non SecNumCloud-readypcs property set no-quorum-policy=stop--force rend le setup idempotent même après un run partiel précédent (un nœud sur 3 déjà initialisé). En continu d'exploitation, on préférerait pcs cluster destroy + setup propre.
Phase 6, Ressources cluster + colocation
Section intitulée « Phase 6, Ressources cluster + colocation »Deux ressources seulement, et deux contraintes qui les lient. Ce sont les contraintes, bien plus que les ressources elles-mêmes, qui font la fiabilité du montage.
pcs resource create capstone-eip ocf:capstone:osc-eip \ ip=5.104.99.102 profile=haproxy-cluster \ op monitor interval=30s timeout=10s \ op start timeout=120s op stop timeout=60s
pcs resource create capstone-haproxy systemd:haproxy \ op monitor interval=20s timeout=10s
pcs constraint colocation add capstone-haproxy with capstone-eip INFINITYpcs constraint order start capstone-eip then start capstone-haproxyLa colocation INFINITY force haproxy à démarrer sur le même nœud que capstone-eip, toujours. La contrainte d'ordre garantit que l'EIP est attachée avant que HAProxy démarre, sinon HAProxy bind sur *:443 quand l'EIP n'est pas encore là, et la connexion suivante depuis Internet échoue.
STONITH désactivé, pourquoi et conséquence
Section intitulée « STONITH désactivé, pourquoi et conséquence »Ce réglage mérite d'être compris avant d'être recopié. Il écarte délibérément le mécanisme d'isolation qui protège un cluster Pacemaker du split-brain, la situation où deux moitiés du cluster continuent chacune de leur côté en se croyant seules légitimes.
pcs property set stonith-enabled=falseSur cloud, le STONITH (Shoot The Other Node In The Head) classique nécessite un accès iLO/IPMI au nœud à isoler. OUTSCALE ne donne pas cet accès, la seule alternative est un fence agent custom qui appelle oapi-cli StopVms pour forcer l'arrêt d'un nœud rebelle. C'est faisable mais alourdit le chapitre. Le capstone désactive STONITH et documente la limite :
- Sans STONITH, en cas de split-brain réseau, deux moitiés du cluster peuvent croire être seules avec quorum et toutes les deux essayer de démarrer la ressource. Sur OUTSCALE, l'API empêche d'avoir 2× la même EIP attachée donc le pire cas reste limité, mais ce n'est pas une garantie aussi forte que STONITH.
- En production SecNumCloud / OIV, écrire un fence agent
fence_outscalequi appelleStopVms, et activerstonith-enabled=true. Hors-scope du capstone pédagogique.
Test de bascule, RTO mesuré
Section intitulée « Test de bascule, RTO mesuré »La mesure se fait en deux temps : on provoque la bascule côté cluster, et on observe en parallèle ce que voit un client depuis l'extérieur. Le RTO (Recovery Time Objective) correspond ici à la durée pendant laquelle les requêtes échouent.
# Bascule contrôlée vers un autre nœudpcs node standby 10.10.0.233
# Côté client (curl en boucle sur l'EIP flottante)for i in {1..8}; do date +%H:%M:%S curl -ks --max-time 3 -w "HTTP %{http_code}\n" -o /dev/null https://5.104.99.102/healthz sleep 5doneSortie observée sur le compte de référence :
09:13:15 HTTP 000 ← bascule en cours, EIP en transit09:13:20 HTTP 200 ← EIP attachée au nouveau primary, HAProxy démarré09:13:25 HTTP 200...RTO mesuré : ~5 secondes sur une bascule contrôlée (pcs node standby). C'est 30 à 50 fois plus rapide que le DNS round-robin du Chap 5. À mettre dans la balance avec la complexité.
Le piège connu, deadlock EIP unique
Section intitulée « Le piège connu, deadlock EIP unique »Ce que vous découvrirez en testant : si vous enchaînez 2 bascules (standby A puis standby B), la 2e timeout. Pourquoi ? Parce que la VM abandonnée par l'EIP flottante n'a plus aucune IP publique attachée (Outscale a 1 EIP par NIC primaire, écrasée à chaque LinkPublicIp), donc plus d'accès API → impossible de récupérer l'EIP au prochain failover. Le cluster timeout 2 minutes puis essaie le 3e nœud (qui lui a encore son EIP egress).
Le fix propre est une NIC secondaire dédiée à la flottante, la NIC primaire garde son EIP egress permanente, la NIC secondaire seule reçoit/perd la flottante. Le code Terraform et les ajustements de l'agent OCF sont décrits dans la page EIP flottante HA, expérimentation.
Étapes, apply pas à pas
Section intitulée « Étapes, apply pas à pas »La séquence complète, du provisionnement jusqu'à la vérification d'une bascule. Les deux pré-requis ne se jouent qu'une seule fois ; les étapes suivantes sont rejouables autant de fois que nécessaire.
-
Pré-requis E, allouer l'EIP flottante via
oapi-cli+ tags. -
Pré-requis F, bootstrap du compte EIM scopé.
Fenêtre de terminal python3 scripts/bootstrap-eim-haproxy-cluster.py# Crée le user, la policy, l'access key, écrit ~/.osc/config.json profil haproxy-cluster -
Apply Terraform
30-haproxy-cluster/.Fenêtre de terminal cd terraform/30-haproxy-cluster/terraform initterraform plan # → 45 ressourcesterraform apply -
Mettre à jour le drop-in SSH bastion,
LoginGraceTime 120.Fenêtre de terminal cd ../../ansible/ansible-playbook playbooks/harden-ssh-bastion.yml -
Lancer le playbook cluster.
Fenêtre de terminal ansible-playbook playbooks/deploy-haproxy-cluster.ymlCompter ~3 minutes (la phase 2 install pacemaker via
aptest la plus longue). -
Vérifier l'état du cluster.
Fenêtre de terminal ssh outscale@<bastion> 'ssh outscale@<haproxy-X-priv> sudo pcs status'Attendu : 3 nodes Online,
capstone-eipStarted sur 1 nœud,capstone-haproxyStarted sur le même nœud. -
Tester l'EIP flottante depuis l'extérieur.
Fenêtre de terminal curl -k https://<eip-flottante>/healthz # → ok -
Tester une bascule contrôlée (mesurer le RTO).
Fenêtre de terminal pcs node standby <node-actif># curl en boucle pendant la basculepcs node unstandby <node-actif>
Pièges courants
Section intitulée « Pièges courants »Ces six symptômes sont ceux réellement rencontrés en montant le chapitre.
Trois d'entre eux relèvent de l'outillage et non de la conception du cluster :
le nom du paquet PyPI, le bug des clients en ligne de commande sur
--Document, et le délai de connexion SSH imposé par le bastion.
| Symptôme | Cause | Solution |
|---|---|---|
Could not find a version that satisfies the requirement osc-cli (pip) | Mauvais nom de paquet PyPI | pip install osc-sdk (le binaire osc-cli est dans osc-sdk) |
Connection timed out during banner exchange (apt install via ProxyJump) | LoginGraceTime 30 du bastion + tunnel preauth long | Bastion : LoginGraceTime 120 ; Ansible : timeout=60, ServerAliveInterval=30 CountMax=10 |
osc-cli api CreatePolicy --Document ... retourne InvalidParameter 3003 ou InvalidParameterValue 4110 | Bug client oapi-cli / osc-cli sur --Document JSON | Bootstrap EIM en SDK Python (script fourni) |
pcs cluster setup échoue avec host seems to be in a cluster already | Run partiel précédent | Ajouter --force au pcs cluster setup |
capstone-eip Starting indéfiniment + Failed Resource Actions: ConnectTimeout | Deadlock EIP unique : la VM cible n'a plus d'IP publique | Solution NIC secondaire, voir page expérimentation |
password_hash filter Ansible non disponible | passlib non installé sur le contrôleur | Soit pip install passlib, soit chpasswd direct |
À retenir
Section intitulée « À retenir »- 3 VMs HAProxy + Pacemaker = HA souveraine sans DNS tiers, RTO ~5 s sur bascule simple.
- L'agent OCF en stratégie IMDS-first ne fait aucun appel API dans le hot path
monitor, robustesse face aux drops OAPI. - Le compte EIM scopé (3 actions seulement) est non négociable, bootstrap en SDK Python parce que les CLI ont un bug sur
--Document. - STONITH désactivé en lab, à coder via
fence_outscalecustom en SecNumCloud / OIV. - Le piège deadlock EIP unique est documenté mais non corrigé dans le capstone, solution NIC secondaire en production.
- Comparé au Chap 5 : RTO 30× meilleur, mais 3 VMs à patcher et un cluster à exploiter. Choix pragmatique selon le contexte.
Pour aller plus loin
Section intitulée « Pour aller plus loin »- EIP flottante en haute disponibilité : Le code complet de l'agent OCF, la solution NIC secondaire au deadlock et des mesures de RTO étendues.
- Talos Linux, cluster multi-AZ : Une autre façon de répartir un service sur trois sous-régions, cette fois par un plan de contrôle Kubernetes auto-géré.
- OMI déclaratives avec NixOS : Construire les images des nœuds du cluster de façon reproductible, sans dépendre d'un playbook de post-installation.