Dieser Artikel ist Teil einer Reihe.

Kubernetes sicher und transparent: Erste Schritte mit Cilium

In diesem Artikel möchte ich mit euch etwas über extended Berkeley Packet Filter (eBPF) generell und konkret über Cilium lernen. Cilium ist eine auf eBPF basierende OpenSource-Software, die Security-, Netzwerk- und Observability-Features für Kubernetes bereitstellt um die Entwicklung und den Betrieb von Anwendungen in Kubernetes zu erleichtern, beispielsweise eine Service Map und Netzwerkpolicies auf OSI-Schicht 7. Der Kernel-Mechanismus von eBPF wird dabei von Cilium genutzt, um solche Features nachzurüsten, ohne am Code der Anwendungen im Cluster etwas zu ändern.

Um Cilium kennenzulernen, werden wir in diesem Artikel zuerst einen lokalen Kubernetes-in-Docker (kind) -Cluster aufzusetzen, der mit Hilfe von Cilium die oben genannten Features bereitstellt.

Ist das erledigt, schauen wir uns ein paar der Observability- und Security-Features genauer an, die Cilium mitbringt. Wir vergleichen sie mit «vanilla» Kubernetes und schauen auch mal «unter die Haube», um zu verstehen, was wie zusammenspielt. Viel Spaß!

Eine Analogie zum Verständnis von Kubernetes und Cilium

Zum besseren Verständnis von Cilium und eBPF im Kubernetes-Kontext nutzen wir eine kleine Analogie:

Stellen wir uns Kubernetes einfach mal als eine große Stadt vor. Der Verkehr fließt in dieser Stadt in verschiedenen, großen Verkehrsknoten (Nodes) über unterschiedliche Straßen (Services, Netzwerkschnittstellen etc.), um Gebäude und Menschen (Pods) miteinander zu verbinden.

Die generelle Idee ist, dass wir den in diesem Netz laufenden Verkehr überwachen, regeln und analysieren wollen. Dazu möchten wir Ampeln und Kameras aufstellen.

Wir haben hier aber leider ein Problem: Das Straßennetz und die Gebäude dürfen gar nicht angefasst werden. Sie stehen quasi unter Denkmalschutz. Tja, das macht es eigentlich unmöglich, unsere Ziele zu erreichen, nicht wahr?

Nun, hier kommen Cilium und eBPF ins Spiel. Ohne hier technisch zu tief einsteigen zu wollen - das wäre einen eigenen Artikel wert - ermöglichen es die Cilium zugrunde liegenden Technologien, dass wir jetzt dynamisch Ampeln und Kameras für das Straßennetz installieren können. Das Beste daran: Wir können diese an beliebigen Verkehrsknoten, Straßenabschnitten oder Gebäuden aufstellen, ohne dabei den Boden aufreißen oder Gebäude umbauen zu müssen.

Dies ermöglicht es uns, der (zugegebenermaßen ganz schön fiesen) Anforderung gerecht zu werden und den Verkehrsfluss durch die Stadt (Daten und Requests) trotzdem zu beobachten und zu steuern. Wenn beispielsweise unerwartet hoher Verkehr auf einer Straße ist, können wir schnell eine «rote Ampel» einschalten oder Umleitungen ausweisen (Security- und Netzwerk-Regeln).

Cilium nutzt genau diese dynamischen eBPF-Ampeln und -Kameras, um in Kubernetes für Sicherheit, Netzwerksteuerung und Monitoring zu sorgen, ohne in die eigentlichen Anwendungen eingreifen zu müssen. Die eBPF-Applikationen laufen dabei direkt im Kernel-Kontext und werden von Cilium orchestriert, um beispielsweise Netzwerk-Policies durchzusetzen.

Eingesetzte Tools im Überblick

Um komfortabel mit der im weiteren Artikel vorgestellten Umgebung zu arbeiten, setze ich einige installierte Tools voraus:

Bitte nutzt den Package Manager eurer Wahl oder die verlinkten Webseiten, um euch die Tools zu installieren. Es sind nicht wenige Werkzeuge, aber Kubernetes ist und bleibt eben ein Monster kompliziertes und mächtiges Stück Software. Mit den hier gelisteten Werkzeugen wird es um einiges komfortabler.

