Retour d'expérience Cloud-Native Kubernetes RKE2 Cilium Rancher

Quand Cilium envoie le VXLAN sur la mauvaise interface : retour d’expérience sur un cluster RKE2 multi-NIC

Aimen E. DevOps and Principal Cloud Architect

Dans ce retour d’expérience, nous revenons sur un incident discret mais bloquant : dans un cluster RKE2 multi-NIC, le trafic VXLAN passait par le réseau d’administration au lieu du réseau applicatif, simplement parce que le nœud annonçait la mauvaise `InternalIP`.

Dans un cluster RKE2 géré par Rancher, avec une patte d’administration, une patte applicative et une patte de stockage, la promesse semble simple : chaque type de flux doit rester sur son propre réseau. Le control plane doit rester sur l’administration. Le stockage doit sortir sur sa patte dédiée. Et le trafic inter-workers doit passer par l’interface applicative.

Ce découpage est propre sur le papier. En pratique, il suffit qu’un seul mécanisme réseau reprenne la mauvaise adresse du nœud pour décaler tout le dataplane. C’est ce qui s’est produit ici : les workers remontaient en Ready, kubectl get nodes -o wide ne montrait rien d’alarmant, cilium status restait propre, et les workloads se déployaient. Pourtant, dès qu’un pod sur un worker devait joindre un pod sur un autre worker, le trafic échouait. Le VXLAN partait sur la mauvaise interface.

L’architecture minimale à garder en tête

La plateforme reposait sur deux clusters, avec deux rôles différents :

  • un cluster de management, porté par Rancher, chargé d’administrer la plateforme ;
  • un user cluster RKE2, chargé d’héberger les workloads applicatifs.

Dans le user cluster, les rôles n’étaient pas symétriques :

  • les masters portaient le control plane RKE2 et les composants système ;
  • les workers portaient les workloads et le dataplane applicatif.

Sur les workers, trois interfaces logiques coexistaient :

InterfaceRôleCe qu’elle transporte
adminadministrationenrôlement, API Kubernetes, flux de gestion
appliapplicatiftrafic inter-workers, dataplane Cilium, exposition applicative
storagestockageNFS et volumes persistants

Pour que cette architecture tienne, il fallait donc une séparation nette :

  • le control plane devait rester joignable via le réseau d’administration ;
  • le trafic pod-to-pod entre workers devait passer par la patte applicative ;
  • le stockage devait sortir sur sa propre interface ;
  • les policies de sécurité devaient pouvoir bloquer le réseau d’administration sans casser les flux applicatifs légitimes.
Architecture cible

Vue technique cible : Rancher sur K3s, RKE2 multi-NIC, bus séparés

Rancher reste sur K3s côté management. Dans le user cluster RKE2, le control plane reste sur admin, le dataplane Cilium et VXLAN passe sur appli, et le stockage suit storage / NFS.

Management (Rancher) Administration User Cluster (RKE2) Master Nodes Administration Worker Nodes Administration Applicatif (Cilium) Stockage (NFS/PV) External Env. Users Storage Applicatif Stockage Flux d'Administration Flux Applicatif (Cilium) Flux de Stockage

Quelques notions à poser avant le cas réel

InternalIP, ExternalIP et node-ip

Dans Kubernetes, un objet Node expose plusieurs adresses dans son statut, dont InternalIP et parfois ExternalIP. La documentation Kubernetes sur les nœuds et la référence Node Status décrivent ce modèle. Côté kubelet, l’adresse publiée peut être influencée par --node-ip, documenté dans la référence kubelet.

Dans RKE2, ce réglage se fait généralement via le fichier de configuration de l’agent. La référence de configuration agent RKE2 documente node-ip, node-external-ip et lb-server-port.

# /etc/rancher/rke2/config.yaml.d/99-nodeip-app.yaml
node-ip: 198.51.100.31
node-external-ip: 192.0.2.21

Avec cette configuration, le nœud publie maintenant l’adresse attendue dans kubectl get nodes -o wide.

Dans un design comme celui-ci, l’idée est justement de pouvoir publier l’adresse applicative comme InternalIP tout en gardant, si nécessaire, une adresse de gestion séparée comme ExternalIP.

Ce qu’elle ne change pas à elle seule : l’interface sur laquelle Cilium attache ses programmes eBPF. C’est un autre sujet.

VXLAN et tunnelendpoint

