Skip to content

ADR 017 — Load balancer d'exposition : NLB plutôt que Classic ELB (2026-06-14)

Statut

Accepté (2026-06-14). Issue #70. Validé live le 2026-06-16 (console AWS : 1 seul load balancer, de type Network, internet-facing, aucun Classic ELB).

Contexte

Le Service LoadBalancer provisionné par Envoy Gateway pour le Gateway (point d'entrée public, ADR 010) crée par défaut un Classic ELB (CLB) sur AWS. Le CLB est déprécié par AWS : plus de nouvelles fonctionnalités, pas de preserve client IP natif, pas de mode IP target, latence supérieure à un NLB.

Décision 1 — NLB plutôt que CLB

On provisionne un Network Load Balancer (NLB, L4) à la place du CLB, via une ressource EnvoyProxy (gateway.envoyproxy.io/v1alpha1) qui annote le Service Envoy et est référencée par le GatewayClass (parametersRef) :

spec:
  provider:
    kubernetes:
      envoyService:
        annotations:
          service.beta.kubernetes.io/aws-load-balancer-type: "nlb"

Le GatewayClass ne se personnalise pas directement : Envoy Gateway délègue la config d'infrastructure à un EnvoyProxy (cluster-level via parametersRef), qui doit vivre dans le namespace du contrôleur (envoy-gateway-system).

Décision 2 — NLB, pas ALB

L'ALB (L7) serait redondant avec Envoy Gateway, qui fait déjà tout le routing L7 (HTTPRoute, terminaison TLS via cert-manager, hostnames). Un ALB en amont ajouterait une couche L7 inutile et du lock-in AWS. Le NLB (L4, TCP pass-through) est la bonne brique : il transporte le trafic jusqu'à Envoy, qui gère le L7 en aval. Cohérent avec ADR 010 (exposition portable, pas de dépendance à un Ingress controller AWS).

Décision 3 — Annotation in-tree, pas AWS Load Balancer Controller

Deux façons d'obtenir un NLB :

Voie Annotation Prérequis
In-tree (retenue) aws-load-balancer-type: "nlb" Aucun (cloud provider AWS intégré)
AWS LB Controller aws-load-balancer-type: "external" + nlb-target-type: "ip" Installer l'AWS LB Controller

On retient l'in-tree : le projet n'installe pas l'AWS Load Balancer Controller (Envoy assure le L7, on ne veut pas d'un second contrôleur LB). Le NLB en mode instance couvre le besoin. Tradeoff assumé : pas de mode IP target (qui éviterait le double saut via NodePort). Migration possible vers l'AWS LB Controller + target-type: ip si le besoin de preserve client IP / IP target apparaît (évolution).

Pièges / Conséquences

  • Cluster éphémère = pas de migration in-place : le NLB est créé from-scratch à chaque aws-start avec le Gateway. On évite le piège du changement de type d'ELB sur un Service existant (AWS recrée le LB, l'ancien peut rester orphelin). L'ancien CLB n'existe que le temps d'une session ; sur un cluster neuf, seul le NLB est créé.
  • Teardown inchangé (anti-INC-016) : le NLB est supprimé quand le Gateway est supprimé (Envoy Gateway supprime son Service), exactement comme le CLB aujourd'hui. L'EnvoyProxy n'est que de la configuration, il n'ajoute aucune ressource AWS à nettoyer.
  • ExternalDNS : le CNAME api.devopsyouss.com suit le hostname du nouveau LB automatiquement (source gateway-httproute), aucune action manuelle.

Décision 4 — Le listener :80 ne sert qu'à rediriger (#147, 2026-07-26)

Le défaut corrigé. Le Gateway expose deux listeners, http:80 et https:443, tous deux en allowedRoutes: from: All. Or une HTTPRoute sans sectionName s'attache à tous les listeners que le Gateway lui ouvre — et aucune des 4 routes du repo n'en portait. Les quatre services publics (api, app, grafana, argocd) répondaient donc aussi bien en clair qu'en TLS. Constaté en live le 2026-07-26 : curl http://api.devopsyouss.com/healthz/ready renvoyait 200.

Ce n'est pas un détail de configuration. POST /login transporte identifiants et JWT (app/routers/auth.py), et la CLI argocd transporte un token de session : sur http://, les deux sont lisibles par un observateur du réseau (CWE-319). Le TLS fonctionnait, rien n'obligeait simplement à l'emprunter.

La décision, en deux pièces qui ne jouent pas le même rôle :

  1. sectionName: https sur les 4 routes applicatives. C'est ceci qui ferme le clair : les routes quittent le listener :80.
  2. Une HTTPRoute de redirection sur sectionName: http (k8s/platform/httproute-https-redirect.yaml), sans hostnames donc attrape-tout, avec un filtre RequestRedirect en 301. Elle ne ferme rien, elle offre une sortie propre plutôt qu'un 404 sec, et le 301 étant mis en cache par le navigateur, les visites suivantes partent directement en HTTPS.

La redirection n'a ni hostname ni backend : un RequestRedirect répond au niveau du Gateway, la requête n'atteint jamais un pod. Elle peut donc vivre dans le namespace plateforme fastapi sans qu'aucun Service n'y tourne.

Ownership. Elle est posée par le bootstrap Ansible avec le Gateway, et non en GitOps comme les routes applicatives (ADR 010) : elle décrit le comportement d'un listener, elle appartient à la plomberie du Gateway, pas à une application.

Le point de vigilance. Le :80 ne doit plus servir à rien d'autre. Les certificats passent par DNS-01 (wildcard oblige), donc aucun challenge HTTP-01 à préserver aujourd'hui. Si un ClusterIssuer HTTP-01 était ajouté un jour, cette route attrape-tout l'aveuglerait : il faudrait exclure /.well-known/acme-challenge/ avant de le mettre en service.

Validation

  • Hors infra : YAML conforme à l'API Envoy Gateway (EnvoyProxy + parametersRef).
  • Live (2026-06-16, critères de l'issue #70) : console AWS = 1 NLB de type Network (pas de CLB), curl https://api.devopsyouss.com/healthz/ready → 200, aucun CLB orphelin. ✅

Date : 2026-06-14 Sprint : 4 Issue : #70