Skip to content

Comprendre Cilium (migration depuis le VPC CNI)

Ce guide explique comment on remplace le CNI d'AWS (VPC CNI) par Cilium sur le cluster EKS, et pourquoi on a fait chaque choix. Le pourquoi condensé est dans l'ADR 019 ; ici on prend le temps de comprendre.

Issue : #80. Cette migration rend la Prefix Delegation (#79, ADR 012) obsolète.


1. C'est quoi un CNI, et pourquoi en changer ?

Le CNI (Container Network Interface) est le composant qui donne une adresse IP à chaque pod et qui fait passer le trafic d'un pod à l'autre. Sans CNI, un node reste NotReady : il ne peut pas faire tourner de pods en réseau.

Sur EKS, le CNI par défaut est le VPC CNI (DaemonSet aws-node) :

  • chaque pod reçoit une vraie IP du VPC, prise sur une ENI (carte réseau) du node ;
  • conséquence : le nombre de pods par node est plafonné par le nombre d'IP que l'ENI peut porter. Sur un t3.medium, c'est ~17 pods par défaut. On a dû activer la Prefix Delegation (#79) pour monter à 110, sinon ArgoCD et l'observabilité ne tenaient pas.

Le VPC CNI marche bien, mais il a des limites qu'on veut dépasser :

