Kubernetes Operator: Operations-Wissen als Code

Keine Kommentare

In diesem Artikel erkläre ich, was ein Kubernetes Operator ist und wie er aufgebaut ist. Anschließend zeige ich euch, wie man seinen ersten eigenen Kubernetes Operator in Go schreibt.

Was ist ein Kubernetes Operator

Ein Kubernetes Operator hilft, eine Applikation, die auf Kubernetes läuft zu verpacken, zu verwalten und zu deployen. Der Operator wird über das Standard-Tooling (kubectl und Kubernetes API) deployed und verwaltet.

Die Idee ist, das Wissen von Operations-Teams in Software zu verpacken, die benötigte Schritte automatisiert ausführen kann.

So kann zum Beispiel ein Elasticsearch-Cluster erstellt werden, bei dem man die Größe des Clusters automatisch anpassen kann. Auch können automatisiert Updates installiert werden. Das geschieht dabei über wenige Angaben, wie die zu verwendete Version oder die Anzahl Nodes. Der Operator übernimmt dabei den Rest. So ist es möglich, mit relativ wenig Angaben und ohne Administrations-Wissen das Cluster aufzusetzen.

Operatoren befolgen das Prinzip des “Maturity Model”. Dies bedeutet, der Operator übernimmt am Anfang nur einen kleinen Teil der Arbeit. Je ausgereifter der Operator, desto mehr Funktionen (wie automatisierte Upgrades und Backups, auf Fehler reagieren) übernimmt dieser. Die nachfolgende Grafik veranschaulicht das gut.

Operatoren können mithilfe des Operator-SDK auf Basis von Helm Charts, Ansible Playbooks oder Go-Code erstellt werden. In diesem Blogpost erstellen wir einen Operator mit Go. Falls ihr lieber wissen wollt wie man einen Operator mithilfe eines Helm Charts erstellt, empfehle ich euch den Blogpost “Deploying Helm Charts without Tiller” meines Kollegen Roman.

Maturity Model

Visualisierung “Maturity Model” Quelle: https://github.com/operator-framework/operator-sdk

Was sind CR/CRDs?

Ein Operator besteht aus einer oder mehreren Custom Resources (CR) und einer eigenen Logik in Form eines Controllers. Der Controller einer Ressource ist dafür verantwortlich, den aktuellen Zustand des Kubernetes-Cluster näher zum gewünschten Zustand zu bringen.

CR sind dabei eine Möglichkeit, Kubernetes um eigene Ressourcen zu erweitern. Diese können dann wie normale Kubernetes-Objekte verwendet werden. Man erweitert so die API von Kubernetes um eigene Objekte. Diese lassen sich wie andere Objekte mit Standard-Tools wie kubectl konfigurieren.

Die Spezifikation einer Ressource befindet sich in der Custom Resource Definition (CRD).

Das Operator-SDK unterstützt bei der Erstellung einer CRD, indem es anpassbare, bereits funktionale Code-Templates generiert. Die Business-Logik für diese neue CRD kann direkt im Operator in Form von Go-Code umgesetzt werden.

CRD sind, wie es der Name bereits verrät, eine Möglichkeit, Kubernetes um eigene Ressourcen zu erweitern. Aufgrund dieser Definition können Entwickler eigene CR in Form von YAML-Dateien benutzen/schreiben und diese wie andere Kubernetes-API-Objekte über kubectl hinzufügen. Der Operator übernimmt dann den Rest und macht die nötigen Änderungen im Cluster selbstständig.

 

Primäre/Sekundäre Ressourcen

Bevor wir jetzt unseren eigenen Operator entwickeln, gibt es noch eine Sache zu klären. Der Operator kann verschiedene Ressourcen überwachen. Üblicherweise wird zwischen primären und sekundären Ressourcen unterschieden. Als primäre Ressourcen zählen die selbst erstellten CR. Sekundäre Ressourcen sind Ressourcen, die üblicherweise durch den Operator erstellt und verwaltet werden, um den in den CR beschriebenen Zustand zu erreichen.

Als Beispiel nehmen wir die Kubernetes-Komponente “Deployment”. In einem Deployment kann man eine Anzahl an Replikas definieren. Die Zahl gibt dabei vor, wie viele Pods erstellt werden sollen. In diesem Beispiel wäre das Deployment die primäre und die Pods die sekundäre Ressource.

Die primären Ressourcen (Deployments) entsprechen in diesem Beispiel der CR und die sekundären Ressourcen sind die zu überwachenden Objekte (Anzahl Pods), die zur Erfüllung der Funktion des Operators dienen.

 