En mode encapsulé, Cilium construit un maillage de tunnels entre nœuds. La documentation Cilium sur le routage décrit ce mode overlay et rappelle que VXLAN utilise l’UDP 8472.

L’idée importante pour la suite est la suivante :

  • le pod d’arrivée n’est pas joint directement par L3 ;
  • le paquet est encapsulé vers un endpoint de tunnel porté par le nœud distant ;
  • si cet endpoint correspond à la mauvaise interface du nœud, tout le chemin du dataplane est décalé.

Dans la pratique, c’est visible dans le BPF IPCache que l’on peut inspecter via cilium bpf ipcache list ou cilium-dbg bpf ipcache list, décrit dans la documentation de commande Cilium. C’est là que l’on retrouve les tunnelendpoint utilisés par Cilium.

CiliumNodeConfig, devices et direct-routing-device

Cilium permet d’appliquer une configuration par groupe de nœuds via CiliumNodeConfig. La documentation officielle sur la configuration par nœud explique le principe : on surcharge une partie de la configuration de l’agent pour un ensemble de nœuds sélectionnés par label.

apiVersion: cilium.io/v2
kind: CiliumNodeConfig
metadata:
  name: worker-app-interface
  namespace: kube-system
spec:
  nodeSelector:
    matchLabels:
      node-role.kubernetes.io/worker: "true"
  defaults:
    devices: "appli"
    direct-routing-device: "appli"

Le comportement des options vient de l’agent Cilium lui-même. La référence cilium-agent précise que :

  • --devices sélectionne les interfaces réseau sur lesquelles l’agent va s’appuyer pour des fonctions comme BPF NodePort, le masquage BPF ou le host firewall ;
  • --direct-routing-device désigne explicitement l’interface utilisée pour les décisions de routage direct si l’auto-détection n’est pas suffisante.

Avec cette configuration, le dataplane Cilium s’attache à la bonne interface et prend ses décisions de routage au bon endroit.

Dans une architecture où masters et workers n’utilisent pas la même interface réseau pour le dataplane, cela implique souvent deux CiliumNodeConfig distincts, pas un seul manifest global.

Ce qu’elle ne change pas : la valeur InternalIP du nœud Kubernetes. Si cette InternalIP pointe encore vers la patte d’administration, le problème peut rester entier.

Pourquoi rp_filter=2 compte dans un cluster multi-NIC

Le guide ip-sysctl du noyau Linux documente rp_filter. En mode strict, le noyau attend qu’un paquet reçu ait aussi son meilleur chemin de retour par la même interface. Dans un cluster multi-interface avec encapsulation, cette hypothèse devient fragile.

net.ipv4.conf.all.rp_filter = 2
net.ipv4.conf.default.rp_filter = 2

Avec ce réglage, le noyau passe en mode loose et évite de dropper silencieusement des paquets valides dans une topologie asymétrique.

Ce qu’il ne change pas : l’endpoint VXLAN choisi par Cilium. rp_filter=2 évite une classe de drops, mais ne corrige pas un mauvais ancrage du tunnel.

Le scénario à qualifier

Une fois le cluster créé via Rancher, le point à qualifier était simple :

  1. des pods répartis sur deux workers distincts ;
  2. un dataplane Cilium attaché à l’interface applicative des workers ;
  3. une policy interdisant l’accès au réseau d’administration pour les workloads utilisateurs ;
  4. des flux pod-to-pod inter-workers qui continuent pourtant à fonctionner parce qu’ils passent bien par la patte applicative.

Le détail qui va compter plus tard est le suivant : en provisionnement via l’UI Rancher, il est facile d’enrôler les workers via leur interface de gestion, mais beaucoup moins de leur injecter dès l’enrôlement un node-ip différent et propre à chaque nœud. Le cluster démarre donc dans un état plausible pour la gestion, sans garantie que le dataplane final suive déjà la bonne patte réseau.

Comment le problème s’est manifesté

Le symptôme n’est pas apparu pendant l’installation. Il est apparu au moment de qualifier les flux.

Nous avons déployé des pods de test sur deux workers distincts, puis rejoué des échanges simples entre eux. À ce moment-là :

  • les nœuds remontaient en Ready ;
  • kubectl get nodes -o wide ne montrait rien d’absurde ;
  • les pods système étaient présents ;
  • cilium status restait propre ;
  • les workloads applicatifs démarraient.

