Sebastian Wöhrl

Lesezeit: 12 Minuten

Techblog

Operatoren für Kubernetes entwickeln

Kubernetes-Operatoren erweisen sich als nützlich, wenn die Cloud-Umgebungen hybrid sind und auf komplexen Architekturen beruhen, die mehrere Mandanten bedienen müssen. Dieser Beitrag zeigt auf, wie Operatoren durch Automatisierung einen DevOps-Ansatz unterstützen.

Techblog

Kubernetes-Operatoren sind seit einigen Jahren nicht nur ein heißes Thema in der Cloud-Native-Community. Auch in Kundenprojekten erweisen sie sich als äußerst nützlich, insbesondere wenn deren Cloud-Umgebungen hybrid sind und auf komplexen Architekturen beruhen, die mehrere Mandanten bedienen müssen (Hybrid-Cloud-Multi-Tenant-Szenarien).

Dieser Beitrag zeigt auf, wie Operatoren durch Automatisierung einen DevOps-Ansatz unterstützen, wie sie durch einheitliche APIs Hybrid-Cloud-Szenarien ermöglichen und ob sich Go, Rust oder Python besser für die Entwicklung von Kubernetes Operatoren eignen.

Automatisieren mit Operatoren

Das Kubernetes Operator Pattern ist ein Weg, die Funktionalität und API von Kubernetes mit custom resources, also um eigene benutzerdefinierte Datentypen, zu erweitern. Dank dieser Operatoren sind Kubernetes-Nutzer: innen und Admins in der Lage, komplexe – und oft verteilte – Systeme wie Message Broker oder Datenbank-Cluster zu deployen und zu managen. Abstrakt gesprochen gießt ein Operator das Wissen und die Prozesse einer erfahrenen menschlichen Betriebsmannschaft in Software. Mit diesem Code lassen sich etliche Aufgaben zum Betrieb eines Systems automatisieren, für andere Aufgaben benötigt die Betriebsmannschaft weniger Wissen, Erfahrung und Zeit.

Beispiel: Opensearch Operator

Schauen wir uns als Beispiel den kubernetes operator für Opensearch an, den MaibornWolff federführend mitentwickelt. OpenSearch ist ein unter der Apache-Lizenz lizenzierter Fork von Elasticsearch, den Unternehmen wie AWS unterstützen.

Vereinfacht gesprochen kümmert sich der Operator um das Deployment eines OpenSearch-Clusters mit seinen Knoten, zusammen mit einer Instanz von OpenSearch Dashboards, der OpenSearch Fork von Kibana. Dabei folgt er einem Standardablauf: Der Operator installiert eine Custom Resource Definition (CRD), die eine Custom Resource (CR) namens OpenSearchCluster in Kubernetes bereitstellt. Nutzer: innen können mit dem Operator kommunizieren, indem sie Objekte dieses Typs mit ihren normalen Abläufen zu Kubernetes hinzufügen. Das kann durch ein einfaches kubectl apply geschehen oder mit High-Level-Tools wie Helm oder FluxCD. Basierend auf den Optionen, die in dem Objekt angegeben sind, wird der Operator einen OpenSearch-Cluster deployen und verwalten.

So hilft der Opensearch Operator beim DevOps-Betrieb

Ein einfaches Deployment hätte auch mit einem Helm-Chart erledigt werden können (und es gibt tatsächlich ein offizielles Chart dafür), aber der Operator kümmert sich noch um viel mehr. Hier einige Szenarien:

  • Du möchtest die Kommunikation innerhalb deines Clusters mit TLS absichern, dich aber nicht um das Generieren der Zertifikate kümmern? Kein Problem, der Operator kann das für dich erledigen, wenn du eine entsprechende Option in das Custom Object einfügst.
  • Du möchtest deine eigenen User und Rollen in OpenSearch konfigurieren, was über die securityconfig des opensearch-securityconfig-Plugins erledigt wird? Speichere sie in einer normalen Kubernetes ConfigMap, füge den Namen der ConfigMap zum Custom Object hinzu – der Operator übernimmt den Rest,und wird reagieren, wann immer du die securityconfig in der ConfigMap änderst.
  • Möchtest du den Cluster runterskalieren und Knoten entfernen, ohne Daten oder gar Schlaf zu verlieren?  Ändere einfach die konfigurierte Anzahl an Knoten im Custom Object. Der Operator wird den Cluster einen Knoten nach dem anderen runterskalieren und zuvor alle Daten umziehen, indem er den betroffenen Knoten leert (sogenanntes draining).
  • Du möchtest deinen Cluster auf eine neue Version von OpenSearch upgraden oder die CPU- oder Speicher-Konfiguration der Knoten ändern? Der Operator wird die Änderungen umsetzen, indem er einen Knoten nach dem anderen neu startet und dazwischen jeweils wartet, dass der Cluster sich stabilisiert, alle Daten wieder voll repliziert sind und der Cluster einen grünen Status zeigt.

