Aller au contenu
Cloud medium

Chapitre 5 — 3 LBU OUTSCALE + DNS round-robin pour la HA cross-AZ

20 min de lecture

logo 3ds outscale

Ce chapitre expose publiquement l'application via 3 LBU OUTSCALE managés (un par AZ) en mode TCP/443 passthrough vers les frontends Nginx du Chapitre 4. Les 3 EIPs des LBU sont publiées en DNS round-robin côté client, avec un healthcheck DNS externe qui retire l'IP morte du carrousel en cas de perte d'AZ. C'est l'approche cloud-native standard : pas de Pacemaker à orchestrer, pas de pilote Python à maintenir, le client TCP retry naturellement sur l'IP suivante quand une connexion échoue. Le Chapitre 6 présente l'alternative souveraine (HAProxy + Pacemaker) pour les contextes où la dépendance à un DNS tiers est inacceptable.

  • Coder un stack Terraform 25-lbu/ qui crée 3 LBU internet-facing avec backends VM côté Net.
  • Configurer un listener TCP/443 passthrough — le LBU ne déchiffre pas le TLS.
  • Vérifier que le certificat exposé par le LBU est bien celui du Nginx local (pas du LBU lui-même).
  • Publier les 3 EIPs en DNS round-robin dans une zone tiers (Cloudflare, NS1, Route 53).
  • Configurer un healthcheck DNS externe qui retire l'IP morte automatiquement en cas de perte d'AZ.
  • Comprendre les limites du DNS RR (TTL-bound, comportement client variable) et ce que ça implique en RTO réel.
  • Les Chapitres 1 à 4 appliqués (frontends Nginx HTTPS opérationnels).
  • Un compte tiers DNS avec healthcheck (Cloudflare gratuit, NS1, Route 53). Le DNS auto-hébergé sans healthcheck (Bind9, PowerDNS) est documenté en alternative.
  • Un nom de domaine dont vous gérez les NS (par exemple capstone.example.org).

OUTSCALE laisse créer un LBU sur plusieurs subnets simultanément (un par AZ), ce qui semble plus simple que 3 LBU séparés. Pourquoi le capstone choisit néanmoins 3 LBU ?

Aspect1 LBU multi-AZ3 LBU séparés
EIP1 seule (mais résolue par DNS Outscale en 3 IPs internes)3 EIPs publiques distinctes
Blast radius configUne mauvaise modif tue toutUn LBU isolé = 2 autres OK
Audit indépendant par AZFiltrage logique à appliquerTrivial — un LBU = un audit
DNS round-robin côté clientPas applicableNaturel (3 records A)
Gestion fine des SGSG globalSG par Net, audit par AZ
Coût1× LBU3× LBU (~30 €/mois total)

Le trigger principal est le DNS round-robin : pour qu'il marche, il faut 3 IPs publiques distinctes. Avec un LBU multi-AZ, on n'a qu'une IP côté client — le routage cross-AZ se fait à l'intérieur du service managé, opaque, et incontrôlable en cas de défaillance partielle.

Pourquoi TCP/443 passthrough et pas HTTPS terminé

Section intitulée « Pourquoi TCP/443 passthrough et pas HTTPS terminé »

C'est la suite directe du choix SSL bout en bout du Chapitre 4. Le LBU est configuré comme suit :

listeners {
backend_port = 443
backend_protocol = "TCP"
load_balancer_port = 443
load_balancer_protocol = "TCP"
}

TCP partout — pas HTTP ni HTTPS. Ça veut dire :

  • Le LBU ne déchiffre rien — pas de cert sur le LBU lui-même.
  • Le client TLS négocie directement avec le Nginx qui le sert (signé par la CA capstone).
  • Aucun header L7 (X-Forwarded-For, ALB-Sticky-Cookie…) n'est injecté — le LBU est invisible sur le payload.

Validation côté client :

Fenêtre de terminal
$ curl -kvs https://171.33.98.30/healthz 2>&1 | grep "subject:"
* subject: CN=capstone-nginx-a
$ curl -kvs https://171.33.93.29/healthz 2>&1 | grep "subject:"
* subject: CN=capstone-nginx-c