Eigener Operator mit Operator-SDK

Die folgende Anleitung basiert auf der aktuell neuesten Version v0.11.0 des Operator-SDK. Das SDK ist für Linux und Mac verfügbar. Eine Windows-Version ist aktuell nicht geplant.

Vorbereitungen

Ihr solltet das Operator-SDK bereits installiert haben. Eine Anleitung und die dazugehörigen Voraussetzungen gibt es im offiziellen Github-Repo. Überprüfen ob ihr es erfolgreich installiert habt, könnt ihr mit folgendem Befehl:

operator-sdk version

 

Projekt-Erstellung

Wir erstellen nun einen PodReplica Operator. Dieser soll wie ein Deployment für ein bestimmtes Image (Busybox, sleep 1h) fungieren. Man soll über das Feld “size” steuern können, wie viele Pods mit dem Busybox-Image vorhanden sein sollten. Wir verwenden hier mit Absicht ein einfaches Beispiel, da es darum geht zu lernen, wie ein Operator funktioniert.

Folgende Befehle werden im Terminal ausgeführt um ein Projekt anzulegen (den example-user sollte man dabei durch den eigenen Github-Usernamen ersetzen):

export GO111MODULE=on # Falls eigene Go-Version < 1.13
operator-sdk new podreplica-operator --repo github.com/example-user/podreplica-operator
cd podreplica-operator

Das Operator-SDK generiert nun folgende Projektstruktur:

├── build
│   ├── bin
│   │ ├── entrypoint
│   │ └── user_setup
│   └── Dockerfile
├── cmd
│   └── manager
│       └── main.go
├── deploy
│   ├── operator.yaml
│   ├── role_binding.yaml
│   ├── role.yaml
│   └── service_account.yaml
├── go.mod
├── go.sum
├── pkg
│   ├── apis
│   │ └── apis.go
│   └── controller
│       └── controller.go
├── tools.go
└── version
└── version.go

Diese beinhaltet zum Beispiel ein Dockerfile (im Ordner build), die später zu deployenden YAML-Dateien (im Ordner deploy) und einige Go-Files.

CRD erstellen

Als nächstes fügen wir unsere neue CRD hinzu:

operator-sdk add api --api-version=app.example.com/v1alpha1 --kind=PodReplica

Dieser Befehl registriert die neue CR “PodReplica” und deren APIs. In der generierten CRD (deploy/crds/app.example.com_podreplicas_crd.yaml) sieht man auch, dass verschiedene Namen für die neue Ressource reserviert werden:

Wir als Entwickler geben den Zielzustand (Spec) eines Objekts in Kubernetes vor, während der Status hingegen den aktuellen Zustand des Objekts zeigt. Der Kubernetes-Scheduler versucht dabei den Zielzustand zu erreichen und zu halten. Dies geschieht mithilfe des Reconcilation Loop. Dieser ist ein zentraler Bestandteil eines Operators – dazu aber später mehr.

Für unsere eigene CR müssen wir die dazugehörigen Felder definieren. Diese werden in den jeweiligen Go-Structs angegeben (zu finden unter pkg/apis/app/v1alpha1/podreplica_types.go).


Wir definieren ein Feld namens Size, um die Größe unserer PodReplica zu bestimmen. Im Status definieren wir das Feld Replicas, welches ein Slice (ein dynamisches Array) von allen erstellten Podnamen enthält.

Ein Feld besteht aus einem Namen, dem Typ und dem Namen, den es bei der De-/Serialisierung zu JSON haben sollte.

Nach jeder Anpassung dieser *types.go-Dateien müssen die folgenden zwei Befehle ausgeführt werden:

// Updated den generierten Code
operator-sdk generate k8s
// Updated die CRD und die OpenAPI Validierungen
operator-sdk generate openapi

Nach der Ausführung dieser beiden Befehle sieht man, dass unsere CRD (deploy/crds/app.example.com_podreplicas_crd.yaml) um die entsprechenden Felder erweitert wurde.

Controller erstellen

Nachdem die CRD erstellt wurde, wollen wir nun auch noch den dazugehörigen Controller erstellen, der unsere Businesslogik enthält:

operator-sdk add controller --api-version=app.example.com/v1alpha1 --kind=PodReplica

In diesem generierten Controller (pkg/controller/podreplica/podreplica_controller.go) müssen wir angeben, welche Ressourcen (siehe Primäre/Sekundäre Ressourcen) wir überwachen wollen. Für diese Ressourcen wird der Controller bei jeder Änderung benachrichtigt. Standardmäßig werden die erstellten CR (in unserem Fall die PodReplicas) und die daraus resultierenden Pods überwacht (siehe Code-Ausschnitt).

// Watch for changes to primary resource PodReplica
err = c.Watch(&source.Kind{Type: &appv1alpha1.PodReplica{}}, &handler.EnqueueRequestForObject{})

// Watch for changes to secondary resource Pods and requeue the owner PodReplica
err = c.Watch(&source.Kind{Type: &corev1.Pod{}}, &handler.EnqueueRequestForOwner{
IsController: true,
OwnerType:    &appv1alpha1.PodReplica{},
})

Ausschnitt aus Funktion add

Für unseren Fall passt dies bereits und wir müssen hier nichts weiter anpassen.

Nun fehlt nur noch die eigentliche Logik, um Pods hoch- und runterzufahren und die Namen der Pods im Status unserer CR zu pflegen. Das geschieht in der Funktion “reconcile”. Diese Methode wird immer aufgerufen, wenn sich eine der überwachten Ressourcen ändert.

Wir schauen uns exemplarisch den Code zum Skalieren von Replikas an.

pod replica code sample

Code für Skalierung der Pods

(1) Sobald die Anzahl der aktuell laufenden Pods nicht der definierten Größe aus der Spec entspricht, wird ein Pod-Objekt erstellt.
(2)
Bewirkt, dass unser Operator die Kontrolle über die Laufzeit des Objektes hat und bei Änderungen über diesen Pod benachrichtigt wird (sofern wie bei uns auf diese Ressource eine Watch mit EnqueueRequestForOwner vorhanden ist).
(3) Als letztes wird über den Client der Pod im Kubernetes-Cluster angelegt.

Ich gehe nun nicht weiter auf die Details der Implementierung ein (der Code ist auf Github verfügbar).

Zusammengefasst ist wichtig, sich über folgende Punkte klar zu sein:

  1. Die Funktion “Reconcile “ wird bei jeder Änderung der überwachten Ressourcen aufgerufen.
  2. Erstellt man neue Objekte, sollte die eigene Ressource (dh. PodReplica) der Besitzer der Ressource sein. Dies geschieht mit der Funktion controllerutil.SetControllerReference.
    Damit werden beim Löschen der PodReplica auch alle dazugehörigen Pods automatisch gelöscht.
  3. Die Reconcile-Funktion sollte idempotent sein. Zu einem gegebenen Zustand sollte es keinen Unterschied machen, wie oft man die Funktion aufruft. Sie sollte immer dasselbe Resultat liefern.
    Man sollte zum Beispiel nicht zwischen verschiedenen Events (Create/Delete/Update) unterscheiden.
  4. Um mit der Kubernetes-API zu interagieren, kann man den Client verwenden, welcher vom Operator-SDK zur Verfügung gestellt wird.

Build, Run und Publish

Es gibt zwei Möglichkeiten, wie man den Operator nun starten kann:

1. Operator lokal starten

Während des Entwicklungszyklus macht es mehr Sinn, den Operator lokal zu testen. Ich habe dafür minikube verwendet, werde aber auf die Installation hier nicht weiter eingehen.

Als erstes wird die neue CRD in Kubernetes registriert:

kubectl apply -f deploy/crds/app.example.com_podreplicas_crd.yaml

Nachfolgend setzt man noch den Namen des Operators in einer Umgebungsvariablen:

export OPERATOR_NAME=podreplica-operator

Anschließend startet man den Operator mit folgendem Befehl:

operator-sdk up local --namespace=default

Oder mit aktiviertem Debugging (Delve sollte installiert sein):

operator-sdk up local --namespace=default --enable-delve

 

2. Als Deployment in einem Cluster

Um den Operator in einem Cluster zu verwenden, müssen wir diesen zuerst bauen. Danach pushen wir das resultierende Docker Image in eine beliebige Docker Registry. Wir benutzen https://hub.docker.com als Registry.

operator-sdk build <user>/podreplica-operator:v0.0.1
# Linux
sed -i 's|REPLACE_IMAGE|<user>/podreplica-operator:v0.0.1|g' deploy/operator.yaml
# OSX:
sed -i "" 's|REPLACE_IMAGE|<user>/podreplica-operator:v0.0.1|g' deploy/operator.yaml

docker push <user>/podreplica-operator:v0.0.1

Die Befehle ersetzen den Platzhalter “REPLACE_IMAGE” mit dem gebauten Docker Image <image:tag>. In der Datei deploy/operator.yml sollte nun auf das eben gebaute Image verwiesen werden.

