eBPF : vers la fin des sidecars dans Kubernetes ?

Kubernetes s’est diffusé de manière extrêmement rapide ces dernières années. Ses capacités d’orchestration de conteneurs ont permis de standardiser le déploiement d’applications dans le cloud et de fiabiliser leur exécution.

Des besoins non fonctionnels ont rapidement émergé. Il a par exemple été nécessaire de sécuriser les communications entre pods avec du mTLS et de restreindre les flux de communication entre pods aux seuls échanges légitimes. La tolérance aux pannes a également été améliorée avec par exemple l’utilisation de coupe circuits (circuit breaker) et des mécanismes automatiques de rejeu (auto retry).

Initialement implémentées au sein du code applicatif, il est rapidement apparu nécessaire de découpler ces préoccupations de celui-ci. C’est pour cela que des Service Mesh comme Istio et Linkerd ont vu le jour. Ils apportent une surcouche implantée à l’extérieur du code métier pour prendre en charge ces aspects liés à la sécurité, l’observabilité, le routage. D’un point de vue déploiement, ils prennent la forme de sidecars injectés dans les pods que l’on souhaite administrer. Mais leur succès ne devait pas pour autant faire oublier qu’ils venaient avec un coût non négligeable en termes de complexité, de latence, ...

eBPF (pour extended BPF, BPF n’étant plus un acronyme) est une fonctionnalité introduite dans le Kernel Linux et disponible depuis la version 4.X. Cette fonctionnalité, bien que datant de 2004, attire de plus en plus l’attention, comme en témoigne le lancement de eBPF Summit, une conférence entièrement dédiée à ce sujet dont la première édition s’est tenue en 2021. eBPF permet d’intercepter les appels systèmes dans le noyau Linux de manière plus simple et plus fiable qu’à l’aide des modules noyau. C’est donc tout à fait logiquement que cette fonctionnalité a été employée afin d'intercepter les appels réseaux, et d’implémenter des préoccupations initialement prises en charge par les Service Mesh même si, nous le verrons, le champ d’action d’eBPF n’est pas strictement égal à celui des Service Mesh actuels.

Dans cet article, la première partie va rappeler ce qu’est un Service Mesh et la manière avec laquelle les sidecars sont utilisés pour les mettre en place. Ensuite, nous exposerons le fonctionnement de eBPF, et nous verrons en quoi cette approche peut répondre à une partie des préoccupations des Service Mesh. Nous donnerons ensuite des détails sur Cilium qui se positionne en challenger des Service Mesh actuels. Enfin, nous conclurons en comparant ces deux approches et en répondant à cette question très controversée : eBPF va-t-il tuer les sidecars dans Kubernetes ?

Les Service Mesh

Les Service Mesh ont connu un fort engouement ces dernières années et leur périmètre fonctionnel s’est peu à peu élargi. Côté sécurité, un Service Mesh apporte généralement la sécurisation des échanges entre pods avec mTLS. Cela permet à l’émetteur d’un message d’être certain qu’il ne puisse être lu que par son destinataire et, symétriquement, le destinataire est certain de l’identité de l'émetteur. Ainsi, les attaques de type man in the middle sont impossibles, et la confidentialité comme l’intégrité des messages est assurée au sein du cluster. Pour cela, le Service Mesh s’occupe notamment d’émettre les certificats et de les provisionner au niveau de chaque pod. En plus de ce cas d’usage très classique, les Service Mesh ajoutent généralement un jeu de règles pour n’autoriser les échanges de messages qu’entre certains pods.

Sur le plan de la gestion du trafic, les Service Mesh peuvent fournir un ensemble de fonctionnalités pour ne pas surcharger le réseau avec des requêtes en erreur. C’est le cas des coupes circuits (Circuit Breakers) qui permettent de suspendre temporairement l’envoi de requêtes vers des services en erreur. A l’opposée, des mécanismes de rejeu automatique de requêtes peuvent être fournis.

Les Service Mesh peuvent également répartir les requêtes entre plusieurs versions d’un même service. Avec le canary testing, seule une petite part du trafic sera envoyé vers une version nouvellement déployée (le canari que l’on souhaite tester). Dans le cas de l’A/B testing, le trafic sera généralement (mais pas toujours) réparti de manière égale entre les versions d’un même service.

Les Service Mesh apportent souvent des fonctionnalités liées à l’observabilité, en rendant compte du résultat des requêtes échangées sur le réseau. Les appels en cascade d’un service à un autre peuvent également être suivis de bout en bout grâce à l’injection d’identifiants de suivi. On obtient alors les fondations d’une fonctionnalité de distributed tracing.

D’un point de vue déploiement, les Service Mesh utilisent aujourd’hui des sidecars. A l’intérieur de chaque pod exécutant une application un proxy est injecté. Ce conteneur va intercepter tous les appels entrants et sortants du conteneur applicatif et les échanges d’informations entre pods se feront entre proxies, comme indiqué dans le schéma suivant.

Le Service Mesh assure ainsi de nombreux services tout en étant invisible pour le code applicatif qui ignore jusqu’à son existence ; cela permet une excellente séparation des préoccupations. Enfin, le Service Mesh dispose d’un panneau de contrôle distribué sur plusieurs pods afin d’assurer une supervision globale, de gérer les injections de proxies et leur configuration (comme la distribution des certificats).

Le point important à retenir ici est que le Service Mesh est capable d’intercepter du trafic au niveau 7 (applicatif) du modèle OSI. De cette manière, il peut donc manipuler les headers HTTP et inspecter les codes de réponse HTTP. Cependant, il se trouve totalement aveugle au niveau transport (couche 4) et il n’a donc par exemple pas connaissance du taux de perte des paquets sur le réseau.

Enfin, il est important de noter que ces fonctionnalités viennent au prix de l’injection d’un conteneur par pod… et donc potentiellement d’un très grand nombre de conteneurs au niveau de l’ensemble d’un cluster Kubernetes. C’est la raison pour laquelle les Service Mesh viennent avec un coût non négligeable en termes de processeur et de mémoire. De plus, la technique d’injection de sidecars n’étant pas spécifique aux Service Mesh, il peut arriver que plusieurs sidecars soient injectés au sein d’un même pod, causant des problèmes de stabilité au sein de ceux-ci.

eBPF

Pour ne pas multiplier les conteneurs et libérer des ressources système tout en évitant des interactions non maîtrisées entre sidecars, une solution est d’implémenter les fonctionnalités des Service Mesh directement au niveau du noyau Linux. Pour cela, la première approche qui vient à l’esprit est le recours aux modules qui permettent de longue date d’étendre le comportement du noyau. Mais si comme moi, vous avez déjà programmé un module Linux, vous savez à quel point cela est à la fois complexe et dangereux pour la stabilité du système ! Je me souviens avoir été contraint de redémarrer un nombre incalculable de fois ma machine après avoir modifié, compilé puis inséré un module !

Cette solution, bien que séduisante et extrêmement puissante est donc en pratique extrêmement complexe à mettre en œuvre et également extrêmement dangereuse pour la stabilité du système d’exploitation… donc pour la stabilité de tous les conteneurs de notre cluster Kubernetes !

C’est justement pour pallier ces difficultés qu’eBPF a vu le jour. Cette fonctionnalité propose un mécanisme qui permet d’étendre le noyau Linux sans compromettre ni sa stabilité ni sa sécurité. En pratique, trois fonctionnalités majeures sont proposées pour cela. Tout d’abord, seul root ou un programme ayant la capability CAP_BPF (disponible depuis Linux 5.8) peut charger du code eBPF. Ensuite, le code n’est pas fourni compilé, mais sous forme de bytecode. Cela permet à un processus de vérification d’effectuer une analyse statique pour s’assurer par exemple de l’absence de boucles infinies ou d’accès mémoire non autorisés. Enfin, le programme est compilé par un compilateur JIT pour être exécuté sous forme de code natif à l’intérieur d’un bac à sable. Dans ce contexte sécurisé, seules un nombre restreint de fonctions permettent de lire ou d’écrire des données en dehors du bac à sable.

