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 ?
- 2. Les choix de cette migration
- 3. Le piège du démarrage : l'ordre poule / œuf
- 4. Ce qui change concrètement dans le repo
- 5. NetworkPolicy sous Cilium
- 5bis. Le piège des webhooks d'admission (vécu en live, INC-058)
- 6. Comment vérifier que tout marche (en live)
- Le garde-fou de compatibilité EKS / Cilium
- Qui surveille la version de Cilium
- En résumé
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/16est choisi différent du VPC10.0.0.0/16pour é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.mediumdé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
Readydans un certain délai, sinon la création échoue côté Terraform. - Or un node n'est
Readyque s'il a un CNI. Si on enlevait tout CNI en attendant Cilium, les nodes resteraientNotReadyet 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 sur10260, ESO sur10261. - 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ù
10260et10261.
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 estRunning, plus aucunaws-nodenikube-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
curlvers un Service ClusterIP répond (preuve quekubeProxyReplacementroute bien). - NetworkPolicy
allow-fastapi: test positif + négatif. - Webhooks joignables (INC-058) : l'amorce passe
Install cert-manageret la création duClusterIssuersansAddress is not allowed, l'ExternalSecretse synchronise (SecretSynced). Preuve que les webhookshostNetworksont bien atteints par l'API server. - metrics-server joignable (INC-061) :
kubectl top nodes/kubectl top podsrépondent (APIServicemetrics.k8s.ioAvailable) et le HPA fastapi montre des métriques (kubectl get hpa -n fastapi→ colonnesTARGETSchiffrées, plus<unknown>). Preuve que l'APIService agrégéhostNetworkest atteint par l'API server. cilium hubble port-forwardpuis l'UI : on voit les flux.- Bout en bout :
https://api.devopsyouss.comrépond200, ArgoCDSynced/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 | Où | 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) :
- Cilium 1.19.5 → 1.20.1, le cluster restant en 1.35. Combinaison testée.
- 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.