
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 est un boîtier transparent ; un DNS managé tiers est une boîte noire avec une UI.
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 »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/ »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 »# 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 »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 »- 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 »- 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 »# 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 »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 »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é »# 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 »-
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 »| 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.