Alle Artikel
Einen K8s-Operator für Managed-Cluster-Nodes bauen
Wir haben einen Kubernetes-Operator gebaut, der die OCI-Runtime auf Managed Nodes austauscht – so laufen WebAssembly-Workloads auch auf gewöhnlichen AWS-, Azure- und GCP-Clustern.
·
Max Schmidt

Hinweis: Dieser Beitrag wurde aus dem Englischen übersetzt. Fachbegriffe, Code und Befehle wurden bewusst im Original belassen.
Einführung
Wir haben eine Lösung entwickelt, die ein privilegiertes Kubernetes-DaemonSet nutzt, um Befehle auf den Cluster-Nodes auszuführen. Das war jedoch nicht flexibel genug, weshalb wir einen Operator gebaut haben, der Ihre Kubernetes-Nodes anhand einer Annotation automatisch provisioniert. Den Operator finden Sie hier.
Einfach gesagt: Ziel dieses Projekts ist es, WebAssembly auf Kubernetes einfach und unkompliziert ausprobieren zu können. Wenn Sie das in einem Produktionsszenario einsetzen möchten, sprechen Sie uns bei Liquid Reply an! Wir arbeiten gerne mit Ihnen zusammen.
Der Installer
Der Operator deployt auf den Nodes einen Job, der das Docker-Image von https://github.com/KWasm/kwasm-node-installer verwendet. Dieses Docker-Image installiert crun mit WasmEdge-Unterstützung auf dem Node. Es ist außerdem dafür zuständig, die ContainerD-Konfigurationsdatei anzupassen und ein neues WasmEdge-Plugin hinzuzufügen, das anschließend von der RuntimeClass genutzt wird. Mehr über den Installer erfahren Sie über den Link oben.
DaemonSet vs. Operator
DaemonSet | Operator |
Installer im InitContainer, dauerhaft laufender Pause-Container erforderlich | Job vom Operator läuft einmal und wird nach Erfolg beendet |
Alle Nodes werden gleichzeitig provisioniert – kann den gesamten Cluster lahmlegen | Annotation-basiert – ein Node nach dem anderen möglich; alle Nodes lassen sich auch automatisch provisionieren |
Affinity/NodeSelector, um nur bestimmte Nodes zu provisionieren = manuelles Scheduling der Workloads | Automatisches Workload-Scheduling anhand eines Labels nach erfolgreicher Provisionierung durch den Operator |
Wir empfehlen, den Operator im Cluster zu installieren und jeweils einen Node nach dem anderen zu labeln, um Ausfälle zu vermeiden.
Welcher Operator-Typ ist die richtige Wahl?
Es stehen mehrere Operator-Frameworks zur Auswahl. Das für den jeweiligen Anwendungsfall passende auszuwählen, ist entscheidend.
Es gibt Operator-Frameworks für Python (Kopf), Java (java-operator-sdk), Rust (kube-rs), .NET (KubeOps) und Go (Operator-SDK) sowie einfachere Frameworks, um Anwendungen „nur" auszurollen. Mit dem Operator-SDK können Sie Ihren Operator je nach Anwendungsfall in Go, mit Helm oder sogar mit Ansible umsetzen.
Da unser Anwendungsfall eher komplex ist, haben wir uns für das Operator-SDK mit einem Go-basierten Operator entschieden. Zudem ist die Operator-SDK-Community recht groß und das Projekt gut dokumentiert.
So funktioniert unser Operator
Da wir ein DaemonSet vermeiden wollten, das weiterläuft, obwohl der Node bereits für WebAssembly-Unterstützung provisioniert ist, haben wir uns entschieden, die Nodes über Kubernetes-Jobs zu provisionieren.
Das bedeutet: Unser Operator muss eine Logik implementieren, die einen Kubernetes-Job erzeugt, sobald ein Node im Cluster die Annotation „kwasm.sh/kwasm-node=true" trägt. Damit das funktioniert, muss die Funktion „SetupWithManager" des Operator-SDK so angepasst werden, dass sie auf Node-Events lauscht. Erreichen lässt sich das mit einem einfachen „For(&corev1.Node{}).". Anschließend wird bei jeder Node-Aktion (Update, Delete, Create) die Reconcile-Funktion für den Node ausgelöst. Diese Funktion prüft, ob das Label gesetzt ist; falls ja, muss der Job mit einem NodeSelector erzeugt werden, der auf den Node zeigt, für den das Reconcile ausgeführt wird. Nach dem Anlegen des Jobs wird zudem ein Label auf diesem Node gesetzt. Trägt ein Node bereits Annotation und Label, wird ein künftiges Reconcile übersprungen, da der Node bereits provisioniert ist. Der Job, der auf dem annotierten Node ausgerollt wird, läuft privilegiert, unter HostPID und mountet sogar das Root-Verzeichnis des Nodes. Denken Sie daran: Dies ist lediglich ein Proof of Concept dessen, was möglich ist.
Die Reconcile-Funktion des Provisioners
Wie oben beschrieben, haben wir uns für zwei verschiedene Reconciler entschieden: einen für das Deployment des Jobs und einen Reconcile-Controller, der die ausgerollten Job-Objekte überwacht. Der Provisioner-Reconciler ist das Herzstück des Operators. Er verarbeitet die Node-Annotationen und lauscht auf jede Node-Änderung. Sobald wir den Node mit unserer Annotation kwasm.sh/kwasm-node=true versehen, wird die Reconcile-Funktion ausgelöst. Am wichtigsten ist es, die beiden benötigten Annotationen als Konstante zu hinterlegen.
Im Reconcile müssen wir dann prüfen, ob beide Annotationen gesetzt sind. Ist das der Fall, ist der Node bereits provisioniert und kann übersprungen werden.
Erhält ein Node die Annotation zum ersten Mal, soll ihm ein Label aus der zuvor definierten Konstante nodeNameLabel zugewiesen werden. Außerdem wird eine Funktion ausgeführt, die die Kubernetes-Manifeste für den auszurollenden Job erzeugt. Die Funktion für das Deployment des Jobs sprengt den Rahmen dieses Beitrags, Sie finden sie jedoch hier
Natürlich brauchen wir auch eine Logik, die den Job entfernt, wenn die Annotation vom Node entfernt wird.
Die Reconcile-Funktion des Jobs
Derzeit dient der Reconcile-Controller für die Jobs ausschließlich dazu, den Status des ausgerollten Jobs an den Provisioner-Controller zu melden. Wir haben eine Funktion isJobFinished erstellt, die die Statusbedingung des Jobs an die Reconciler-Funktion zurückgibt. Diese wertet den Rückgabewert von isJobFinished aus und protokolliert „Ongoing", „Failing" oder – bei abgeschlossenem Job – „Completed".
Automatische Provisionierung
Wir haben eine Funktion ergänzt, damit Sie Ihre Nodes nicht manuell labeln müssen. Um sie zu nutzen, installieren Sie das Helm-Chart mit dem Wert kwasmOperator.autoProvision auf „true". Beachten Sie jedoch: Damit wird sofort auf allen Nodes Ihres Clusters WebAssembly-Unterstützung installiert!
Teilen