Mais les échanges inter-workers échouaient. Dans notre cas, la première manifestation exploitable a été un échec de connectivité de type ping ou requête inter-pods, avec des drops visibles côté politique réseau.

Chronologie du diagnostic

Du premier échec à la preuve réseau décisive

Les signaux généraux restaient verts. Le basculement s’est fait en comparant l’InternalIP publiée et l’endpoint VXLAN réel.

  1. 1

    Test inter-worker

    Deux pods répartis sur des workers différents échouent à communiquer alors que le cluster semble installé correctement.

  2. 2

    Faux signaux verts

    Ready, kubectl get nodes -o wide et cilium status rassurent, mais ne disent rien sur la vraie cible VXLAN.

  3. 3

    Drops visibles côté policy

    Hubble et les policies orientent l’attention vers le réseau d’administration, là où le flux n’aurait jamais dû passer.

  4. 4

    Comparaison décisive

    Le diagnostic se débloque quand on confronte `InternalIP` et `tunnelendpoint` dans le BPF IPCache.

  5. 5

    Correction et requalification

    Après le `node-ip` sur la patte applicative, les endpoints VXLAN et les tests inter-workers se réalignent.

Pourquoi les premiers signaux semblaient pourtant corrects

Le piège de ce type d’incident, c’est que plusieurs contrôles utiles ne répondent pas à la bonne question.

kubectl get nodes -o wide indique qu’un cluster fonctionne, mais pas quel réseau Cilium utilise réellement comme endpoint de tunnel.

cilium status est indispensable, mais il peut confirmer un agent sain sans prouver que le VXLAN inter-workers est ancré sur la bonne adresse. On peut très bien lire un état du genre :

KubeProxyReplacement: True [appli 198.51.100.31 (Direct Routing)]

…et avoir quand même un tunnelendpoint qui pointe vers l’adresse d’administration du worker distant.

De la même manière, une annotation cilium.io/ipv4-node cohérente avec la patte applicative peut exister sans que cela ne modifie, dans la combinaison de versions observée, l’endpoint réellement utilisé dans le BPF IPCache.

L’observation qui a débloqué le diagnostic

Le diagnostic a progressé quand nous avons arrêté de regarder les signaux “verts” et commencé à comparer deux choses très précises :

  • l’adresse InternalIP annoncée par les workers ;
  • l’adresse de tunnelendpoint utilisée par Cilium pour joindre les pods du worker distant.

Avant correction, on observait un écart de ce type :

$ kubectl get nodes -o wide
NAME        STATUS   ROLES    INTERNAL-IP    EXTERNAL-IP
worker-01   Ready    <none>   192.0.2.21     <none>
worker-02   Ready    <none>   192.0.2.22     <none>
$ cilium bpf ipcache list
10.42.5.0/24      tunnelendpoint=192.0.2.22
10.42.5.144/32    tunnelendpoint=192.0.2.22

Le cluster était sain du point de vue Kubernetes. Le dataplane, lui, s’orientait vers la patte d’administration.

À partir de là, le comportement des policies devenait logique. Si une CiliumClusterwideNetworkPolicy interdit aux workloads utilisateurs de sortir vers le réseau d’administration, alors un flux inter-worker encapsulé vers 192.0.2.22 sera bloqué, même si l’intention fonctionnelle était “autoriser app-a vers app-b”.

VXLAN avant / apres

Le même cluster, deux ancrages réseau radicalement différents

À gauche, le tunnel vise l’interface d’administration et rencontre la policy. À droite, le `node-ip` réaligne l’endpoint VXLAN avec la patte applicative.

VXLAN Success Worker 01 Pod Source Administration 192.0.2.21 Applicatif 198.51.100.31 Worker 02 Pod Destination Administration 192.0.2.22 Applicatif 198.51.100.32 ← tunnelendpoint

La cause racine, formulée proprement

Une fois cette comparaison faite, la chaîne causale devient beaucoup plus claire :

  1. les workers sont enrôlés via Rancher à partir de leur interface d’administration ;
  2. RKE2 publie alors une InternalIP qui correspond à cette interface ;
  3. Cilium reprend cette InternalIP comme endpoint VXLAN dans son BPF IPCache ;
  4. le trafic inter-workers part donc vers le réseau d’administration ;
  5. la policy censée protéger ce réseau se met à bloquer un flux qui n’aurait jamais dû y passer.