Natürlich könnten all diese Aufgaben entweder per Hand erledigt oder auf eine andere Art automatisiert werden, etwa durch Skripte, die Nutzer:innen oder Admins schreiben. Aber das frisst viel Zeit, sei es um die Aufgaben tatsächlich auszuführen oder um die Automatisierungs-Skripte zu schreiben. Zudem müssten die Admins dafür viel Domänenwissen mitbringen.

Darum greift hier ein Vorteil, wenn man auf den OpenSearch Operator setzt: Andere haben auf Basis ihres Wissens und ihrer Erfahrung mit OpenSearch sich um diese Automatisierung gekümmert und sie auf einfache und (mehr oder weniger) standardisierte Art nutzbar gemacht.

Neben dem OpenSearch Operator gibt es unzählige weitere Operatoren, die sich mit Tools und verteilten Systemen beschäftigen. Auch sie gießen Betriebswissen in Code, mit dem die Nutzer:innen über Kubernetes Custom Resources interagieren. Die Operatoren haben unterschiedliche Reifegrade, einige sind sehr simpel und ersetzen nur den Helm-Install-Befehl. Andere verwalten den gesamten Lebenszyklus ihres Zielsystems.

Operatoren managen externe System

Operatoren können auch eine einheitliche, Kubernetes-native und deklarative API für ihre Plattform bereitstellen. In vielen Situationen ist Kubernetes die Basis einer kompletten Plattform für das Unternehmen, die oft viele Cluster und Benutzer: innen mit Infrastruktur- und Plattformdiensten wie Datenbanken, Kafka, Objektspeicher und vielen anderen umfasst.

Die meisten dieser Dienste verfügen über eine eigene API, sei es die Programmierschnittstelle eines Cloud-Anbieters wie AWS, Azure oder GCP, oder die von Produkten wie PostgreSQL oder Kafka bereitgestellte. Wer eine solche Plattform nutzt, muss auf mehrere APIs zugreifen und diese automatisieren, was mehr Komplexität und größeren Aufwand nach sich zieht.

Als Lösung setzt sich durch, alle APIs unter dem Dach von Kubernetes zu vereinheitlichen. Hier kommen die Operatoren ins Spiel. Sie können Custom Resources für jeden Plattformdienst bereitstellen, so dass die Benutzer: innen diese Dienste genauso verwalten können wie ihre Kubernetes-Workloads, etwa Deployments, Pods oder Secrets. So könnte eine Custom Resource PostgreSQLServer einen neuen PostgreSQL-Server bereitstellen, indem sie ein Custom Object in Kubernetes einspielt.

Möchtest du die Größe des Servers ändern?  Ändere die Definition im Custom Object, der Operator wird sich um die Änderung kümmern. Möchtest du separate Datenbanken innerhalb dieses Servers für einzelne Dienste bereitstellen, so dass du einen Server pro Team mit separaten Datenbanken pro Dienst hast?  Erstelle einige Custom Objects vom Typ PostgreSQLDatabase.

Du kannst dieses Muster nach Bedarf für andere Plattformdienste wie Kafka-Topics, Objektspeicher-Buckets, Zugriff auf MQTT-Broker oder alles Mögliche verwenden. Anstatt Terraform-Code für die Datenbank und benutzerdefinierte Skripte für die Orchestrierung von Kafka zu schreiben, kannst du alles mit Kubernetes-YAMLs erledigen. Die Bereitstellungs- und Verwaltungslogik wird nur einmal im Operator implementiert. Die Benutzer: innen der Plattform schreiben nur ein paar einfache YAML-Dateien, anstatt verschiedene Infrastruktursprachen und APIs lernen und verwenden zu müssen.

