Aller au contenu
Cloud medium

Chapitre 6 — HAProxy + Corosync/Pacemaker, alternative souveraine

20 min de lecture

logo 3ds outscale

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).

  • 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 + haproxy avec 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.
  • 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-python côté contrôleur Ansible (le bootstrap EIM ne peut pas se faire avec oapi-cli/osc-cli).
  • Le LoginGraceTime du bastion porté à 120 s dans le drop-in du Chap 3 (sinon apt install pacemaker via ProxyJump timeout côté bastion sshd).
  • Quelques notions de Corosync/Pacemaker (votes, ressources, contraintes, OCF) — voir le guide Corosync/Pacemaker fondamentaux.

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.

Architecture Chap 6 — 3 HAProxy auto-gérés en cluster Corosync/Pacemaker, EIP flottante pilotée par agent OCF, backends Nginx en passthrough TLS

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 :

Fenêtre de terminal
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).

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.

  • 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.

# 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
...
haproxy.cfg.j2 (extrait Jinja)
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.

- 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: true

lookup('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.

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 :

ActionSource primaireAppel API ?
monitor toutes les 30 sIMDS public-ipv40 en chemin nominal
startIMDS first puis LinkPublicIp si l'EIP n'est pas déjà locale0 ou 1
stopIMDS first puis UnlinkPublicIp si on porte effectivement l'EIP0 ou 1
meta-data / validate-allStatique / IMDS0

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.

Fenêtre de terminal
# Tournée run_once sur le 1er nœud
pcs 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.153
pcs cluster enable --all
pcs property set stonith-enabled=false # lab — non SecNumCloud-ready
pcs 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.

Fenêtre de terminal
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 INFINITY
pcs constraint order start capstone-eip then start capstone-haproxy

La 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.

Fenêtre de terminal
pcs property set stonith-enabled=false

Sur 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_outscale qui appelle StopVms, et activer stonith-enabled=true. Hors-scope du capstone pédagogique.
Fenêtre de terminal
# Bascule contrôlée vers un autre nœud
pcs 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 5
done

Sortie observée sur le compte de référence :

09:13:15 HTTP 000 ← bascule en cours, EIP en transit
09: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é.

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.

  1. Pré-requis E — allouer l'EIP flottante via oapi-cli + tags.

  2. 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
  3. Apply Terraform 30-haproxy-cluster/.

    Fenêtre de terminal
    cd terraform/30-haproxy-cluster/
    terraform init
    terraform plan # → 45 ressources
    terraform apply
  4. Mettre à jour le drop-in SSH bastionLoginGraceTime 120.

    Fenêtre de terminal
    cd ../../ansible/
    ansible-playbook playbooks/harden-ssh-bastion.yml
  5. Lancer le playbook cluster.

    Fenêtre de terminal
    ansible-playbook playbooks/deploy-haproxy-cluster.yml

    Compter ~3 minutes (la phase 2 install pacemaker via apt est la plus longue).

  6. 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-eip Started sur 1 nœud, capstone-haproxy Started sur le même nœud.

  7. Tester l'EIP flottante depuis l'extérieur.

    Fenêtre de terminal
    curl -k https://<eip-flottante>/healthz # → ok
  8. Tester une bascule contrôlée (mesurer le RTO).

    Fenêtre de terminal
    pcs node standby <node-actif>
    # curl en boucle pendant la bascule
    pcs node unstandby <node-actif>
SymptômeCauseSolution
Could not find a version that satisfies the requirement osc-cli (pip)Mauvais nom de paquet PyPIpip 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 longBastion : LoginGraceTime 120 ; Ansible : timeout=60, ServerAliveInterval=30 CountMax=10
osc-cli api CreatePolicy --Document ... retourne InvalidParameter 3003 ou InvalidParameterValue 4110Bug client oapi-cli / osc-cli sur --Document JSONBootstrap EIM en SDK Python (script fourni)
pcs cluster setup échoue avec host seems to be in a cluster alreadyRun partiel précédentAjouter --force au pcs cluster setup
capstone-eip Starting indéfiniment + Failed Resource Actions: ConnectTimeoutDeadlock EIP unique : la VM cible n'a plus d'IP publiqueSolution NIC secondaire — voir page expérimentation
password_hash filter Ansible non disponiblepasslib non installé sur le contrôleurSoit pip install passlib, soit chpasswd direct
  • 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_outscale custom 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.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn