Aller au contenu

Cilium : l'approche du routage natif dans un environnement Kubernetes

Unleash the beast: maîtrisez et améliorez la mise en réseau de votre cluster Kubernetes.

Routage natif dans un environnement Kubernetes

La conférence KubeCon approche à grands pas, offrant une opportunité excitante de perfectionner nos compétences dans le domaine du réseau pour nos clusters Kubernetes ! 

Il est fort probable que vous ayez déjà rencontré de nombreux clusters Kubernetes déployés sur des machines virtuelles, créés à l’aide de playbooks Ansible et de Kubeadm. Nous ne parlerons pas ici des clusters gérés par vos fournisseurs de cloud préférés, mais plutôt de ceux qui optent pour une gestion autonome de l’infrastructure.

Dans cet article, nous mettrons l’accent sur la gestion du réseau à travers les outils de gestion des CNI. Cette réflexion a pour départ un article publié sur le blog de Cilium dont l’objectif est l’amélioration des performances réseau de notre cluster Kubernetes :

Cilium Standalone Layer 4 Load Balancer XDP
See the performance increase Seznam.cz saw using Cilium for load balancing…

L’idée n’est pas de vérifier les métriques exposées, mais d’évaluer la complexité et les prérequis (software) pour atteindre ce niveau de performance.

Avant de se lancer tête baissée dans une implémentation en production, il est judicieux de déployer sur son poste de travail ou dans son homelab son propre cluster. On ne le présente plus, kind est idéal pour déployer un cluster Kubernetes nous permettant de tester et modifier des configurations rapidement et en toute autonomie.


Kind le kickstarter simple

Commençons par créer un cluster en prenant soin de :

  • Créer plusieurs noeuds: 1 utilisé comme control plane et 2 noeuds pour le data plane 
  • Désactiver le déploiement d’un plugin CNI (on installera Cilium ultérieurement)
  • Ne pas utiliser kube-proxy pour la gestion de notre réseau (l’objectif étant de tout basculer en eBPF)
# kind-config.yaml 
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
  - role: worker
  - role: worker
networking:
  disableDefaultCNI: true
  kubeProxyMode: none

# Use `kind create cluster --config=kind-config.yaml` command to create the cluster

Une fois la commande kind lancée pour créer le cluster (à gauche), on remarque que certains pods restent dans l’état “pending”. Ce phénomène est causé par l’absence d’un plugin CNI dans le cluster pour gérer les adresses IP internes au cluster (pods et services). Passons maintenant au déploiement de Cilium dans notre cluster kind pour remédier à cela.


Cilium pour gérer la couche réseau

L’installation de Cilium est aisée, que ce soit via un chart Helm ou en utilisant un interface de ligne de commande (CLI) qui fait également appel au chart Helm. 

Cependant, avant de poursuivre, revenons à l’objet de notre article : l’optimisation des performances réseau dans notre cluster.

Malheureusement, le simple déploiement de Cilium ne suffit pas, il est important de comprendre son fonctionnement. 

Cilium offre deux types de configuration réseau : l’encapsulation et le routage direct (natif).

Routing — Cilium 1.15.1 documentation

Le routage natif sera préféré afin de maximiser les performances. Bien qu’il nécessite une configuration plus avancée, il permettra aux administrateurs de tirer pleinement parti des fonctionnalités réseau offertes par eBPF, et de se passer entièrement d’iptables/ipvs.

Si vous souhaitez approfondir le sujet, n’attendez pas pour lire l'article de @_asayah:

Choosing the Right Routing in Cilium
Cilium provides two primary methods of routing traffic between nodes -- learn about the advantages and disadvantages of each.

Passons à l’installation de Cilium dans notre cluster à l’aide de la configuration suivante :

# cilium-medium.yaml
cluster:
  name: kind-kind

k8sServiceHost: kind-control-plane
k8sServicePort: 6443
kubeProxyReplacement: strict

ipv4:
  enabled: true
ipv6:
  enabled: false

hubble:
  relay:
    enabled: true
  ui:
    enabled: true
ipam:
  mode: kubernetes

# Cilium Routing
routingMode: native
ipv4NativeRoutingCIDR: 10.244.0.0/16
enableIPv4Masquerade: true
autoDirectNodeRoutes: true

Et de la commande helm :

$ helm install -n kube-system cilium cilium/cilium -f cilium-medium.yaml

Après quelques minutes, tous les pods seront déployés conformément aux attentes.