Le CN change selon l'IP visée — preuve directe que le LBU n'est pas en train de présenter un cert mutualisé, c'est bien le frontend qui répond avec son propre certificat.

Le couplage AZ-local — chaque LBU pointe vers SON Nginx

Section intitulée « Le couplage AZ-local — chaque LBU pointe vers SON Nginx »

Décision d'architecture du capstone pédagogique : lbu-a ne sert que nginx-a, lbu-b que nginx-b, etc. Pas de routage cross-AZ par le LBU.

resource "outscale_load_balancer_vms" "front_backends" {
for_each = local.lbus
load_balancer_name = outscale_load_balancer.front[each.key].load_balancer_name
backend_vm_ids = [each.value.backend_vm_id] # un seul Nginx
}

Ce choix est directement lié à la PKI définie au Chapitre 4 : chaque frontend a son propre certificat signé par la CA capstone, avec un CN distinct par AZ (CN=capstone-nginx-a, b, c). Conséquence : un LBU ne peut pas router cross-AZ vers un autre nginx sans présenter au client un cert dont le CN ne match pas — le client refuse la connexion. Le couplage 1-pour-1 est imposé par cette PKI per-frontend.

Bénéfices secondaires : latence sub-ms (pas de traversée peering), lisibilité audit (1 LBU = 1 AZ), facture NAT minimale.

Variante prod — cert mutualisé + 3 backends par LBU

Section intitulée « Variante prod — cert mutualisé + 3 backends par LBU »

Pour la production, la bonne pratique est un cert unique partagé par les 3 frontends (CN=capstone.example.org + SAN sur les 3 IPs privées et le hostname public). Tous les nginx présentent alors le même certificat. Le LBU peut router indifféremment vers les 3, le client final ne voit aucune différence.

Conséquence sur le code Terraform :

resource "outscale_load_balancer_vms" "front_backends" {
for_each = local.lbus
load_balancer_name = outscale_load_balancer.front[each.key].load_balancer_name
backend_vm_ids = [
for k in keys(local.nginx_frontends) : local.nginx_frontends[k].vm_id
] # les 3 nginx en backend de chaque LBU
}

Bénéfice : deux niveaux de HA au lieu d'un.

PanneMécanismeRTO
1 nginx tombe (instance)Healthcheck LBU sort le backend~10 s
1 AZ entière tombe (LBU + nginx)DNS RR retire l'IP du carrousel~2-4 min (TTL + healthcheck DNS)

Conséquence sur le playbook Ansible : un seul appel community.crypto.openssl_csr (sur localhost), subject_alt_name qui inclut DNS:capstone.example.org + les 3 IP:10.x.1.Y, puis le même couple key/cert copié sur les 3 frontends.

Le capstone garde la version 1-cert-par-AZ pour rendre visible dans les tests curl quel frontend répond effectivement (CN=capstone-nginx-a vs b vs c) — c'est pédagogiquement précieux. Mais en prod, basculer sur cert mutualisé.

La CA capstone est auto-signée : pas valide pour les navigateurs ou les clients sans installation manuelle de ca.crt dans le store. Pour passer en prod, trois voies — toutes compatibles avec le passthrough TCP/443 (les certs restent sur les Nginx, le LBU ne touche pas au TLS).

Voie 1 — Let's Encrypt via DNS-01 challenge (commercial)

Section intitulée « Voie 1 — Let's Encrypt via DNS-01 challenge (commercial) »

Les frontends Nginx n'ont pas d'IP publique (subnet privé), donc le challenge HTTP-01 standard ne fonctionne pas. Le DNS-01 challenge passe par le tiers DNS qu'on utilise déjà pour le round-robin :

Fenêtre de terminal
# Sur chaque frontend nginx (ou via Ansible) :
apt install certbot python3-certbot-dns-cloudflare
# Token Cloudflare scopé sur la zone (Edit DNS only)
echo "dns_cloudflare_api_token = ${CF_TOKEN}" > /etc/letsencrypt/cloudflare.ini
chmod 0600 /etc/letsencrypt/cloudflare.ini
certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
--dns-cloudflare-propagation-seconds 30 \
-d capstone.example.org \
--email ops@example.org --agree-tos --non-interactive

Plugins certbot équivalents : python3-certbot-dns-route53, python3-certbot-dns-nsone. Renouvellement automatique via le timer systemd certbot.timer (90 jours par certificat, refresh à 60 jours).

Avantage : gratuit, automatisable, largement reconnu. Limite : non eIDAS-qualifié — pas adapté aux contextes SecNumCloud / OIV où l'autorité doit figurer sur la liste de confiance ANSSI.

Voie 2 — Cert commercial (DigiCert, Sectigo, Certigna)

Section intitulée « Voie 2 — Cert commercial (DigiCert, Sectigo, Certigna) »

Pour SecNumCloud ou OIV, choisir une CA eIDAS-qualifiée présente sur la TSL ANSSI. Certigna (groupe Dhimyotis, hébergement France) et DigiCert sont les deux plus utilisés en France. Le cert est généré une fois (validité 1 ou 2 ans), déployé sur les 3 frontends.

Le renouvellement est manuel — pas de DNS-01 automatique sur ces CA. Ça impose un calendrier opérationnel et un runbook de rotation. C'est le tribut de la conformité.

Si l'organisation exploite déjà une PKI interne (ICP-EU pour les administrations françaises, AD CS pour les SI Microsoft, smallstep CA, etc.), signer le cert capstone avec cette CA. Les postes clients ont déjà la racine en confiance — aucune installation côté utilisateur.

C'est le pattern souverain par excellence : la confiance reste dans l'organisation, sans dépendance ni à Let's Encrypt ni à un tiers commercial. Limite : nécessite que la PKI interne soit déjà mature (ce n'est pas le sujet du capstone).

  • Répertoireterraform/
    • Répertoire25-lbu/
      • provider.tf
      • variables.tf
      • locals.tf
      • security-group.tf
      • lbu.tf
      • outputs.tf

Total créé : 3 SG + 3 SG rules + 3 LBU + 3 attachements backend = 12 ressources. Le pattern for_each = local.lbus (clés a/b/c) est identique au stack 20-app/ — cohérence visuelle dans tous les plans Terraform.

La ressource outscale_load_balancer_vms (et pas backend_vm_ids inline)

Section intitulée « La ressource outscale_load_balancer_vms (et pas backend_vm_ids inline) »
resource "outscale_load_balancer_vms" "front_backends" {
for_each = local.lbus
load_balancer_name = outscale_load_balancer.front[each.key].load_balancer_name
backend_vm_ids = [each.value.backend_vm_id]
}

L'attachement des backends est une ressource séparée, pas un attribut de outscale_load_balancer. C'est un choix de design du provider : ça permet d'ajouter/retirer des VMs backend sans recréer le LBU. Si plus tard on veut ajouter un 2e Nginx par AZ, on étend backend_vm_ids — le LBU lui-même reste en place avec son EIP.

La sortie Terraform — 3 EIPs prêtes pour le DNS

Section intitulée « La sortie Terraform — 3 EIPs prêtes pour le DNS »

Le terraform output lbus produit la map qui alimentera la zone DNS :

lbus = {
"a" = {
"dns_name" = "capstone-lbu-a-225682977.eu-west-2.lbu.outscale.com"
"name" = "capstone-lbu-a"
"public_ip" = "171.33.98.30"
"subregion" = "eu-west-2a"
}
"b" = {
"dns_name" = "capstone-lbu-b-785877885.eu-west-2.lbu.outscale.com"
"name" = "capstone-lbu-b"
"public_ip" = "171.33.106.215"
"subregion" = "eu-west-2b"
}
"c" = { "public_ip" = "171.33.93.29" ... }
}

Trois IPs publiques distinctes — c'est le pré-requis du DNS round-robin.

Trois enregistrements A sur le même nom, TTL court (60 secondes recommandé) pour une bascule rapide. Sur Cloudflare, NS1 ou Route 53, c'est le pattern « Multivalue Answer Routing » avec healthchecks :

Zone capstone.example.org
capstone 60 IN A 171.33.98.30 ; LBU-A (eu-west-2a)
capstone 60 IN A 171.33.106.215 ; LBU-B (eu-west-2b)
capstone 60 IN A 171.33.93.29 ; LBU-C (eu-west-2c)

Le TTL 60 s est un compromis : TTL court = bascule rapide quand un healthcheck retire une IP, mais coût de query DNS plus élevé pour les clients (ils refresh 1×/min). 60 s est un bon défaut grand public, descendre à 30 s n'apporte que marginalement et augmente la facture DNS.

Cloudflare propose des healthchecks gratuits dans le tier Free, attachés à un Load Balancer DNS (qui n'est pas un proxy mais un module DNS sophistiqué). Configuration type :

Pool: capstone-lbu-pool
Origins:
- lbu-a: 171.33.98.30 weight=1 monitor=https-healthz
- lbu-b: 171.33.106.215 weight=1 monitor=https-healthz
- lbu-c: 171.33.93.29 weight=1 monitor=https-healthz
Monitor: https-healthz
Type: HTTPS
Method: GET
Path: /healthz
Expected status: 200
Expected body: "ok"
Interval: 60s
Timeout: 5s
Retries: 2
Allow insecure: true # cert self-signed CA capstone
Steering policy: random # round-robin équiprobable

Cloudflare interroge /healthz toutes les 60 s sur chacun des 3 LBU. Si un LBU manque 2 réponses consécutives (3 minutes), il sort du pool — le DNS arrête de retourner cette IP aux clients. Quand le LBU revient, il re-rentre automatiquement.

Alternative gratuite et auto-hébergée — Bind9 + cron

Section intitulée « Alternative gratuite et auto-hébergée — Bind9 + cron »

Sans budget DNS managé, on peut faire du DNS round-robin pur (sans healthcheck) avec Bind9 et un script cron qui réécrit la zone toutes les minutes après healthcheck. Brouillon de la logique :

#!/bin/sh
# /usr/local/bin/capstone-dns-healthcheck — toutes les 60s via cron
ALL_IPS="171.33.98.30 171.33.106.215 171.33.93.29"
HEALTHY=""
for ip in $ALL_IPS; do
if curl -k -sf --max-time 3 "https://${ip}/healthz" >/dev/null; then
HEALTHY="${HEALTHY} ${ip}"
fi
done
# Régénérer la zone avec uniquement les IPs healthy + rndc reload
generate_zone "$HEALTHY" > /etc/bind/zones/capstone.zone
rndc reload capstone.example.org

Adapté pour les labs et les déploiements où la dépendance à un DNS managé n'est pas acceptable. Pour les déploiements grand public, Cloudflare reste plus robuste.

C'est la section honnête — le DNS RR n'est pas une recette parfaite. Trois limites à connaître avant de choisir :

  • TTL bound — quand une IP est retirée du pool, les caches DNS des clients (resolvers FAI, navigateurs) gardent l'ancienne réponse jusqu'au TTL. Avec TTL=60 s, un client peut continuer à appeler l'IP morte pendant 1 min après la défaillance. RTO de bascule ~ TTL + healthcheck interval, soit 2-4 minutes en pratique.
  • Comportement client variablecurl retry naturellement sur l'IP suivante en cas de connexion refusée. Les navigateurs modernes font pareil (Happy Eyeballs RFC 8305). Mais certains clients legacy ou applicatifs personnalisés ne retry pas et tombent sur l'erreur.
  • Pas de session affinity — chaque résolution DNS peut tomber sur une IP différente. Si l'application utilise des sessions stockées localement sur le frontend (cas anti-pattern qu'on évite déjà au Chap 4 avec stateless), ça casse. Avec sessions en BDD partagée, transparent.