Im Cluster muss die CRD als erstes registriert werden:

kubectl apply -f deploy/crds/app.example.com_podreplicas_crd.yaml

Danach erstellt man die nötigen Berechtigungen (RBAC) und den Operator selbst:

kubectl create -f deploy/service_account.yaml
kubectl create -f deploy/role.yaml
kubectl create -f deploy/role_binding.yaml
kubectl create -f deploy/operator.yaml

Überprüfen, ob das Operator-Deployment läuft:

kubectl get deployments
NAME                  READY   UP-TO-DATE   AVAILABLE AGE
podreplica-operator   1/1     1            1         58s

Überprüfen, ob der Operator-Pod läuft:

kubectl get pods
NAME                                   READY   STATUS    RESTARTS AGE
podreplica-operator-5cd9b67b79-lg7mc   1/1     Running   0        59s

 

PodReplica erstellen

Jetzt steht nichts mehr im Wege, eigene PodReplicas zu erstellen. Wir haben eine Beispiel-Definition einer PodReplica im Ordner (deploy/crds) und können sie mit nachfolgendem Befehl erstellen:

kubectl apply -f deploy/crds/app.example.com_v1alpha1_podreplica_cr.yaml

Jetzt stellen wir noch sicher, dass die Ressource existiert und die Namen der Replica(Pods) eingetragen sind:

kubectl get podreplica/example-podreplica -o yaml

Überprüfen, ob alle Pods funktionieren:

kubectl get pods
NAME                                  READY   STATUS    RESTARTS AGE
example-podreplica-pod2hm4w           1/1     Running   0        7m17s
example-podreplica-pod5g7kh           1/1     Running   0        7m17s
example-podreplica-podl55nq           1/1     Running   0        7m17s
podreplica-operator-5cd9b67b79-lg7mc  1/1     Running   0        37m

 

Größe anpassen

Wir können die “Size” in der .yml-Datei deploy/crds/app_v1alpha1_podreplica_cr.yml anpassen und die Änderung hinzufügen:

cat deploy/crds/app.example.com_v1alpha1_podreplica_cr.yaml

apiVersion: app.example.com/v1alpha1
kind: PodReplica
metadata:
  name: example-podreplica
spec:
  size: 4
kubectl apply -f deploy/crds/app.example.com_v1alpha1_podreplica_cr.yaml
kubectl get pods

NAME                                   READY   STATUS    RESTARTS AGE
example-podreplica-pod5g7kh            1/1     Running   0        28m
example-podreplica-podjhdhl            1/1     Running   0        3m31s
example-podreplica-podl55nq            1/1     Running   0        28m
example-podreplica-podws7cp            1/1     Running   0        3m31s
podreplica-operator-5cd9b67b79-lg7mc   1/1     Running   0        58m

 

PodReplica/Operator entfernen

So leicht wie das Hinzufügen der Ressource war, ist auch das Löschen:

kubectl delete -f deploy/crds/app.example.com_v1alpha1_podreplica_cr.yaml

Alle relevanten Pods werden nun automatisch heruntergefahren/gelöscht.
Den Operator und seine Berechtigungen löscht man mit folgenden Befehlen:

kubectl delete -f deploy/crds/app.example.com_podreplicas_crd.yaml
kubectl delete -f deploy/operator.yaml
kubectl delete -f deploy/role_binding.yaml
kubectl delete -f deploy/role.yaml
kubectl delete -f deploy/service_account.yaml

 

Fazit

Wir haben es geschafft! Wir haben unseren ersten eigenen Kubernetes Operator erstellt und deployed 😀

Den kompletten Code findet ihr auf Github.

Falls ihr noch weitere Inspiration braucht, gibt es noch einen zweiten Beispiel-Operator der “Chaos” stiftet, indem dieser Pods mit einem vordefinierten Prefix löscht. Diesen gibt es ebenfalls auf Github (ChaosPod Operator).

Falls ihr weitere Infos zum Operator-SDK benötigt, ist das offizielle Repository ein guter Start. Weitere, auch produktiv genutzte Operators können auf folgenden Seiten gefunden werden:

https://operatorhub.io/
https://github.com/operator-framework/awesome-operators

Manuel Wessner

Manuel ist seit 2019 für codecentric am Standort Karlsruhe tätig. Sein Schwerpunkt liegt im Java und Python Umfeld. Er ist ein großer Fan von Clean Code und Refactorings. Zu seinen aktuellen Interessen gehört Chaos Engineering und Go

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.