Comme on peut le constater, on est bien loin des modules Linux qui permettent l’exécution de code arbitraire, avec tous les dangers que cela comporte ! C’est en cela qu’eBPF est réellement révolutionnaire de mon point de vue : il propose un modèle de développement et d’exécution qui permet de fiabiliser très fortement l’exécution de code tiers au sein du noyau.

En pratique, le modèle de programmation d’eBPF s’appuie sur des points d’extension (hook) utilisés par du code écrit en langage C (exemples disponibles dans la documentation) ou à l'aide de projets fournissant un plus haut niveau d'abstraction (Cilium, bcc, ou bpftrace). Il est possible d'intercepter des appels systèmes, des événements réseau ou encore à l’entrée ou à la sortie d’une fonction du noyau Linux. Des événements sont ainsi inspectés, reportés ou modifiés. Le schéma suivant illustre la manière avec laquelle eBPF vient intercepter le trafic envoyé à une socket TCP directement au niveau du noyau.

Comme nous l’avons dit, eBPF (et plus encore son ancêtre BPF) n’est pas une technologie récente. À titre d’exemple, l’utilitaire tcpdump de Linux utilise cette fonctionnalité de longue date ! Ce qui est plus nouveau, c’est l’utilisation de eBPF dans l’écosystème Kubernetes. A ce sujet, plusieurs projets ont plus ou moins récemment vu le jour :

  • Cilium propose des fonctionnalités liées à l’observabilité et la sécurité réseau ;
  • Falco se focalise sur la sécurité avec la détection de comportements suspects en temps réel ;
  • Pixie offre des fonctionnalités d’observabilité ;
  • Groudcover cherche à unifier l’observabilité (métriques, traces et logs) et à corréler ces informations pour faciliter la résolution d’erreurs.

Tous ces projets sont encore récents, et il est certain que leur périmètre fonctionnel et leur maturité va devoir évoluer avant qu’ils ne s’imposent en production. Cependant, au-delà de tel ou tel produit, c’est leur approche basée sur eBPF qui attire aujourd’hui beaucoup l’attention.

Nous allons maintenant détailler les fonctionnalités de Cilium, qui est le projet le plus abouti et le plus connu de la liste que nous venons de présenter.

Cilium

Créé initialement par l’entreprise Isovalent, ce projet a rejoint la CNFC en 2021 où il est actuellement en incubation. Cilium propose principalement des fonctionnalités réseau d’observabilité et de sécurité au sein d’un cluster Kubernetes (même s’il peut également être utilisé en dehors).

La partie observabilité est basée sur Hubble, qui utilise eBPF pour intercepter les appels réseau. De cette manière, tout le trafic peut être analysé et reporté sur différentes interfaces. En ligne de commande, l’utilitaire Hubble permet de rendre compte du trafic, comme ici, en ciblant un pod particulier :

$ hubble observe --compact --server localhost:4245 --pod kube-system/kube-dns-6465f78586-xhgtk
Apr 16 13:27:44.181: 10.168.0.2:49598 -> kube-system/kube-dns-6465f78586-xhgtk:808 to-endpoint FORWARDED (TCP Flags: ACK)
Apr 16 13:27:44.182: 10.168.0.2:49598 -> kube-system/kube-dns-6465f78586-xhgtk:8081 to-endpoint FORWARDED (TCP Flags: ACK, PSH)
Apr 16 13:27:44.182: kube-system/kube-dns-6465f78586-xhgtk:8081 -> 10.168.0.2:49598 to-stack FORWARDED (TCP Flags: ACK, PSH)
Apr 16 13:27:44.182: kube-system/kube-dns-6465f78586-xhgtk:8081 -> 10.168.0.2:49598 to-stack FORWARDED (TCP Flags: ACK, FIN)
Apr 16 13:27:44.182: 10.168.0.2:49598 kube-system/kube-dns-6465f78586-xhgtk:8081 to-endpoint FORWARDED (TCP Flags: ACK, FIN)
Apr 16 13:27:46.577: 10.168.0.2:40534 kube-system/kube-dns-6465f78586-xhgtk:10054 to-endpoint FORWARDED (TCP Flags: SYN)
Apr 16 13:27:46.577: kube-system/kube-dns-6465f78586-xhgtk:10054 -> 10.168.0.2:40534 to-stack FORWARDED (TCP Flags: SYN, ACK)
Apr 16 13:27:46.577: 10.168.0.2:40534 -> kube-system/kube-dns-6465f78586-xhgtk:10054 to-endpoint FORWARDED (TCP Flags: ACK)
Apr 16 13:27:46.577: 10.168.0.2:40534 -> kube-system/kube-dns-6465f78586-xhgtk: 10054 to-endpoint FORWARDED (TCP Flags: ACK, PSH)
Apr 16 13:27:46.578: kube-system/kube-dns-6465f78586-xhgtk:10054 -> 10.168.0.2:40534 to-stack FORWARDED (TCP Flags: ACK, PSH)
Apr 16 13:27:46.578: kube-system/kube-dns-6465f78586-xhgtk:100 -> 10.168.0.2:40534 to-stack FORWARDED (TCP Flags: ACK, FIN)
Apr 16 13:27:46.578: 10.168.0.2:40534 -> kube-system/kube-dns-6465f78586-xhgtk: 10054 to-endpoint FORWARDED (TCP Flags: ACK, FIN)
Apr 16 13:27:47.066: 10.168.0.2:40536 -> kube-system/kube-dns-6465f78586-xhgtk: 10054 to-endpoint FORWARDED (TCP Flags: SYN)
Apr 16 13:27:47.066: kube-system/kube-dns-6465f78586-xhgt 10054 -> 10.168.0.2:40536 to-stack FORWARDED (TCP Flags: SYN, ACK)
Apr 16 13:27:47.066: 10.168.0.2:40536 -> kube-system/kube-dns-6465f78586-xhgtk:1 to-endpoint FORWARDED (TCP Flags: ACK)
Apr 16 13:27:47.067: 10.168.0.2:40536 -> kube-system/kube-dns-6465f78586-xhgtk:10054 to-endpoint FORWARDED (TCP Flags: ACK, PSH)
Apr 16 13:27:47.067: 10.168.0.2:40536 -> kube-system/kube-dns-6465f78586-xhgtk:10054 to-endpoint FORWARDED (TCP Flags: ACK, FIN)
Apr 16 13:27:47.067: kube-system/kube-dns-6465f78586-xhgtk: 10054 -> 10.168.0.2:40536 to-stack FORWARDED (TCP Flags: ACK, PSH)
Apr 16 13:27:47.067: kube-system/kube-dns-6465f78586-xhgtk:10054 -> 10.168.0.2:40536 to-stack FORWARDED (TCP Flags: ACK, FIN)
Apr 16 13:27:47.306: kube-system/kube-dns-6465f78586-xhgtk:44286 -> 34.94.3.74:443 to-stack FORWARDED (TCP Flags: ACK)

Hubble propose également une interface graphique appelée Hubble UI qui offre une représentation graphique des flux réseaux, tout en affichant en partie basse la liste des échanges observés à un instant donné.

En complément de cette partie dédiée à l’observabilité, Cilium propose également des fonctionnalités de sécurité qui reposent sur l’évaluation de CRD de type CiliumNetworkPolicy. La force de Cilium est de pouvoir restreindre le trafic réseau aussi bien au niveau de la couche 3, 4 et 7 du modèle OSI.

Au niveau de la couche 3 (Réseau), l’exemple suivant montre comment autoriser tous les endpoints ayant le label app=myService à communiquer avec une l’IP externe 20.1.1.1 et aussi avec le CIDR 10.0.0.0/8 à l’exception du CIDR 10.96.0.0/12.

apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "cidr-rule"
spec:
endpointSelector:
matchLabels:
app: myService
egress:
- toCIDR:
- 20.1.1.1/32
- toCIDRSet:
- cidr: 10.0.0.0/8
except:
- 10.96.0.0/12

