Aller au contenu
Cloud medium

Chapitre 6, HAProxy + Corosync/Pacemaker, alternative souveraine

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

- 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 bastion, LoginGraceTime 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 tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn