
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.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- 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.
Prérequis
Section intitulée « Prérequis »- 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).
Pourquoi 3 LBU et pas 1 LBU multi-subnet
Section intitulée « Pourquoi 3 LBU et pas 1 LBU multi-subnet »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 ?
| Aspect | 1 LBU multi-AZ | 3 LBU séparés |
|---|---|---|
| EIP | 1 seule (mais résolue par DNS Outscale en 3 IPs internes) | 3 EIPs publiques distinctes |
| Blast radius config | Une mauvaise modif tue tout | Un LBU isolé = 2 autres OK |
| Audit indépendant par AZ | Filtrage logique à appliquer | Trivial — un LBU = un audit |
| DNS round-robin côté client | Pas applicable | Naturel (3 records A) |
| Gestion fine des SG | SG global | SG par Net, audit par AZ |
| Coût | 1× LBU | 3× 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 :
$ 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-cLe 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.
| Panne | Mécanisme | RTO |
|---|---|---|
| 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é.
Vers des certs valides — 3 voies
Section intitulée « Vers des certs valides — 3 voies »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 :
# 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.inichmod 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-interactivePlugins 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é.
Voie 3 — CA d'entreprise (PKI interne)
Section intitulée « Voie 3 — CA d'entreprise (PKI interne) »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).
Le code Terraform — 12 ressources
Section intitulée « Le code Terraform — 12 ressources »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.
DNS round-robin — la configuration côté zone
Section intitulée « DNS round-robin — la configuration côté zone »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 :
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.
Healthcheck DNS — Cloudflare exemple
Section intitulée « Healthcheck DNS — Cloudflare exemple »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-poolOrigins: - 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 équiprobableCloudflare 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 cronALL_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}" fidone
# Régénérer la zone avec uniquement les IPs healthy + rndc reloadgenerate_zone "$HEALTHY" > /etc/bind/zones/capstone.zonerndc reload capstone.example.orgAdapté 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.
Limites du DNS round-robin
Section intitulée « Limites du DNS round-robin »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 variable —
curlretry 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.
Étapes — apply pas à pas
Section intitulée « Étapes — apply pas à pas »-
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; docurl -ks https://$ip/healthz; done'# → ok ok ok -
Apply Terraform
25-lbu/.Fenêtre de terminal cd terraform/25-lbu/terraform initterraform plan # → 12 ressources à créerterraform apply -
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"] -
Tester le passthrough TLS de chaque LBU.
Fenêtre de terminal for ip in $(terraform output -json lbu_public_ips | jq -r '.[]'); doecho "=== $ip ==="curl -ks --max-time 5 https://$ip/healthzcurl -kvs --max-time 5 https://$ip/ 2>&1 | grep "subject:"doneSortie attendue : 3×
ok+ 3 lignessubject: CN=capstone-nginx-{a,b,c}(cert différent par LBU = passthrough confirmé). -
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> -
Configurer le healthcheck.
- Cloudflare → Traffic → Load Balancing → Pools → Create Pool → 3 origins + monitor
/healthzHTTPS. - NS1 → Records → Filter Chains →
upfilter sur monitor. - Route 53 → Health checks → 3 healthchecks, attacher un par record A.
- Cloudflare → Traffic → Load Balancing → Pools → Create Pool → 3 origins + monitor
-
Test bout en bout depuis n'importe où.
Fenêtre de terminal for i in 1 2 3 4 5; docurl -ks https://capstone.example.org/healthzdig +short capstone.example.orgdoneLes 3 IPs doivent apparaître à tour de rôle dans la résolution
dig.
Pièges courants
Section intitulée « Pièges courants »| Symptôme | Cause | Solution |
|---|---|---|
curl: (60) SSL certificate problem: self signed certificate | CA capstone n'est pas dans le store du client | Importer ca.crt dans le store, ou utiliser -k en lab |
curl --resolve retourne le cert d'un autre LBU | Routage cross-AZ activé par erreur | Vérifier backend_vm_ids = [each.value.backend_vm_id] (1 seul backend) |
HTTP 000 répété sur un LBU précis | LBU ou Nginx backend down | oapi-cli ReadLoadBalancers + oapi-cli ReadVms ; vérifier le SG ingress 443 du Nginx |
| TTL DNS plus long qu'attendu | Cache resolver intermédiaire | Forcer un resolver direct : dig @1.1.1.1 capstone.example.org |
Cloudflare healthcheck failed: cert error | Allow insecure: false sur le monitor | Activer Allow insecure: true (cert self-signed accepté) |
| Le LBU ne reçoit pas de trafic | Subnet sans route 0.0.0.0/0 → IGW | Le LBU doit être dans un subnet public (route IGW) — vérifier au Chap 1 |
À retenir
Section intitulée « À retenir »- 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é.