Autrement dit, la policy n’était pas fautive. Elle rendait visible une erreur d’ancrage du dataplane.

Pourquoi cilium.io/ipv4-node et devices: appli ne suffisaient pas

Deux pistes semblaient naturelles, et c’est précisément pour cela qu’elles méritent d’être expliquées.

L’annotation cilium.io/ipv4-node

Elle donnait une information correcte sur l’adresse applicative souhaitée. Dans notre cas, cela ne suffisait pas à faire basculer les tunnelendpoint observés dans le BPF IPCache. Le point de vérité restait l’InternalIP Kubernetes.

devices: appli

Cette configuration restait indispensable. Sans elle, le dataplane des workers ne s’attachait pas au bon endroit. Mais elle ne répondait pas à la même question que node-ip.

  • node-ip agit sur l’adresse annoncée par le nœud ;
  • devices agit sur l’attachement du dataplane ;
  • direct-routing-device complète cette configuration et évite, dans notre cas, l’erreur unable to determine direct routing device.

On peut donc avoir une situation intermédiaire où :

  • cilium status montre bien appli côté worker ;
  • le remplacement de kube-proxy fonctionne ;
  • mais le VXLAN continue malgré tout à viser l’adresse portée par admin.

La correction retenue

La correction n’a pas consisté à forcer encore davantage Cilium. Elle a consisté à modifier l’adresse que les workers annonçaient à Kubernetes.

Concrètement, les workers ont reçu un override node-ip vers leur patte applicative, après le provisionnement du cluster :

# /etc/rancher/rke2/config.yaml.d/99-nodeip-app.yaml
node-ip: 198.51.100.31

Avec ce fichier, worker-01 publie désormais son adresse applicative comme InternalIP.

Ce qu’il ne change pas : la façon dont Cilium choisit ses interfaces d’attachement. Le CiliumNodeConfig reste nécessaire.

En parallèle, la configuration Cilium côté workers devait rester explicite :

apiVersion: cilium.io/v2
kind: CiliumNodeConfig
metadata:
  name: worker-app-interface
  namespace: kube-system
spec:
  nodeSelector:
    matchLabels:
      node-role.kubernetes.io/worker: "true"
  defaults:
    devices: "appli"
    direct-routing-device: "appli"

Avec cette configuration, l’agent Cilium des workers travaille bien sur la patte applicative.

Ce qu’elle ne change pas : l’InternalIP du nœud. Sans le node-ip précédent, le tunnel peut rester mal orienté.

Pourquoi le control plane n’a pas cassé

Changer l’InternalIP d’un worker vers la patte applicative ressemble, au premier abord, à une très mauvaise idée. Dans notre cas, cela n’a pas cassé le chemin de gestion pour deux raisons.

La première est visible dans la documentation RKE2 :

  • la page Cluster Access montre que le kubeconfig généré par RKE2 pointe vers 127.0.0.1 ;
  • la référence agent RKE2 documente lb-server-port, c’est-à-dire le load-balancer client local utilisé par l’agent.

Autrement dit, le nœud ne parle pas directement à un master distant en utilisant sa propre InternalIP comme point d’entrée. RKE2 interpose déjà un chemin local pour l’accès au supervisor et à l’API server.

La seconde raison est d’architecture : le réseau d’administration reste présent et continue de porter les flux de gestion, de Rancher et du control plane. Le fait de publier une autre InternalIP sur le worker corrige donc le dataplane VXLAN sans déplacer toute la gestion sur la patte applicative.

Control plane vs dataplane

Deux chemins distincts : gestion locale RKE2 d’un côté, VXLAN applicatif de l’autre

Le worker ne change pas de mission réseau. Il continue à gérer le control plane via le proxy local RKE2, pendant que le dataplane inter-workers s’appuie sur `node-ip` et `CiliumNodeConfig`.

Worker Node Kubelet Gestion 127.0.0.1:6443 (Local Proxy) Master Node Administration Pod App Trafic Cilium VXLAN (APPLI) Worker Distant Applicatif

Comment nous avons validé la correction

La validation ne s’est pas arrêtée au retour d’un cluster Ready. Nous avons cherché une chaîne de preuves.

1. Vérifier le basculement de InternalIP

Après correction, les workers devaient publier leur patte applicative comme InternalIP :

$ kubectl get nodes -o wide
NAME        STATUS   ROLES    INTERNAL-IP      EXTERNAL-IP
worker-01   Ready    <none>   198.51.100.31    192.0.2.21
worker-02   Ready    <none>   198.51.100.32    192.0.2.22