Note: il est important de spécifier les attributs k8sServiceHost et k8sServicePort pour que l’opérateur Cilium sache adresser le control-plane. En l’absence des 2 attributs, l’opérateur Cilium restera sur un status crashloopbackoff car il ne sera pas en capacité de faire la résolution DNS (via le service créé par CoreDNS mais qui ne possède pas encore d’IP).


Deep into Cilium configuration

L’adoption du réseau natif n’est pas sans implication. Dans Kubernetes, chaque nœud hérite d’un plan d’adressage unique qu’il utilise pour identifier les pods qu’il exécute.

$ kubectl get configmaps -n kube-system kubeadm-config -o yaml | grep podSubnet
      podSubnet: 10.244.0.0/16
$ # Or using kubectl cluster-info dump | grep -m 1 cluster-cidr

$ kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.podCIDR}{"\n"}'
kind-control-plane      10.244.0.0/24
kind-worker     10.244.2.0/24
kind-worker2    10.244.1.0/24

Afin que chaque nœud du cluster puisse accéder aux pods exécutés sur les autres nœuds, il est impératif que ces sous-réseaux soient correctement routés. 
La documentation est explicite à ce sujet : par défaut, cette configuration n’est pas en place:

Each individual node is made aware of all pod IPs of all other nodes and routes are inserted into the Linux kernel routing table to represent this.
https://docs.cilium.io/en/stable/network/concepts/routing/#id4

Cette situation peut être facilement constatée sur un nœud du cluster :

root@kind-worker:/# ip route   
default via 172.18.0.1 dev eth0 
10.244.2.0/24 via 10.244.2.224 dev cilium_host proto kernel src 10.244.2.224 
10.244.2.224 dev cilium_host proto kernel scope link 
172.18.0.0/16 dev eth0 proto kernel scope link src 172.18.0.3

Néanmoins lorsque tous les nœuds se trouvent dans le même réseau (L2), il est possible d’activer l’attribut autoDirectNodeRoutes: true. Cela permet à l’agent Cilium d’ajouter automatiquement toutes les routes pour chaque nœud du cluster, évitant ainsi la nécessité de le faire manuellement.

root@kind-worker:/# ip route   
default via 172.18.0.1 dev eth0 
10.244.0.0/24 via 172.18.0.2 dev eth0 proto kernel 
10.244.1.0/24 via 172.18.0.4 dev eth0 proto kernel 
10.244.2.0/24 via 10.244.2.224 dev cilium_host proto kernel src 10.244.2.224 
10.244.2.224 dev cilium_host proto kernel scope link 
172.18.0.0/16 dev eth0 proto kernel scope link src 172.18.0.3

Déploiement d’une application dans le cluster

Pour confirmer la validité des configurations et garantir la continuité des communications entre les pods, j’ai opté pour l’utilisation du chart fourni par Cilium :

https://raw.githubusercontent.com/cilium/cilium/HEAD/examples/minikube/http-sw-app.yaml

$ kubectl exec tiefighter – curl --connect-timeout 10 -s https://swapi.dev/api/starships
{"count":36,"next":"https://swapi.dev/api/starships/?page=2","previous":null,"results":[...]}

$ kubectl exec tiefighter – curl --connect-timeout 10 -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing
Ship landed

Il me permet ainsi de valider :

  • La résolution DNS dans mon cluster
  • La communication privée et publique

More eBPF features

Par défaut, Cilium utilise iptables pour configurer les règles de masquerade. 
Le masquerade est une technique de réseau qui permet de masquer les adresses IP d’un réseau interne (celui des pods) lorsqu’ils communiquent avec des réseaux externes (en dehors du cluster). Dès lors qu’un paquet quitte le réseau du cluster pour se rendre sur un réseau externe, l’adresse source du paquet est modifiée pour être celle du noeud qui exécute le container.

Exemple :

root@kind-worker:/# iptables -t nat -S  CILIUM_POST_nat
-N CILIUM_POST_nat
-A CILIUM_POST_nat -s 10.244.2.0/24 -m set --match-set cilium_node_set_v4 dst -m comment --comment "exclude traffic to cluster nodes from masquerade" -j ACCEPT
-A CILIUM_POST_nat -s 10.244.2.0/24 ! -d 10.244.0.0/16 ! -o cilium_+ -m comment --comment "cilium masquerade non-cluster" -j MASQUERADE
-A CILIUM_POST_nat -m mark --mark 0xa00/0xe00 -m comment --comment "exclude proxy return traffic from masquerade" -j ACCEPT
-A CILIUM_POST_nat -s 127.0.0.1/32 -o cilium_host -m comment --comment "cilium host->cluster from 127.0.0.1 masquerade" -j SNAT --to-source 10.244.2.224
-A CILIUM_POST_nat -o cilium_host -m mark --mark 0xf00/0xf00 -m conntrack --ctstate DNAT -m comment --comment "hairpin traffic that originated from a local pod" -j SNAT --to-source 10.244.2.224