Kubernetes YAMLs: Die Vorteile einer einheitlichen API

Eine einheitliche API ist nur ein Vorteil dieses Ansatzes. Wer Kubernetes-YAMLs verwendet, kann sie genauso behandeln wie „normale“ Kubernetes-Implementierungen. Das heißt, du kannst sie mit denselben Mechanismen und Workflows deployen, etwa mit Helm und FluxCD. Verwendest du bereits eine GitOps-ähnliche Automatisierung für deine Deployments, passt die Verwaltung von Plattformdiensten über benutzerdefinierte Ressourcen und Operatoren genau dazu.

Ein weiterer Vorteil der vereinheitlichten API ist, dass du Abstraktionen erhältst. Nehmen wir an, deine Plattform läuft auf verschiedenen Cloud-Anbietern und vielleicht sogar on-premise, aber du möchtest die verwalteten Angebote der Cloud-Anbieter (z. B. Managed Postgres) so weit wie möglich nutzen. Die Bereitstellung eines Dienstes ist für jede Cloud anders und hängt von den benutzerdefinierten APIs der Anbieter ab.

Sobald du aber Kubernetes Custom Resources als einheitliche API verwendest, kannst du die meisten Unterschiede zwischen den Umgebungen und Bereitstellungsansätzen abstrahieren und dem Benutzer dieselbe Schnittstelle bieten – unabhängig vom Bereitstellungsort.

Benutzer: innen interessieren die Abläufe hinter einer PostgreSQL-Datenbank nicht, solange diese das PostgreSQL-Protokoll spricht und sich wie ein PostgreSQL-Server verhält.

In der Realität bleiben einige Unterschiede bestehen: Ist eine Funktion, die von einer Cloud-Plattform unterstützt wird, nicht on-premise oder auf einer anderen Cloud-Plattform verfügbar, muss der Operator die Konfiguration in Umgebungen, die diese Funktion nicht unterstützen, je nach Situation entweder ignorieren oder durch schlechtere Alternativen nachbauen.

Code_Operator_Kubernetes

Sprachen und Frameworks, um Operatoren zu entwickeln

Kubernetes Operatoren lassen sich theoretisch in jeder Sprache, für die eine Kubernetes Client Library existiert, entwickeln. Du könntest auch eine eigene Library schreiben. In der Praxis empfiehlt sich aber der Einsatz eines Frameworks, das den langweiligen Kram wegabstrahiert und es dir erlaubt, dich auf die Logik des Operators zu konzentrieren. Bei MaibornWolff entwickeln wir seit längerer Zeit Operatoren für Kunden in verschiedenen Sprachen und mit verschiedenen Frameworks. Aus unserer Perspektive bieten sich dafür drei Sprachen beziehungsweise Frameworks an:

Go: Weit verbreitet und viel Hilfe aus der Community

Für einen neuen Operator steht Go ganz oben auf der Liste der Möglichkeiten. Da Kubernetes selbst in Go geschrieben ist, gibt es eine Menge offizieller Tools für die Integration mit Kubernetes. Außerdem ist Go die bekannteste Sprache für Cloud-native Tools wie Kubernetes, Terraform und Prometheus. Viele Menschen verwenden Go. Es ist einfach, Dokumentation zu finden, Hilfe aus der Community zu erhalten oder eine Library eines Drittanbieters zu finden, die praktische Zusatzfunktionen bietet.

Die Basis der Operator-Entwicklung mit Go ist die controller-runtime von der Kubernetes API Machinery SIG. Sie stellt die grundlegenden Bausteine bereit, um Operatoren zu entwickeln und kann mit verschiedenen Frameworks benutzt werden: Die bekanntesten sind kubebuilder und operator-sdk. Während kubebuilder von denselben Menschen wie controller-runtime entwickelt wurde, steht hinter operator-sdk hauptsächlich RedHat.

Rust: Für sichere und zuverlässige Operatoren

Rust legt als Programmiersprache den Fokus auf Geschwindigkeit und Sicherheit und wird oft für systemnahe und Embedded-Entwicklung benutzt. Dennoch gibt es eine sehr aktiv entwickelte Kubernetes Library namens kube-rs, die auch Controller unterstützt und Anleihen von der controller-runtime nimmt.

Die Kombination von Rust und kube-rs als Framework ist nicht so weit verbreitet wie Go und controller-runtime und hat weder viel unterstützendes Tooling noch eine große Community, die Hilfe leisten könnte. Setzt deine Organisation Rust bereits ein oder plant das, lassen sich damit sehr einfach sichere und zuverlässige Operatoren entwickeln – nicht zuletzt dank der Sicherheitsgarantien wie dem Borrow Checker. Abhängig von der Umgebung und der Compliance-Situation, in der der Operator laufen soll, kann das ein wichtiger Faktor sein.

Schnell loslegen mit Python

Python und das kopf-Framework bieten mehr Abstraktionen und Helferlein als die anderen Frameworks, so dass du schnell loslegen und einfache Operatoren entwickeln kannst. Das ist für simple Interaktionen mit Kubernetes nützlich: wenn es nur gilt, auf Änderungen in den Objekten zu reagieren und Status-Objekte zu setzen, aber der Großteil des Codes und der Logik mit Systemen außerhalb von Kubernetes interagiert, die sowieso von ihren eigenen APIs gesteuert werden.

Da Python bereits sehr häufig für Infrastruktur-Skripte/Automatisierung verwendet wird, gibt es eine Fülle von Bibliotheken für jedes erdenkliche System, so dass es eine gute Wahl ist, wenn du sowieso aus der Ecke kommst. Zum Beispiel haben AWS, Azure und GP jeweils offizielle SDKs für Python.

Go, Rust oder Python: Was denn nun?

Aus unserer persönlichen Erfahrung mit dem Schreiben von Operatoren wird Go in Kombination mit kubebuilder und controller-runtime oft benutzt. Code in Go zu schreiben kann sehr mühsam sein, da diese Sprache nicht für ihren prägnanten Code bekannt ist. Zudem können Feature-Limitierungen in Go selbst komische Probleme zur Laufzeit verursachen. Ein Beispiel: Zum Zeitpunkt der Veröffentlichung dieses Beitrags wurden Generics frisch mit Go 1.18 eingeführt. Viele Bibliotheken nutzen jedoch noch immer interface {} und führen Typprüfungen erst zur Laufzeit durch.

Für Fans von Rust ist das Schreiben von Operatoren mit kube-rs eine Freude. Die kann allerdings getrübt werden, weil Bibliotheken zur Interaktion mit externen Systemen nur in beschränkter Zahl verfügbar sind.

Mit Python und kopf ist es sehr einfach, loszulegen und Ergebnisse zu erzielen. Daher sind sie perfekt für kleine Operatoren. Aber wenn es darum geht, sicheren Code zu schreiben, kann Python schmerzhaft sein.

Zusammenfassend lässt sich sagen:

  1.  Go und kubebuilder/operator-sdk sind ein guter Default für das Schreiben von Operatoren, wenn man Go bereits kennt und mag oder es lernen will.
  2.  Rust und kube-rs haben ihre Nischen.
  3. Python und kopf sollten für Automatisierungsaufgaben oder als allgemeine Programmiersprache erste Wahl sein, wenn in diesen Situationen Python in deiner Organisation schon etabliert ist.

Operatoren aus der Feder von MaibornWolff

Hier könnt ihr euch Beispiele für von und mit MaibornWoff entwickelte Operatoren anschauen:


Über den Autor

Sebastian Wöhrl

Senior Lead IT Architect

Sebastian arbeitet seit 2015 bei MaibornWolff und gestaltet und entwickelt als IT-Architekt Plattformen für (Industrial-)IoT-Usecases, meist auf Basis von Kubernetes, mit einem Fokus auf DevOps und Datenverarbeitungspipelines. Dabei implementiert er als technischer Experte nicht nur maßgeschneiderte Eigenbaulösungen mit seinen Lieblingssprachen Python und Rust, sondern arbeitet auch federführend an verschiedenen meist von MaibornWolff initiierten Opensource-Projekten, die er im Projektalltag wieder zum Nutzen der Kunden einsetzt.