0020 — Exposition réseau tout-Cilium (kube-proxy replacement + LB-IPAM + L2 + Gateway API)
Contexte
Section intitulée « Contexte »L’exposition réseau du cluster reste à construire (Phase 1 du plan pipeline de
collaborations, dépôt applicatif). Le cluster est bare-metal, kubeadm, 4 nœuds
non-HA (control-plane unique = SPOF assumé, cf.
ADR 0002 et
ADR 0009). Cilium 1.19.4 est déjà le CNI
(bootstrap/cni.sh), durci avec chiffrement WireGuard
pod-to-pod + Hubble relay/CLI (ADR 0019).
Stockage Rook-Ceph (ADR 0018). Réseau prod
10.0.0.0/22 (isolé) ; banc Vagrant 192.168.67.0/24.
Sur bare-metal, un Service type=LoadBalancer n’a pas de provider cloud : il
faut (a) allouer des IP virtuelles depuis un pool et (b) annoncer ces IP
sur le LAN pour qu’elles soient joignables. Il faut aussi un point d’entrée
HTTP(S) en bordure (routage par host/path, terminaison TLS).
« Bordure » = bordure du réseau privé, pas Internet. Le cluster de production n’est pas accessible depuis l’extérieur (cohérent ADR 0003 : réseau
10.0.0.0/22isolé). Les IP du pool sont annoncées sur le LAN interne ; le Gateway est le point d’entrée des clients internes (LAN / VPN), jamais d’Internet. Conséquence directe pour le TLS de bordure (étape cert-manager suivante) : ACME/Let’s Encrypt est exclu (challenge injoignable depuis l’extérieur) → une CA interne sera utilisée.
Le plan, calqué sur le benchmark DataOps, prévoyait deux briques OSS externes :
étape 1.1 = MetalLB (IPAddressPool + L2Advertisement) puis étape 1.2 =
ingress-nginx (contrôleur d’ingress exposé via une IP MetalLB). Le plan note
d’ailleurs « Cilium ne fait pas le LB de service externe ici » — hypothèse que
cet ADR remet en cause : Cilium ≥ 1.13/1.14 sait désormais faire LB-IPAM,
annonces L2 et Gateway API nativement, et le RUNBOOK décrit déjà
kubeProxyReplacement comme option (section « Optionnel —
kubeProxyReplacement »).
La question : faut-il ajouter MetalLB + ingress-nginx (deux composants externes, deux datapaths supplémentaires) alors que le CNI Cilium en place couvre maintenant ces trois fonctions (allocation d’IP, annonce L2, passerelle L7) au sein du datapath eBPF déjà déployé ?
Décision
Section intitulée « Décision »Amendé par ADR 0071 : le Gateway est exposé en hostNetwork (
gatewayAPI.hostNetwork.enabled=true) — l’Envoy bind 80/443 sur l’IP du nœud, sans LB-IPAM ni L2. C’est le mode d’exposition unique (exposition.mode: gateway | none;lb-ipamethostportsont des alias historiques degateway), défaut partout (banc Lima comme VM publique). Le chemin LB-IPAM + L2 décrit ci-dessous (IP virtuelle annoncée sur le LAN) est rétrogradé en option de prod (CILIUM_LB_IPAM_ENABLED=1, plage négociée avec l’admin réseau), il n’est plus armé par défaut. NodePort reste écarté.
Adopter une exposition réseau tout-Cilium, en remplacement de MetalLB (étape
1.1) et d’ingress-nginx (étape 1.2). Quatre fonctions Cilium sont activées,
appliquées par bootstrap/cni.sh (idempotent à
l’install et à l’upgrade, comme le durcissement de l’ADR 0019), les CRs
étant versionnés sous platform/cilium-expo/.
1. kubeProxyReplacement — remplacement de kube-proxy par eBPF
Section intitulée « 1. kubeProxyReplacement — remplacement de kube-proxy par eBPF »--set kubeProxyReplacement=true (+ k8sServiceHost=cluster-api,
k8sServicePort=6443, obligatoires sans kube-proxy : l’agent ne peut plus
joindre l’API server via la ClusterIP kubernetes.default). Le routage des
Services (ClusterIP, NodePort, LoadBalancer) passe d’iptables au datapath
eBPF. C’est le pré-requis du LB-IPAM, des annonces L2 et du Gateway API.
Impact transverse assumé : ce réglage change le datapath de tout le
cluster, pas seulement la bordure ; à valider en priorité sur banc. En 1.19 le
flag est un booléen (true/false) — les anciennes valeurs
strict/partial/probe n’existent plus — et il active automatiquement
NodePort/HostPort/ExternalIPs (les flags --enable-node-port, etc. sont retirés
en 1.19 : ne pas les positionner).
Retrait de kube-proxy : ordre obligatoire — Cilium converge d’abord
(KubeProxyReplacement: True), puis on retire le DaemonSet/ConfigMap
kube-proxy et on purge ses règles iptables résiduelles par nœud. Durabilité :
skipPhases: [addon/kube-proxy] dans l’InitConfiguration kubeadm (v1beta4)
évite que kubeadm init/upgrade ne les recrée.
2. LB-IPAM — allocation d’IP LoadBalancer (remplace le pool MetalLB)
Section intitulée « 2. LB-IPAM — allocation d’IP LoadBalancer (remplace le pool MetalLB) »CiliumLoadBalancerIPPool (cilium.io/v2, promu en 1.19 — v2alpha1
déprécié) versionné sous platform/cilium-expo/. LB-IPAM est actif dès qu’un
pool existe (pas de flag d’activation). Banc : pool 192.168.67.240-250.
Prod (10.0.0.0/22) : TODO documenté, plage à fixer avec l’admin
réseau — aucune IP prod attribuée à l’aveugle (collision sur un /22 partagé
hors de notre maîtrise).
3. L2 Announcements — annonce des IP sur le LAN (remplace L2Advertisement)
Section intitulée « 3. L2 Announcements — annonce des IP sur le LAN (remplace L2Advertisement) »CiliumL2AnnouncementPolicy (cilium.io/v2alpha1 — ce CRD reste en alpha
en 1.19, à ne pas confondre avec le pool en v2) +
--set l2announcements.enabled=true. Un nœud élu (lease) répond aux ARP pour
les IP du pool. Le réglage k8sClientRateLimit.qps/burst est relevé : L2
crée un Lease par Service LoadBalancer renouvelé en continu, et les défauts
(5–10 QPS) sont vite saturés. Cadrage non-HA honnête : L2 fournit du
failover d’IP (réélection si le nœud annonceur tombe), PAS de la
répartition de charge multi-nœuds — tout le trafic d’une IP transite par un seul
nœud à la fois. Cohérent avec le SPOF déjà assumé (ADR 0002) ; on ne survend pas
une HA inexistante. Statut amont : les L2 Announcements sont en beta en
1.19.
4. Gateway API — bordure L7 (remplace ingress-nginx)
Section intitulée « 4. Gateway API — bordure L7 (remplace ingress-nginx) »--set gatewayAPI.enabled=true (active enable-envoy-config ; l7Proxy=true
est déjà le défaut), CRDs Gateway API v1.4.1 pré-installées (Cilium ne les
embarque pas), GatewayClass/Gateway (gateway.networking.k8s.io/v1)
versionnés. controllerName = io.cilium/gateway-controller. Le Gateway fait
créer un Service type=LoadBalancer (servi par LB-IPAM + L2 → boucle
tout-Cilium) ; routage host/path via HTTPRoute et terminaison TLS via un
listener HTTPS (mode: Terminate + certificateRefs vers un Secret).
externalTrafficPolicy reste Cluster (et non Local) : en L2, Local
droppe le trafic si le nœud annonceur n’héberge pas d’endpoint local.
Pourquoi cette déviation. Le plan et le benchmark DataOps nommaient MetalLB + ingress-nginx. C’est une déviation assumée : maximiser Cilium déjà en place plutôt que d’empiler deux composants externes. Bénéfices structurants : moins de composants à opérer/patcher/superviser ; datapath eBPF unifié (CNI + service-routing + LB + bordure dans un seul plan de données, au lieu de Cilium + kube-proxy + MetalLB speaker + nginx) ; observabilité L7 Hubble native sur le trafic de bordure.
Accepted (2026-06-02).
Conséquences
Section intitulée « Conséquences »Bénéfices.
- Deux composants externes en moins (MetalLB + ingress-nginx) : surface d’opération, de mise à jour et de supervision réduite, une seule matrice de versions à suivre (la ligne Cilium).
- Datapath eBPF unifié : CNI, chiffrement WireGuard (ADR 0019), service-routing (kube-proxy supprimé), LB-IPAM, annonce L2 et bordure L7 dans un seul plan de données.
- Observabilité L7 de bordure : le trafic traversant le Gateway est visible
via
hubble observe, dans la continuité de l’ADR 0019. - Tout est tracé et convergent :
--setet CRs appliqués parcni.shà l’install et à l’upgrade ; un cluster reconverge en rejouant le script.
Prix à payer.
- Déviation du plan écrit et du benchmark : la fiche Phase 1 (étapes 1.1/1.2) nommait MetalLB + ingress-nginx, deux briques OSS éprouvées. On s’écarte d’un chemin balisé pour parier sur la consolidation Cilium ; les étapes 1.1/1.2 du plan sont à réécrire en conséquence.
kubeProxyReplacementimpacte tout le cluster : bascule du service-routing de tous les Services, pas un changement de bordure isolé. Une régression touche l’ensemble des charges.- Maturité amont inégale : L2 Announcements en beta (1.19), Gateway API d’implémentation récente (bugs connus sur certains setups 1.19).
- Pas de répartition de charge : L2 = failover d’IP mono-nœud, conforme au non-HA (ADR 0002/0009), à ne pas confondre avec de la HA.
Garde-fous.
- Validation banc multi-node obligatoire avant prod : un
Service type=LoadBalancerobtient une IP du pool192.168.67.240-250, joignable en ARP/L2 ; uneHTTPRouteroute vers un pod echo ; le failover d’IP fonctionne à l’arrêt du nœud annonceur ; aucune régression de service-routing aprèskubeProxyReplacement(CephHEALTH_OK, CoreDNS, ClusterIP applicatifs). k8sServiceHost=cluster-apià valider : le nom est résolu via/etc/hostsdu nœud (rôlek8s-install) ; les podscilium-agentétant enhostNetwork, ils utilisent ce resolver — vérifier sur banc avant de retirer kube-proxy. Repli sûr documenté : IP du control-plane en dur, ouauto.- IP prod non attribuée à l’aveugle : la plage LB-IPAM dans
10.0.0.0/22reste TODO explicite jusqu’à arbitrage avec l’admin réseau. - Exception drift tracée :
bootstrap/state.sh« Couche 7b — Exposition réseau » (audit P6 #25) flagge toutService type=LoadBalancercomme DRIFT (horskubernetes-dashboard). LeServicedu Gateway de bordure y est ajouté en exception nommée. Le principe #25 reste intact : services applicatifs en ClusterIP, exposition uniquement par la bordure Gateway. rollout restart+ vérification post-upgrade : comme pour WireGuard (ADR 0019),cilium upgradene suffit pas à armer un réglage ;cni.shforce le rollout des agents et échoue visiblement si le datapath attendu n’est pas réellement actif.- NetworkPolicies default-deny préservées (ADR 0019) : ouvrir explicitement les flux vers/depuis le Gateway, ne pas relâcher le default-deny.
Alternatives écartées
Section intitulée « Alternatives écartées »MetalLB + ingress-nginx (le plan d’origine, étapes 1.1/1.2). Solution OSS éprouvée, alignée sur le benchmark. La recherche associée avait abouti (MetalLB v0.16.1, mode L2 pur, images épinglées par digest — digests vérifiés byte-for-byte contre quay.io). Écartée parce qu’elle ajoute deux composants externes (MetalLB speaker/controller + contrôleur nginx) avec leurs datapaths, cycles de version et surfaces à superviser, alors que Cilium — déjà CNI et déjà durci (ADR 0019) — couvre nativement LB-IPAM, annonce L2 et bordure L7. La consolidation l’emporte sur la familiarité, au prix d’une déviation assumée.
Hybride MetalLB (LB-IPAM/L2) + Cilium Gateway API (bordure). Garderait
MetalLB juste pour l’IP LoadBalancer. Écarté : si l’on bascule de toute façon
sur Gateway API + kubeProxyReplacement, LB-IPAM + L2 Cilium viennent « dans la
boîte » — conserver MetalLB en parallèle maintiendrait un composant externe pour
un gain nul, et deux mécanismes d’annonce L2 sur le même LAN (double ARP →
flap MAC/IP).
Garder kube-proxy (iptables) + MetalLB. Éviterait le risque transverse de
kubeProxyReplacement. Écarté : LB-IPAM et annonces L2 Cilium requièrent
kubeProxyReplacement ; le conserver enfermerait dans MetalLB et priverait du
datapath eBPF unifié — l’inverse de l’objectif.
NodePort / externalIPs bruts. Ports hauts non stables, pas d’IP stable,
pas de routage host/path ni de terminaison TLS en bordure, et contredit
frontalement le principe #25 (services applicatifs non exposés). Régression.
Cilium Ingress Controller (API Ingress historique) plutôt que Gateway API.
ingressController.enabled=true reste possible mais l’API Ingress est figée ;
Gateway API est son successeur (routage et TLS plus expressifs, HTTPRoute).
Retenu : Gateway API. Host firewall Cilium non activé : éviterait l’advisory
GHSA-5r23-prx4-mqg3 (host-policy mal appliquée avec WireGuard) — on conserve
WireGuard pod-to-pod + NetworkPolicies pod, sans host-policy à risque.