Il est possible de modifier ce fonctionnement afin de tirer pleinement parti des fonctionnalités offertes par le noyau Linux. Il vous suffit de personnaliser le déploiement Helm en y ajoutant les capacités de masquerade via eBPF :

# cilium-medium.yaml
...

bpf:
  masquerade: true
ipMasqAgent:
  enabled: true
  config:
    nonMasqueradeCIDRs:
      - 10.244.0.0/8

Ensuite, procédez à la mise à jour du déploiement et vérifiez la configuration :

$ helm upgrade -n kube-system cilium cilium/cilium -f cilium-medium.yaml
...

$ kubectl -n kube-system exec ds/cilium -- cilium-dbg status | grep Masquerading
Masquerading:            BPF (ip-masq-agent)   [eth0]   10.244.0.0/16 [IPv4: Enabled, IPv6: Disabled]

$ kubectl -n kube-system exec ds/cilium -- cilium-dbg bpf ipmasq list
IP PREFIX/ADDRESS
10.244.0.0/16
169.254.0.0/16

À ce stade, il est probable que vous rencontriez des problèmes de résolution DNS externe. En effet, Docker utilise également des règles iptables pour rediriger les requêtes externes vers la machine où le cluster kind est déployé :

$ docker exec -ti kind-worker bash
root@kind-worker:/# iptables -t nat -S DOCKER_OUTPUT
-N DOCKER_OUTPUT
-A DOCKER_OUTPUT -d 172.18.0.1/32 -p tcp -m tcp --dport 53 -j DNAT --to-destination 127.0.0.11:44581
-A DOCKER_OUTPUT -d 172.18.0.1/32 -p udp -m udp --dport 53 -j DNAT --to-destination 127.0.0.11:33181

Une alternative consiste à modifier la configuration de CoreDNS pour spécifier directement l’adresse du serveur DNS à utiliser, offrant ainsi une solution de contournement pour notre environnement :

$ nmcli dev show | grep 'IP4.DNS'
IP4.DNS[1]:                             192.168.1.1

$ kubectl edit configmaps -n kube-system coredns
# Replace forward . /etc/resolv.conf with forward . 192.168.1.1

$ kubectl -n kube-system rollout restart deployment coredns

Il est temps de s’occuper de l’équilibrage nord/sud

Nous voilà enfin dans la dernière configuration recommandée par l’article initial, qui implique l’activation de la technologie XDP. 

Mais avant cela, il est nécessaire de créer un service pour notre application.

# cat deathstar-svc.yaml 
apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/name: deathstar
  name: deathstar-lb
  namespace: default
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    class: deathstar
    org: empire
  sessionAffinity: None
  type: LoadBalancer

A cette étape, le service ne récupère pas d’adresse IP externe.

La gestion des adresses IP des services externes, appelée “LoadBalancer IP Address Management (LB IPAM)”, est une fonctionnalité de Cilium. Pour réaliser cette tâche, il est nécessaire de spécifier le sous-réseau dans lequel Cilium réservera les adresses IP.

Afin de faciliter le routage entre la couche docker (backend de kind) et notre laptop, il est préconisé d’utiliser un sous réseau du CIDR utilisé et routé par docker (par défaut “172.18.0.0/16").

kind – LoadBalancer

On peut vérifier que le routage est déjà présent pour cette destination :

$ ip route
default via 192.168.1.1 dev wlp2s0 proto dhcp src 192.168.1.63 metric 600 
169.254.0.0/16 dev wlp2s0 scope link metric 1000 
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown 
172.18.0.0/16 dev br-53ce6d3e9cb9 proto kernel scope link src 172.18.0.1 
192.168.1.0/24 dev wlp2s0 proto kernel scope link src 192.168.1.63 metric 600 

Dans mon exemple, j’utilise le sous réseau “172.18.250.0/24” pour définir les IPs de mes services.

# cilium-lbsvc-pool.yaml
apiVersion: "cilium.io/v2alpha1"
kind: CiliumLoadBalancerIPPool
metadata:
  name: "default"
spec:
  blocks:
  - cidr: "172.18.250.0/24"

Pour vérifier en détail le statut du service, nous pouvons consulter directement la configuration sur les agents :

$ kubectl get service deathstar-lb
NAME           TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)        AGE
deathstar-lb   LoadBalancer   10.96.208.17   172.18.250.1   80:32367/TCP   16h