Pour les contextes où ces limites ne sont pas acceptables (RTO < 30 s, contrôle total du plan de bascule), le Chapitre 6 présente l'alternative HAProxy + Corosync/Pacemaker.

  1. Vérifier que les 3 frontends Nginx répondent.

    Fenêtre de terminal
    ssh outscale@<bastion> 'for ip in 10.10.1.X 10.11.1.Y 10.12.1.Z; do
    curl -ks https://$ip/healthz; done'
    # → ok ok ok
  2. Apply Terraform 25-lbu/.

    Fenêtre de terminal
    cd terraform/25-lbu/
    terraform init
    terraform plan # → 12 ressources à créer
    terraform apply
  3. Récupérer les 3 EIPs.

    Fenêtre de terminal
    terraform output -json lbu_public_ips | jq
    # → ["171.33.98.30", "171.33.106.215", "171.33.93.29"]
  4. Tester le passthrough TLS de chaque LBU.

    Fenêtre de terminal
    for ip in $(terraform output -json lbu_public_ips | jq -r '.[]'); do
    echo "=== $ip ==="
    curl -ks --max-time 5 https://$ip/healthz
    curl -kvs --max-time 5 https://$ip/ 2>&1 | grep "subject:"
    done

    Sortie attendue : 3× ok + 3 lignes subject: CN=capstone-nginx-{a,b,c} (cert différent par LBU = passthrough confirmé).

  5. Publier la zone DNS (Cloudflare ou autre, hors Terraform pour la simplicité — voir terraform-cloudflare si vous voulez l'IaC complet).

    capstone.example.org (3 records A)
    capstone 60 IN A <ip-lbu-a>
    capstone 60 IN A <ip-lbu-b>
    capstone 60 IN A <ip-lbu-c>
  6. Configurer le healthcheck.

    • Cloudflare → Traffic → Load Balancing → Pools → Create Pool → 3 origins + monitor /healthz HTTPS.
    • NS1 → Records → Filter Chains → up filter sur monitor.
    • Route 53 → Health checks → 3 healthchecks, attacher un par record A.
  7. Test bout en bout depuis n'importe où.

    Fenêtre de terminal
    for i in 1 2 3 4 5; do
    curl -ks https://capstone.example.org/healthz
    dig +short capstone.example.org
    done

    Les 3 IPs doivent apparaître à tour de rôle dans la résolution dig.

SymptômeCauseSolution
curl: (60) SSL certificate problem: self signed certificateCA capstone n'est pas dans le store du clientImporter ca.crt dans le store, ou utiliser -k en lab
curl --resolve retourne le cert d'un autre LBURoutage cross-AZ activé par erreurVérifier backend_vm_ids = [each.value.backend_vm_id] (1 seul backend)
HTTP 000 répété sur un LBU précisLBU ou Nginx backend downoapi-cli ReadLoadBalancers + oapi-cli ReadVms ; vérifier le SG ingress 443 du Nginx
TTL DNS plus long qu'attenduCache resolver intermédiaireForcer un resolver direct : dig @1.1.1.1 capstone.example.org
Cloudflare healthcheck failed: cert errorAllow insecure: false sur le monitorActiver Allow insecure: true (cert self-signed accepté)
Le LBU ne reçoit pas de traficSubnet sans route 0.0.0.0/0 → IGWLe LBU doit être dans un subnet public (route IGW) — vérifier au Chap 1
  • 3 LBU séparés (un par AZ) plutôt qu'1 LBU multi-AZ — le DNS RR a besoin de 3 IPs publiques.
  • TCP/443 passthrough — le LBU n'est jamais TLS-terminator, le cert reste celui du Nginx local.
  • Couplage AZ-local côté backend — chaque LBU ne sert que SON Nginx, la résilience cross-AZ vient du DNS RR au-dessus.
  • TTL court (60 s) sur les records A — compromis bascule rapide vs charge DNS.
  • Healthcheck externe payant ou auto-hébergé — Cloudflare Free, NS1, Route 53, ou Bind9 + cron.
  • RTO bascule réel = TTL + healthcheck interval, soit ~2-4 min en pratique. Pas un bascule instantané.
  • L'alternative HAProxy + Pacemaker (Chap 6) résout le RTO long et la dépendance DNS tiers, au prix de la complexité.

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