Cluster-Anpassungen

Bevor wir einen lokalen kind-Cluster aufsetzen, brauchen wir ein paar Grundlagen zum Verständnis, da wir den Cluster etwas anpassen werden. Zuerst einmal: Warum nutzen wir kind? Zuerst einmal: Die Nutzung ist nicht zwingend, auch die Nutzung von minikube ist kein Problem. Wir nutzen es, da Kubernetes selbst im Docker-Container läuft und somit leicht portierbar ist, aber vor allem,weil es bei kind möglich ist, das default-Container-Network-Interface (CNI) zu wechseln.

Das CNI ist in Kubernetes für die Netzwerkanbindung von Pods zuständig. Es weist IP-Adressen zu, verwaltet virtuelle Netzwerk-Interfaces und mehr. Wir müssen das CNI wechseln, da kind als default kindnet verwendet, ein sehr simplistisches CNI.

Dieses bietet zwar mittlerweile Unterstützung für Kubernetes' NetworkPolicy, Cilium nutzt intern aber die CiliumNetworkPolicy und CiliumClusterwideNetworkPolicy , mit denen wir im weiteren Artikel arbeiten werden.

In unserer Stadt-Analogie kann man das CNI als Stadtplanungsamt beschreiben. Es ist grundsätzlich für das Anbinden der Gebäude und die Verwaltung und Freigabe von Straßen zuständig.

Daneben ersetzen wir den kube-proxy durch Ciliums eBPF-Komponente. Kube-proxy regelt vereinfacht gesagt den Verkehr innerhalb von Kubernetes, ist also für Service-Weiterleitung und Load Balancing zuständig.

Dabei nutzt kube-proxy IPTables, um den Netzwerkverkehr an die richtigen Pods weiterzuleiten oder IPTables-Netzwerksegmentierungsregeln an Pods zu verteilen.

Cilium dagegen nutzt eBPF Map-Einträge und kann so gerade in größeren Clustern ein Plus an Performance erreichen, da die IPTables nicht auf jedem Node einzeln aktualisiert werden müssen, wenn sich im Cluster etwas ändert.

In unserer oben aufgespannten Analogie könnte man sich diese Komponente z.B. als Verkehrspolizei vorstellen. Sie sorgt für den richtigen Verkehrsfluss in der Stadt.

Später im Artikel schauen wir uns das nochmal genauer an, jetzt aber genug der Theorie - lasst uns Dinge kaputtmachen!

Einen kaputten kind-Cluster aufsetzen

Wir setzen uns zuerst einen kaputten Kubernetes-Cluster auf, ohne CNI und kube-proxy:

$ kind create cluster --config - <<EOF  
kind: Cluster  
apiVersion: kind.x-k8s.io/v1alpha4  
networking:  
  kubeProxyMode: none # kein kube-proxy  
  disableDefaultCNI: true # kein kindnet CNI
nodes:  
- role: control-plane  
- role: worker  
- role: worker  
- role: worker  
EOF

Unser Cluster besteht aus 4 Nodes. Einer beherbergt die Control Plane, drei weitere Worker-Nodes.

Die Control-Plane ist quasi «das Hirn» von Kubernetes, die Worker-Nodes auf der Data Plane vergleichbar mit dem Körper. Sie erhalten Commands von der Control Plane, und kommunizieren über kubelets miteinander, wobei eigentlich der kube-proxy den Weg, um Services auf Nodes zu erreichen, bereitstellt. Das CNI dagegen ist unter anderem für die Zuweisung von IP-Adressen im Cluster und Inter-Node-Kommunikation zuständig.

Wenn beide Komponenten nicht vorhanden sind, findet im Ergebnis weder zwischen den Nodes noch innerhalb der Nodes Datenverkehr statt. Das äußert sich darin, dass kein Node überhaupt den Status «ready» erreicht. Das so erwartete Verhalten prüfen wir via kubectl get nodes:

$ kubectl get nodes                                                             

NAME                 STATUS     ROLES           AGE   VERSION
kind-control-plane   NotReady   control-plane   85s   v1.32.2
kind-worker          NotReady   <none>          72s   v1.32.2
kind-worker2         NotReady   <none>          72s   v1.32.2
kind-worker3         NotReady   <none>          72s   v1.32.2

«Yay, der Cluster ist kaputt!» 🥳

Gut, nicht wirklich «yay» - aber erwartet. Lösen wir das Problem als nächstes, indem wir die entsprechenden Cilium-Komponenten hinzufügen.

Cilium im kaputten Cluster installieren

Wenn wir Cilium im gerade erstellten Cluster installieren, erwarten wir, dass es unsere (selbst eingebrockten) Probleme löst. Vorausschauend sei gesagt, dass das nicht ohne weiteres ohne den kube-proxy möglich ist, da wir nicht mit dem kube-apiserver über das Service-Interface sprechen können.

Jetzt stellen wir uns aber erst einmal etwas naiv und installieren Cilium. Praktischerweise gibt es ein Helm-Repository, also werden wir Cilium wie folgt installieren:

$ helm repo add cilium https://helm.cilium.io/
$ helm install cilium cilium/cilium --version 1.17.3 \
    --namespace kube-system \
    --set kubeProxyReplacement=true

Wir erhalten die wunderbare Message You have successfully installed Cilium with Hubble.

Aber Stimmt das auch? Schauen wir via K9s in unseren Cluster, sieht es anders aus:

Terminal interface displaying Kubernetes pod statuses with `K9s`. Highlighted pods in orange indicate errors or initialization issues. Includes readiness, status, restart counts, nodes, and pod ages.
Sicht von K9s auf unseren lokalen Cluster

OH nein, Alles rot! Und wieder: Nicht funktionierend, aber wieder erwartet, da wir nicht mit dem API-Server von Kubernetes reden können. Ein Blick in die Cilium-Containerlogs gibt uns entsprechend Aufschluss.

Terminal screen with Kubernetes logs showing connection errors to API server at IP 10.96.0.1:443, citing I/O timeout and failed start in the kube-system namespace.
Cilium-Containerlog mit Timeout

Wir sehen ein Timeout bei der Kommunikation mit dem API-Server. Zur Lösung müssen wir Cilium bei der Installation mitteilen, wo der API-Server aufrufbar ist. Kind unterstützt uns hier, da alle Cluster-Nodes in einem dedizierten Containernetzwerk laufen. Container innerhalb eines Netzwerkes sind von anderen im sleben Netzwerk über ihren Namen erreichbar. Wir müssen also nicht die IP-Adresse des API-Servers herausfinden, sondern nutzen kind-control-plane mit dem Standard-Port 6443.

Der korrekte Befehl zur Installation von Cilium lautet wie folgt:

$ helm install cilium cilium/cilium --version 1.17.3 \
    --namespace kube-system \
    --set kubeProxyReplacement=true \
    --set k8sServiceHost=kind-control-plane \
    --set k8sServicePort=6443

In einer zukünftigen Version wird Cilium auch die Möglichkeit bieten, eine Liste von Endpunkten zu definieren, damit diese wichtige Verbindung hochverfügbar genutzt werden kann.

Via K9s verifizieren wir, dass jetzt alles korrekt ist:

Terminal screenshot of the 'K9s' tool showing Kubernetes pod statuses in 'kube-system' namespace with highlighted pod 'cilium-ln8t8' running on 'kind-control-plane'.
Cilium läuft - endlich!

Zu guter letzt prüfen wir, dass die Konfiguration zum Ersetzen des kube-proxy angewandt wurde. Wir schauen dazu in den Cilium-Agenten und nutzen das Cilium-Debug-CLI.Der Befehl cilium-dbg status --verbose gibt uns wichtige Konfigurations- und Statusmetriken über Cilium aus.

Den konkreten Status des kube-proxy holen wir uns nun via kubectl -n kube-system exec ds/cilium -- cilium-dbg status --verbose | grep KubeProxyReplacement. Das Ergebnis zeigt, dass unsere Konfiguration angewendet wurde:

KubeProxyReplacement:   True   [eth0    ... ...]

Geschafft, wir haben einen Kubernetes-Cluster, der mit der Cilium CNI und kube-proxy Komponente läuft.

Im nächsten Schritt wollen wir «etwas sehen», konkret das im Browser erreichbare UI für Hubble, Ciliums «fully distributed networking and security observability platform». Dort schauen wir uns später die Cilium Star Wars-Demo auf der Servicemap an.

Grundlegende Observability mit Cilium: Die Service-Map

Wir wollen etwas sehen - am besten das, was so in unserem Cluster abläuft. Dafür aktualisieren wir die Cilium-Konfiguration wie folgt:

$ helm upgrade cilium cilium/cilium --version 1.17.3 \
    --namespace kube-system \
    --set hubble.relay.enabled=true \
    --set hubble.ui.enabled=true

Stellt euch Hubble wie einen Event Stream des Clusters vor, der Informationen über den Datenfluss im Netzwerk, die «Flows», beinhaltet. Das Hubble Relay ist für den Multi-Node support zuständig - standardmäßig auf Port 4244.

Mit dem Hubble UI erhalten wir ein Web-Frontend, das uns die Service Map und den Event Stream grafisch darstellt. Die erfolgreiche Installation verifizieren wir kurz via K9s. Zwei neue Pods sollten erscheinen:

Terminal output highlighting kube-system namespace pods, including 'hubble-relay-7bf4f498b-m2gkp' (1/1 Running) and 'hubble-ui-69d69b64cf-285db' (2/2 Running), their IPs, nodes, and uptime of 24 minutes.
Pods für das Hubble-Relay und -UI

Prima!

Um Hubble zu nutzen, gibt es mehrere Möglichkeiten. Wir nutzen folgenden Befehl:

$ cilium hubble ui
ℹ️  Opening "http://localhost:12000" in your browser...

Die CLI-Anwendung übernimmt die Weiterleitung für uns und das Hubble UI öffnet sich im Browser.

Wir wählen zuerst im UI den kube-system-Namespace aus. Hier sehen wir die entsprechende Service Map der Cilium-Komponenten. Unterhalb der Map sehen wir auch den von Hubble erzeugten Eventstream, der den Netzwerkverkehr zeigt:

Network flow diagram showing interactions between 'host' and Kubernetes components ('hubble-ui', 'hubble-relay', 'kube-dns') with TCP ports and connection details displayed.
Service-Map mit Hubble UI

Neben dem UI können wir den Eventstream auch mit dem Hubble-Plugin in unserer Shell verfolgen. Dafür forwarden wir zuerst den entsprechenden Port:

$ cilium hubble port-forward                                                    
ℹ️  Hubble Relay is available at 127.0.0.1:4245

In einer weiteren Shell-Instanz nutzen wir nun hubble observe, um den Eventstream zu sehen. Es ist auch möglich, dem Stream kontinuierlich mit dem flag -f zu folgen oder via -o json den Output als JSON anzuzeigen.

Terminal output showing network traffic logs from the 'hubble observe' command with timestamps, IP addresses, roles, statuses (e.g., FORWARDED), and TCP flags.
Cilium Eventstream in der Shell

Damit haben wir es geschafft: Wir sehen die grundlegenden Observability, die Cilium integriert. Weiteren Experimenten mit Cilium im lokalen kind-Cluster steht nichts mehr im Wege. Vielleicht führen uns diese ja sogar in weit, weit entfernte Galaxien?

Für heute soll das aber erst einmal reichen.

Fazit

In diesem Artikel haben wir gelernt, wie wir Cilium mit Kubernetes aufsetzen. Dabei haben wir gelernt, was das CNI und der kube-proxy eigentlich tun, und uns die ersten grundlegenden Observability-features von Cilium angeschaut.

In Teil 2 dieser Reihe werden wir unseren Cluster zum Leben erwecken und uns die Cilium-Netzwerkpolicies genauer anschauen.