$ kubectl -n kube-system exec pods/cilium-9vc2l - cilium-dbg service list
ID   Frontend            Service Type   Backend                           
....
16   172.18.250.1:80     LoadBalancer   1 => 10.244.2.117:80 (active)     
                                        2 => 10.244.1.108:80 (active)

Lorsque vous utilisez un plan d’adressage différent de celui par défaut de Docker (par exemple 192.168.250.0/24), pour accéder au service depuis votre machine, vous devrez en plus :

  • Mettre en place les annonces ARP en utilisant la fonctionnalité de l2annoncement
# cilium-medium.yaml
...

l2announcements:
  enabled: true
  • Procéder à la mise à jour des agents Cilium (les init-containers ont besoin d’être rejoués)
$ helm upgrade -n kube-system cilium cilium/cilium -f cilium-medium.yaml
$ kubectl rollout -n kube-system restart daemonset cilium
  • Créer une stratégie d’annonce
apiVersion: "cilium.io/v2alpha1"
kind: CiliumL2AnnouncementPolicy
metadata:
  name: default
spec:
  nodeSelector:
    matchExpressions:
      - key: node-role.kubernetes.io/control-plane
        operator: DoesNotExist
  interfaces:
  - ^eth[0-9]+
  externalIPs: true
  loadBalancerIPs: true
  • Configurer une route (192.168.250.0/24) sur notre machine afin de permettre le routage depuis notre poste vers l’environnement sous Docker
$ sudo ip route add 192.168.250.0/24 dev br-53ce6d3e9cb9 src 172.18.0.1

$ ip route
default via 192.168.1.1 dev wlp2s0 proto dhcp src 192.168.1.63 metric 600 
169.254.0.0/16 dev wlp2s0 scope link metric 1000 
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown 
172.18.0.0/16 dev br-53ce6d3e9cb9 proto kernel scope link src 172.18.0.1 
192.168.1.0/24 dev wlp2s0 proto kernel scope link src 192.168.1.63 metric 600 
192.168.250.0/24 dev br-53ce6d3e9cb9 scope link src 172.18.0.1
  • Vérifier que l’annonce est bien en reçue
$ ip neigh | grep 172.18
172.18.0.4 dev br-53ce6d3e9cb9 lladdr 02:42:ac:12:00:04 STALE
172.18.0.2 dev br-53ce6d3e9cb9 lladdr 02:42:ac:12:00:02 STALE
172.18.250.1 dev br-53ce6d3e9cb9 lladdr 02:42:ac:12:00:04 REACHABLE
172.18.0.3 dev br-53ce6d3e9cb9 lladdr 02:42:ac:12:00:03 STALE

Si vous avez opté pour le plan d’adressage par défaut de Docker ou si vous avez correctement configuré les annonces L2, vous pouvez poursuivre ici.

La technologie XDP, ou Express Data Path, est une fonctionnalité du noyau Linux. Elle permet un traitement ultra-rapide des paquets réseau directement au niveau du driver de la carte réseau, avant même qu’ils ne soient transmis à la pile réseau classique du noyau. Cette approche offre des performances très élevées et une latence minimale pour le traitement des paquets.

L’activation de l’accélération XDP est plus simple à mettre en place (attention aux prérequis).

# cilium-medium.yaml
...

loadBalancer:
  acceleration: native
  mode: hybrid

De nouveau, il est impératif de déclencher la mise à jour des agents Cilium.

$ helm upgrade -n kube-system cilium cilium/cilium -f cilium-medium.yaml
$ kubectl rollout -n kube-system restart daemonset cilium

Nous pouvons ainsi valider la configuration

$ kubectl -n kube-system exec ds/cilium – cilium-dbg status --verbose | grep XDP
  XDP Acceleration:       Native

$ docker exec -ti kind-worker bash

root@kind-worker:/# ip addr  | grep xdp
36: eth0@if37: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdp/id:81834 qdisc noqueue state UP group default qlen 1000

En conclusion, la mise en œuvre des configurations décrites dans les étapes précédentes permettra d’optimiser les performances et la gestion du réseau dans votre environnement Kubernetes. Tour cela en exploitant pleinement les fonctionnalités avancées offertes par des outils tels que Cilium, eBPF et XDP.

Cette approche offre des performances très élevées et une latence minimale pour le traitement des paquets, ce qui en fait une solution idéale pour les applications nécessitant un traitement réseau intensif (API managements, IDS et autres).

En contrepartie, les administrateurs devront acquérir de nouvelles compétences, comprendre de nouveaux concepts ou envisager simplement l’adoption de solutions gérées par des des cloud providers.

Dernier