Limite VPC CNI Ce que Cilium apporte
Densité de pods liée aux IP d'ENI (bricolage Prefix Delegation) IP des pods hors VPC (overlay) : plus de plafond ENI
NetworkPolicy L3/L4 seulement NetworkPolicy L3/L4 + L7 (HTTP, DNS...) via CiliumNetworkPolicy
Routage Services par kube-proxy (iptables, qui grossit avec le nombre de Services) Routage Services en eBPF (pas d'iptables, ça scale mieux)
Pas d'observabilité réseau native Hubble : qui parle à qui, en direct
Spécifique AWS Portable : même CNI sur EKS, le homelab kubeadm, la platform

eBPF, c'est la techno clé de Cilium : du code exécuté directement dans le noyau Linux pour traiter les paquets, sans repasser par les longues chaînes iptables.


2. Les choix de cette migration

Trois décisions structurantes, prises pour cette mise en place.

a. Datapath : overlay VXLAN

On fait tourner Cilium en mode overlay (routingMode: tunnel, encapsulation VXLAN). Les IP des pods viennent d'un pool privé géré par Cilium (10.42.0.0/16), distinct du CIDR du VPC (10.0.0.0/16), et le trafic inter-node est encapsulé dans des paquets VXLAN.

  • Pourquoi : on se détache complètement des IP du VPC (fini le plafond ENI et la Prefix Delegation), et l'overlay est portable sur n'importe quel cluster.
  • Le pod CIDR 10.42.0.0/16 est choisi différent du VPC 10.0.0.0/16 pour éviter toute collision d'adresses.

b. kube-proxy remplacé par eBPF (kubeProxyReplacement)

Cilium prend en charge le routage des Services lui-même, en eBPF, et on supprime kube-proxy.

  • Pourquoi : c'est le « plein potentiel » de Cilium. Plus d'iptables géantes pour router les ClusterIP, meilleure perf, et c'est un argument technique solide en entretien (eBPF concret).
  • Détail important : une fois kube-proxy parti, l'agent Cilium ne peut plus joindre l'API server via le ClusterIP kubernetes (c'était kube-proxy qui le routait). On lui donne donc l'adresse directe de l'API (k8sServiceHost = endpoint du cluster EKS, k8sServicePort: 443).

c. Observabilité réseau : Hubble + Hubble UI

On active Hubble (le plan d'observabilité de Cilium) et son interface web.

  • Pourquoi : Hubble montre les flux réseau en direct (L3/L4/L7), une service map, les paquets autorisés/refusés par les NetworkPolicy. Ça complète la stack d'observabilité du Sprint 4 (Prometheus/Grafana/Loki).
  • Comme Prometheus, l'UI Hubble reste interne (accès par port-forward), pas exposée publiquement.
  • À surveiller : Hubble consomme de la RAM, et le cluster est un mono-node t3.medium déjà chargé. Requests basses, et on coupe l'UI si le node est sous pression.

3. Le piège du démarrage : l'ordre poule / œuf

C'est le point délicat de la migration, à bien comprendre.

  • Un managed node group EKS exige que ses nodes deviennent Ready dans un certain délai, sinon la création échoue côté Terraform.
  • Or un node n'est Ready que s'il a un CNI. Si on enlevait tout CNI en attendant Cilium, les nodes resteraient NotReady et le node group échouerait.
  • Et ArgoCD ne peut pas installer Cilium : ArgoCD est lui-même un pod, il a besoin d'un CNI pour démarrer. C'est l'œuf qui a besoin de la poule.

La distinction qui résout tout : le composant ≠ l'addon managé

Il faut séparer deux choses qu'on appelle vite « le vpc-cni » :

Ce que c'est Ce qu'on en fait
Le composant vpc-cni Le DaemonSet aws-node qui distribue les IP. EKS l'installe par défaut à la création du cluster, qu'on le déclare ou non. On le garde au boot (il amène les nodes Ready), puis on le supprime après Cilium.
L'addon managé aws_eks_addon.vpc_cni La ressource Terraform qui adopte et configure ce composant (NetworkPolicy, Prefix Delegation). On retire cette gestion managée : vpc-cni revient en mode self-managed par défaut.

Pourquoi retirer la gestion managée plutôt que de garder l'addon ? Parce qu'un addon managé est réconcilié par un contrôleur EKS. Si on supprimait le DaemonSet aws-node alors qu'il est géré en addon, le contrôleur le recréerait aussitôt, et il se battrait avec Cilium. En self-managed, personne ne le recrée : le cutover est propre.

La séquence retenue

1. Le cluster démarre. vpc-cni self-managed (config par défaut, ~17 IP) amène les
   nodes Ready. Seul coredns a besoin d'IP à ce stade : 17 suffisent largement.
2. L'amorce Ansible installe Cilium EN PREMIER (avant Envoy Gateway, ESO, ArgoCD...).
3. Cutover :
   - on supprime le DaemonSet aws-node (vpc-cni)      -> Cilium devient le seul CNI
   - on supprime le DaemonSet kube-proxy              -> eBPF prend le relais
   - on redémarre coredns                             -> il reprend une IP overlay Cilium
4. Le reste de l'amorce (Envoy GW, cert-manager, ESO, ExternalDNS, ArgoCD) s'installe,
   cette fois sur Cilium.

C'est exactement le motif « Ansible amorce → ArgoCD prend le relais » déjà utilisé pour le reste de la plateforme (voir Observabilité) : Cilium est une brique de socle, posée par Ansible, pas par ArgoCD (sinon, retour au problème de l'œuf et de la poule).

Piège à câbler : kube-proxy laisse des règles iptables derrière lui. Après l'avoir supprimé, il faut purger ces règles résiduelles (sinon comportement de routage incohérent jusqu'au reboot du node).


4. Ce qui change concrètement dans le repo

Fichier Avant Après
terraform/modules/eks/main.tf addon managé vpc-cni (NetworkPolicy + Prefix Delegation) ; node group depends_on cet addon plus d'addon vpc-cni (gestion managée retirée) ; depends_on allégé
launch template maxPods: 110 justifié par la Prefix Delegation conservé, désormais servi par l'overlay Cilium
AmazonEKS_CNI_Policy (rôle node) requis par le VPC CNI conservé : le vpc-cni self-managed s'en sert encore pendant l'amorce
ansible/bootstrap.yml amorce sans CNI (VPC CNI déjà là) nouveau bloc Cilium en tête + cutover

5. NetworkPolicy sous Cilium

Le projet a déjà une NetworkPolicy standard allow-fastapi (#55). Bonne nouvelle : Cilium applique les NetworkPolicy Kubernetes standard, donc elle continue de fonctionner sans modification. On la re-teste quand même en live (un test positif : trafic autorisé qui passe ; un test négatif : trafic interdit qui est bloqué), parce qu'on ne suppose jamais qu'une règle de sécurité marche sans l'avoir vue marcher.

En bonus, Cilium ajoute les CiliumNetworkPolicy, qui savent filtrer au niveau L7 (par exemple : autoriser GET /healthz mais pas POST /admin). On ne s'en sert pas encore, mais c'est une porte ouverte.


5bis. Le piège des webhooks d'admission (vécu en live, INC-058)

C'est le piège le plus subtil de la migration, et il ne se voit qu'en live.

Sur EKS, le control plane (l'API server) est managé par AWS : il tourne dans une infra qu'on ne contrôle pas, et ses cartes réseau (ENI) vivent dans le VPC (10.0.x). Surtout, il n'est pas un node Cilium : il ne participe pas à l'overlay VXLAN. Conséquence directe : il ne sait pas router vers une IP de pod overlay (10.42.x).

Or certains composants installent un webhook d'admission : un petit serveur HTTPS, hébergé dans un pod, que l'API server appelle à chaque création/modification de ressource pour valider ou muter (cert-manager valide ses CRDs, ESO valide ses ExternalSecret/SecretStore). C'est un appel dans le sens inverse de d'habitude : ce n'est pas le pod qui appelle l'API, c'est l'API qui appelle le pod.

Sous overlay, ce pod a une IP 10.42.x → l'API server managé ne sait pas l'atteindre → l'appel échoue avec Address is not allowed. Et comme ces webhooks sont en failurePolicy: Fail (« en cas d'échec, je refuse l'opération »), ça bloque tout : l'amorce plante sur l'installation de cert-manager (son startupapicheck n'atteint pas le webhook) puis sur la création du ClusterIssuer.

Le fix : hostNetwork: true sur les webhooks concernés. En hostNetwork, le pod ne prend plus une IP overlay mais l'IP du node (10.0.x, dans le VPC) — que le control plane sait router. Le webhook redevient joignable.

Deux subtilités câblées dans bootstrap.yml :

  • Le port. En hostNetwork, le webhook écoute directement sur le réseau du node. Son port par défaut (10250) est déjà pris par le kubelet → on le décale. cert-manager passe sur 10260, ESO sur 10261.
  • Mono-node = deux ports distincts. Notre cluster a un seul node, donc les deux webhooks (cert-manager et ESO) bindent le même réseau. S'ils partageaient un port, l'un des deux ne démarrerait pas. D'où 10260 et 10261.

Tous les webhooks ne sont pas touchés : celui d'Envoy Gateway est en failurePolicy: Ignore (« en cas d'échec, je laisse passer »), donc s'il est injoignable c'est un no-op, rien à corriger. Et les webhooks managés par EKS (vpc-resource-*, pod-identity-webhook) ne sont pas concernés (hors overlay).

À retenir : dès qu'un composant doit être joint par l'API server (webhook d'admission, de conversion, agrégation d'API), il faut qu'il soit joignable depuis le VPC sous overlay → hostNetwork. Ce n'est pas lié à la version de Kubernetes, c'est du routage réseau pur.

Deuxième cas concret : metrics-server (INC-061). Ce n'est pas un webhook mais un APIService agrégé (v1beta1.metrics.k8s.io) : l'API server délègue les requêtes /apis/metrics.k8s.io au pod metrics-server, donc là encore c'est l'API server qui appelle le pod. Même racine, même symptôme : sous overlay le pod a une IP 10.42.x, l'API server managé ne sait pas la router → Address is not allowed, l'APIService passe Unavailable. Conséquence vicieuse : kubectl top ET le HPA fastapi (#82) tombent en silence (le HPA n'a plus de métriques), et ça a duré depuis la migration #80 sans erreur visible (le HPA n'alerte pas, il arrête juste de scaler). Même fix : hostNetwork: true + port décalé sur 10262 (10250 kubelet, 10260 cert-manager, 10261 ESO, donc 10262 pour metrics-server). La règle « joint par l'API server → hostNetwork » couvre donc webhooks et APIService agrégés.


6. Comment vérifier que tout marche (en live)

  • cilium status --wait : l'agent et l'opérateur sont sains.
  • kubectl get pods -A : tout est Running, plus aucun aws-node ni kube-proxy.
  • coredns tourne avec une IP 10.42.x (donc bien sur l'overlay Cilium).
  • La résolution DNS interne marche (preuve que coredns est joignable sans kube-proxy).
  • Un curl vers un Service ClusterIP répond (preuve que kubeProxyReplacement route bien).
  • NetworkPolicy allow-fastapi : test positif + négatif.
  • Webhooks joignables (INC-058) : l'amorce passe Install cert-manager et la création du ClusterIssuer sans Address is not allowed, l'ExternalSecret se synchronise (SecretSynced). Preuve que les webhooks hostNetwork sont bien atteints par l'API server.
  • metrics-server joignable (INC-061) : kubectl top nodes/kubectl top pods répondent (APIService metrics.k8s.io Available) et le HPA fastapi montre des métriques (kubectl get hpa -n fastapi → colonnes TARGETS chiffrées, plus <unknown>). Preuve que l'APIService agrégé hostNetwork est atteint par l'API server.
  • cilium hubble port-forward puis l'UI : on voit les flux.
  • Bout en bout : https://api.devopsyouss.com répond 200, ArgoCD Synced/Healthy, la stack d'observabilité est debout.

Le garde-fou de compatibilité EKS / Cilium

Le contexte, avant le détail

Deux versions gouvernent le réseau de ce cluster, et elles vivent dans deux fichiers différents, gérés par deux outils différents :

Quoi Valeur
Version de Kubernetes terraform/ephemeral/variables.tf, eks_cluster_version 1.35
Version de Cilium ansible/bootstrap.yml, cilium_version 1.20.1

Cilium n'est pas un composant accessoire ici : il remplace kube-proxy (kubeProxyReplacement, voir plus haut). Si Cilium ne démarre pas, le cluster n'a pas de réseau du tout. Pas un service dégradé, aucun trafic.

Pourquoi un contrôle, et pas seulement un avertissement

Le merge d'une montée de version et le montage qui l'applique sont séparés de plusieurs semaines. Un terraform apply ne vit que dans infra-start, sous $ACTION == "start" : merger une MR de version ne monte rien.

  jour J      MR « EKS 1.35 -> 1.36 » mergée
              personne ne monte de cluster ce jour-là, rien ne se passe
                          │
                          │  trois semaines
                          ▼
  jour J+21   infra-start, montage from scratch
              Cilium 1.19.5 s'installe sur Kubernetes 1.36
              -> cluster sans réseau, et plus personne ne se souvient de la MR

Un avertissement voyageait déjà dans le corps des MR Renovate (prBodyNotes de renovate.json). C'est un garde-fou documentaire, pas un contrôle : il suppose qu'un humain lise au bon moment, et il ne s'exprime nulle part au montage, qui est le moment où le risque se réalise. C'est le motif de l'ADR 031 : prévenir n'est pas empêcher.

Ce que le contrôle vérifie

scripts/check-version-compat.sh lit les deux fichiers et refuse si la version de Kubernetes sort de l'intervalle testé e2e par la version de Cilium en place.

L'intervalle est déclaré dans ansible/bootstrap.yml, juste sous la valeur qu'il borne :

cilium_version: "1.20.1"
cilium_k8s_range_for: "1.20"   # la mineure pour laquelle les bornes ont été relevées
cilium_k8s_min: "1.33"
cilium_k8s_max: "1.36"

cilium_k8s_range_for est l'ancrage (#212). Il dit à quelle mineure de Cilium les deux bornes se rapportent. Le contrôle refuse si cilium_version quitte cette mineure sans que les bornes aient été relevées. Détail et raison dans la section Qui surveille la version de Cilium plus bas.

Source, à revérifier à chaque montée de Cilium : https://docs.cilium.io/en/v1.20/network/kubernetes/compatibility/

Cilium Kubernetes testés e2e Vérifié le
1.19 1.32 → 1.35 2026-09-07
1.20 (en place) 1.33 → 1.36 2026-09-07

⚠️ La borne basse monte aussi. Cilium 1.20 abandonne Kubernetes 1.32. Monter Cilium sans regarder cette borne peut larguer le cluster par le bas, pas seulement par le haut. C'est pourquoi le contrôle vérifie un intervalle et non un plafond.

Depuis #213, le même script porte une deuxième section, sans rapport avec Cilium : le skew de kubectl face à l'API server. Elle est décrite dans Version du cluster.

Il vérifie aussi que les deux fichiers Terraform qui déclarent eks_cluster_version portent la même valeur. C'est terraform/ephemeral/variables.tf qui gouverne les montages, mais une divergence signale déjà une version bumpée à un seul endroit.

Où il s'exécute, et pourquoi à ces deux endroits

Point d'appel Fichier Ce qu'il arrête
Job version-compat, sur MR .gitlab-ci.yml La MR qui créerait la dette
before_script de infra-start .gitlab-ci-infra.yml Le montage, avant toute ressource facturée

Le second couvre deux trous que le premier laisse : un changement poussé directement sur develop, et une borne mise à jour après le merge, qui rend incompatible un dépôt déjà mergé.

⚠️ Piège si l'on veut déplacer ce contrôle. Il est volontairement dans le before_script d'infra-start, et non dans un stage placé avant. Les rules d'infra-start portent when: always, qui signifie « exécute ce job même si un job d'un stage précédent a échoué ». Un garde-fou placé en amont serait rouge et sans effet : terraform apply tournerait quand même.

Quand une combinaison est refusée

Le message nomme les deux versions et les fichiers à corriger :

REFUS : Kubernetes 1.36 dépasse la borne haute de Cilium 1.19.5 (1.35).
Cilium remplace kube-proxy dans ce cluster : monter cette combinaison peut
laisser le cluster SANS RESEAU DU TOUT.

Les deux versions n'ont pas à bouger ensemble. Les intervalles de deux versions successives de Cilium se recouvrent, ce qui laisse toujours un chemin en deux étapes.

C'est ce qui a été fait pour passer à Cilium 1.20 (#211) :

  1. Cilium 1.19.5 → 1.20.1, le cluster restant en 1.35. Combinaison testée.
  2. Puis EKS 1.35 → 1.36, quand cette MR sera prise. Combinaison testée elle aussi.

Deux changements séparés, chacun dans un intervalle testé. Si un montage casse, on sait lequel des deux est en cause.

⚠️ Le rendu Helm ne dit pas si une option a survécu à la montée de version. Mesuré le 2026-09-07 : helm template accepte sans broncher une option supprimée de la version visée, et cette option figure même encore dans helm show values. Les deux tests sont donc inutilisables. Le seul qui tranche est celui de l'effet : rendre le chart dans les deux versions avec les valeurs du dépôt, et comparer ce qui atterrit dans les objets Kubernetes. Sa discrimination se contrôle en renommant volontairement une clé, ici kubeProxyReplacement, dont le ConfigMap rendu passe alors de true à false.


Qui surveille la version de Cilium

Le contexte, avant le détail

Renovate est le robot qui ouvre une MR quand une dépendance du dépôt a une nouvelle version. Il lit les images Docker, les providers Terraform, les requirements Python, et depuis #204 la version d'EKS.

Jusqu'au 2026-09-07, il ne lisait pas Cilium. Deux raisons cumulatives : enabledManagers ne contenait aucun manager Ansible, et cilium_version ne portait aucune annotation # renovate:.

Le seul rappel était un commentaire dans le fichier, « revérifier la stable avant chaque session ». Entre le 2026-06-18 et le 2026-09-07, personne n'y a pensé : la valeur est restée sur une stable vieille de près de trois mois, et c'est en instruisant une autre MR qu'on l'a vu.

Le piège : brancher Renovate seul aurait été une régression

Le contrôle décrit plus haut compare Kubernetes aux bornes. Il ne vérifiait jamais que ces bornes correspondent à la version de Cilium installée.

  MR Renovate « Cilium 1.20.1 -> 1.21.0 »
  elle ne touche que cilium_version
                    │
                    ▼
  version-compat lit les bornes : 1.33 à 1.36
  Kubernetes vaut 1.35, c'est dans l'intervalle
                    │
                    ▼
  ✅ VERT — alors que 1.33 à 1.36 est l'intervalle de Cilium 1.20,
             pas celui de 1.21, que personne n'a relevé

C'est un critère qui ne discrimine pas : vert et faux en même temps. Brancher Renovate sans traiter ce point aurait produit régulièrement des MR dont le vert ne veut rien dire, donc rendu le dépôt moins sûr qu'avant.

Ce qui a été retenu

Les deux moitiés ont été livrées ensemble.

Moitié Mécanisme
Détecter une nouvelle version Annotation # renovate: sur cilium_version, lue par un customManagers regex de renovate.json qui porte sur tous les YAML d'ansible/
Empêcher un bump aux bornes périmées cilium_k8s_range_for, vérifié par scripts/check-version-compat.sh

Le motif regex est séparé de celui des fichiers .tf, et non fusionné avec lui : la ligne de valeur n'a pas la même forme (clé: "valeur" en YAML contre default = "valeur" en HCL), et le datasource helm exige un registryUrl que l'annotation Terraform ne porte pas.

Comme pour EKS, le motif porte sur tous les YAML du dossier et non sur une liste de fichiers : une liste se périme en silence dès qu'on annote ailleurs.

Ce que l'ancrage prouve, et ce qu'il ne prouve pas

Il prouve que les bornes ont été rouvertes pour cette mineure. Un bump de Cilium seul arrive rouge, par construction, et le reste tant que les trois valeurs ne sont pas mises à jour ensemble.

Il ne prouve pas que les bornes sont justes. Aucune source de compatibilité n'est lisible depuis le job, qui tourne sans réseau utile. Changer l'ancrage sans ouvrir la page Cilium passe le contrôle.

C'est le même niveau de garantie que le contrôle de #210 : un ralentisseur qui oblige à regarder au bon moment, pas une preuve. La différence avec le commentaire qu'il remplace est qu'il s'exprime, et qu'il bloque.

⚠️ Un bump de patch ne déclenche rien (1.20.1 → 1.20.2). C'est voulu : les bornes ne bougent pas d'un patch à l'autre, l'ancrage porte sur la mineure.


En résumé

On garde le composant vpc-cni le temps d'amorcer le cluster, on installe Cilium en overlay dès le début de l'amorce Ansible, on bascule (suppression d'aws-node et de kube-proxy), puis tout le reste tourne sur Cilium. On gagne en densité, en perf (eBPF), en observabilité (Hubble) et en portabilité, et on se débarrasse du bricolage Prefix Delegation.

Voir l'ADR 019 pour la décision et les alternatives écartées.