Tout à fait classiquement, il est également possible de restreindre les échanges de données au niveau de la couche 4 (Transport). Ici, les endpoints disposant du label app=myService ne peuvent envoyer de paquet TCP que vers le seul port 80.

apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "l4-rule"
spec:
endpointSelector:
matchLabels:
app: myService
egress:
- toPorts:
- ports:
- port: "80"
protocol: TCP

Enfin, des restrictions peuvent être appliquées au niveau de la couche 7 (Application), et plus précisément au protocole HTTP. Très classiquement, celles-ci peuvent porter sur le verbe HTTP employé (GET, PUT, POST, DELETE, PATCH) ou sur le chemin ciblé sur le serveur. Ici, seules les requêtes GET avec le chemin /public sont autorisées au endpoint ayant le label env:prod.

apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "rule1"
spec:
description: "Allow HTTP GET /public from env=prod to app=service"
endpointSelector:
matchLabels:
app: service
ingress:
- fromEndpoints:
- matchLabels:
env: prod
toPorts:
- ports:
- port: "80"
protocol: TCP
rules:
http:
- method: "GET"
path: "/public"

Conclusion

Nous avons montré qu’une partie des fonctionnalités offertes par des Service Mesh à l’aide de sidecars étaient à présent implémentées par des compétiteurs à l’aide d’eBPF. En effet, cette manière d’étendre le fonctionnement du noyau Linux offre de multiples avantages :

  • une empreinte mémoire réduite, et une consommation de ressource CPU faible ;
  • un faible impact sur le traitement des requêtes, l'interception n’ajoutant qu’une très faible latence ;
  • une stabilité importante, en regard des interactions non maîtrisées entre sidecars.

Cependant, il convient de souligner une fois de plus que la maturité des solutions basées sur eBPF est encore bien loin d’égaler celle des acteurs bien installés dans le paysage Kubernetes tels qu’Istio ou Linkerd. Plus encore, nous pensons que tous les cas d’usage ne vont pas se développer de la même manière à l’aide d’eBPF. En effet, eBPF est extrêmement adapté à l’observabilité. De notre point de vue, il ne fait aucun doute que son adoption dans ce domaine sera rapide, y compris du côté des Service Mesh actuels. Istio a d’ailleurs déjà commencé à s’intéresser à cette approche. Il en est de même pour les redirections les plus simples au niveau socket. eBPF permet d’analyser et de rediriger le trafic de manière extrêmement fiable et rapide.

Il reste cependant un certain nombre de cas d’usages où les sidecars ont encore toute leur place. Quand les décision sont complexes à prendre au niveau des proxies, quand il faut avoir recours à des services tiers d’authentification ou d’autorisation par exemple, alors cela n’a plus de sens d’évaluer les décisions au niveau du noyau linux… et les sidecars gardent alors toute leur légitimité de notre point de vue.

En résumé, plus la logique d’un cas d’usage est simple, plus elle aura de chances de pouvoir être implantée avec succès à l’aide de eBPF. A l’opposée, plus la complexité sera importante et plus elle mettra en jeu des appels à des services tiers pour s’exécuter, plus les sidecars garderont leur intérêt.

De notre point de vue, il ne fait pas de doute qu’une partie de l’écosystème des Service Mesh va être progressivement réécrite pour augmenter les performances sur certains cas d’usages et en premier lieu l’observabilité. Parallèlement à cela, le développement de challengers totalement basés sur eBPF va vraisemblablement s’accélérer… et en premier lieu Cilium, qui est à ce jour le projet le plus avancé, va poursuivre sa croissance. Il nous semble donc important de garder un œil sur ces technologies afin de voir comment le paysage des Service Mesh va évoluer et se recomposer dans les mois et les années à venir.


Note : les exemples présentés dans cet article sont tirés de la documentation de Cilium et d’une interview de son fondateur Thomas Graf.

Photo de Hansbenn publiée sur Pixabay.