Ce contrôle prouve que Kubernetes voit maintenant la bonne adresse comme identité interne du worker.

Il ne prouve pas encore que Cilium l’utilise déjà comme endpoint VXLAN. Il faut aller un cran plus bas.

2. Vérifier que Cilium reste sain

cilium status devait rester cohérent :

  • agents en bonne santé ;
  • workers attachés à appli ;
  • absence de crash lié à direct-routing-device.

Ce contrôle prouve que la configuration Cilium tient.

Il ne prouve toujours pas que le tunnel est ancré sur la bonne IP.

3. Vérifier le BPF IPCache

Le signal décisif se trouvait là :

$ cilium bpf ipcache list
10.42.5.0/24      tunnelendpoint=198.51.100.32
10.42.5.144/32    tunnelendpoint=198.51.100.32

Cette fois, le tunnelendpoint du worker distant pointait vers la patte applicative. C’était le changement attendu.

4. Rejouer les tests inter-workers

La dernière étape consistait à vérifier le comportement observable :

  • les échanges inter-workers cessaient d’échouer ;
  • les drops auparavant corrélés à la policy d’administration disparaissaient ;
  • la séparation administration / applicatif / stockage redevenait lisible dans les flux réels.

Quand ces trois niveaux s’alignent, le sujet est réellement corrigé :

  • identité du nœud ;
  • endpoint VXLAN ;
  • comportement applicatif.
Chaîne de validation

Quatre vérifications avant de conclure

InternalIP, état Cilium, endpoint VXLAN et test inter-worker doivent tous pointer dans la même direction.

1. InternalIP OK
$ kubectl get nodes -o wide
worker-01 Ready 198.51.100.31 192.0.2.21
2. Cilium status OK
$ cilium status
KubeProxyReplacement: True [ appli 198.51.100.31 (Direct Routing)]
3. BPF IPCache OK
$ cilium bpf ipcache list
10.42.5.0/24 tunnelendpoint= 198.51.100.32
4. Test inter-worker OK
$ kubectl exec pod-a -- ping pod-b
64 bytes from 10.42.5.144: icmp_seq=1 ttl=64 time=0.8 ms

Ce qu’il faut retenir pour un autre projet

Trois points ressortent de ce cas.

1. Ready ne veut pas dire “dataplane qualifié”

Un cluster peut être exploitable côté API et encore faux côté chemin réseau inter-workers. Tant que l’on n’a pas regardé les endpoints réellement utilisés par Cilium, une partie critique du système reste implicite.

2. node-ip et CiliumNodeConfig résolvent deux problèmes différents

node-ip agit sur l’identité réseau annoncée par le nœud. CiliumNodeConfig agit sur le dataplane que l’agent Cilium attache et pilote. Les deux sont complémentaires. Les confondre fait perdre du temps.

3. En multi-NIC, la vraie source de vérité est souvent plus bas que kubectl

Sur ce type de sujet, les commandes qui rassurent trop vite sont :

  • kubectl get nodes -o wide pris isolément ;
  • cilium status pris isolément ;
  • la simple présence d’annotations ou de manifests supposés corrects.

Les commandes qui font avancer le diagnostic sont :

  • la lecture de l’InternalIP réelle des workers ;
  • l’inspection du BPF IPCache ;
  • les tests inter-workers sur des pods effectivement répartis ;
  • l’observation Hubble quand une policy semble “casser” quelque chose.

Conclusion

Le point le plus utile de ce retour d’expérience n’est pas seulement la correction elle-même. C’est la façon de raisonner sur un cluster multi-interface.

Si l’objectif est de faire sortir le dataplane inter-workers sur une patte applicative dédiée, il faut vérifier trois couches distinctes :

  1. l’adresse que le nœud annonce à Kubernetes ;
  2. l’interface sur laquelle Cilium attache réellement son dataplane ;
  3. l’endpoint VXLAN que Cilium utilise pour joindre le nœud distant.

Tant que ces trois couches ne sont pas alignées, un cluster peut paraître sain tout en échouant exactement là où l’architecture devait être qualifiée.

A.E

Aimen E.

DevOps and Principal Cloud Architect

Aimen accompagne les clients sur leurs projets de transformation digitale, d'optimisation de la performance applicative et d'adoption du cloud.