55 Kubernetes Operators

Kubernetes Operators sind eine fortgeschrittene Erweiterung der Kubernetes-Plattform, die es ermöglicht, anwendungsspezifische Betriebsabläufe zu automatisieren und zu standardisieren. Sie stellen eine Methode dar, komplexe, zustandsbehaftete Anwendungen in Kubernetes zu verwalten, indem sie das Domänenwissen eines menschlichen Operators in Software kodieren. Dieses Kapitel erklärt das Konzept der Operators, ihre Funktionsweise, Anwendungsfälle und wie man eigene Operators entwickeln kann.

55.1 Was sind Kubernetes Operators?

Ein Kubernetes Operator ist ein anwendungsspezifischer Controller, der das Kubernetes API erweitert, um komplexe Anwendungen zu erstellen, zu konfigurieren und zu verwalten. Operators folgen dem Kubernetes-Prinzip der Kontrolle durch Beobachtung des aktuellen Zustands und Arbeiten in Richtung eines gewünschten Zustands.

55.1.1 Grundkonzepte:

55.1.2 Warum Operators?

Traditionelle Kubernetes-Ressourcen wie Deployments und StatefulSets reichen oft nicht aus, um komplexe, zustandsbehaftete Anwendungen zu verwalten, die spezifische Kenntnisse für Operationen wie:

Operators schließen diese Lücke, indem sie solche anwendungsspezifischen Prozesse automatisieren.

55.2 Anatomie eines Operators

Ein Kubernetes Operator besteht typischerweise aus mehreren Komponenten:

55.2.1 1. Custom Resource Definition (CRD)

CRDs erweitern die Kubernetes API um benutzerdefinierte Ressourcentypen. Sie definieren das Schema für neue Objekttypen, die die zu verwaltende Anwendung darstellen.

Beispiel einer CRD für eine PostgreSQL-Datenbank:

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: postgresclusters.database.example.com
spec:
  group: database.example.com
  names:
    kind: PostgresCluster
    listKind: PostgresClusterList
    plural: postgresclusters
    singular: postgrescluster
    shortNames:
    - pgc
  scope: Namespaced
  versions:
  - name: v1
    served: true
    storage: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              version:
                type: string
              replicas:
                type: integer
                minimum: 1
              storage:
                type: object
                properties:
                  size:
                    type: string
                  storageClass:
                    type: string

55.2.2 2. Custom Resource (CR)

Eine Custom Resource ist eine Instanz der durch die CRD definierten Ressource, die den gewünschten Zustand der Anwendung beschreibt.

Beispiel einer PostgreSQL-CR:

apiVersion: database.example.com/v1
kind: PostgresCluster
metadata:
  name: example-postgres
spec:
  version: "13.3"
  replicas: 3
  storage:
    size: "10Gi"
    storageClass: "ssd"

55.2.3 3. Controller

Der Controller ist der Kern des Operators. Er überwacht kontinuierlich Custom Resources und führt die notwendigen Aktionen aus, um den gewünschten Zustand zu erreichen.

Typische Aufgaben eines Controllers:

55.3 Operator-Fähigkeitsstufen

Die Operator-Fähigkeitsstufen beschreiben, wie ausgereift und leistungsfähig ein Operator ist:

55.3.1 Level 1: Basic Install

Der Operator kann eine Anwendung installieren und eine grundlegende Konfiguration vornehmen. Dies entspricht in etwa den Fähigkeiten eines Deployment mit einigen zusätzlichen ConfigMaps oder Secrets.

55.3.2 Level 2: Seamless Upgrades

Der Operator kann Upgrades der verwalteten Anwendung durchführen, einschließlich Datenmigration und Dienstkontinuität während des Upgrade-Prozesses.

55.3.3 Level 3: Full Lifecycle

Der Operator übernimmt zusätzliche Lebenszyklusaufgaben wie Backup, Restore, Skalierung und Monitoring mit Anpassungen für spezifische Anwendungsereignisse.

55.3.4 Level 4: Deep Insights

Der Operator sammelt und exportiert detaillierte Metriken und Status-Informationen, bietet spezifische Alarme und Warnungen für anwendungsspezifische Probleme.

55.3.5 Level 5: Auto-Pilot

Der Operator kann automatisch auf komplexe Probleme reagieren, wie Lastspitzen, Knotenausfälle oder andere Störungen, ohne menschliches Eingreifen.

55.4 Bekannte Kubernetes Operators

Es gibt zahlreiche Open-Source-Operators für verbreitete Anwendungen:

55.4.1 1. Prometheus Operator

Verwaltet Prometheus-Monitoring-Stacks, automatisiert die Konfiguration von Prometheus, Alertmanager und Grafana.

apiVersion: monitoring.coreos.com/v1
kind: Prometheus
metadata:
  name: prometheus
spec:
  serviceAccountName: prometheus
  replicas: 2
  serviceMonitorSelector:
    matchLabels:
      team: frontend

55.4.2 2. Elasticsearch Operator

Verwaltet Elasticsearch-Cluster, einschließlich Skalierung, Updates und Monitoring.

apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: logging
spec:
  version: 7.12.0
  nodeSets:
  - name: masters
    count: 3
    config:
      node.roles: ["master"]
  - name: data
    count: 5
    config:
      node.roles: ["data"]

55.4.3 3. etcd Operator

Automatisiert die Verwaltung von etcd-Clustern, einschließlich Erstellung, Skalierung, Upgrades und Disaster Recovery.

apiVersion: etcd.database.coreos.com/v1beta2
kind: EtcdCluster
metadata:
  name: example-etcd-cluster
spec:
  size: 3
  version: "3.4.13"

55.4.4 4. Postgres Operator

Verwaltet PostgreSQL-Cluster mit Funktionen wie Hochverfügbarkeit, automatischen Backups und Point-in-Time-Recovery.

apiVersion: acid.zalan.do/v1
kind: postgresql
metadata:
  name: acid-postgres-cluster
spec:
  teamId: "acid"
  volume:
    size: 10Gi
  numberOfInstances: 3
  postgresql:
    version: "13"

55.5 Entwicklung eines eigenen Operators

Es gibt mehrere Frameworks und Tools, die die Entwicklung von Operators erleichtern:

55.5.1 1. Operator SDK

Das Operator SDK ist ein Framework zur Beschleunigung der Operator-Entwicklung. Es unterstützt mehrere Programmiersprachen und Abstraktionsebenen:

55.5.1.1 Beispiel für die Erstellung eines Go-basierten Operators:

# Operator SDK installieren
export RELEASE_VERSION=v1.16.0
curl -LO https://github.com/operator-framework/operator-sdk/releases/download/${RELEASE_VERSION}/operator-sdk_linux_amd64

# Ein neues Operator-Projekt erstellen
operator-sdk init --domain example.com --repo github.com/example/nginx-operator

# Eine neue API (CRD) erstellen
operator-sdk create api --group demo --version v1 --kind NginxService --resource --controller

# Code implementieren und Operator bauen
make docker-build docker-push

# Operator im Cluster deployen
make deploy

55.5.2 2. KUDO (Kubernetes Universal Declarative Operator)

KUDO vereinfacht die Operator-Entwicklung durch einen deklarativen Ansatz ohne Programmierung.

55.5.3 3. Kubebuilder

Ein Framework zum Erstellen von Kubernetes APIs mit Go, das eine solide Grundlage für Controller bietet.

55.6 Implementierungsdetails eines Operators

Lassen Sie uns genauer betrachten, wie ein einfacher Operator implementiert wird. Dies ist ein vereinfachtes Go-Beispiel für einen Nginx-Operator:

package controllers

import (
    "context"

    appsv1 "k8s.io/api/apps/v1"
    corev1 "k8s.io/api/core/v1"
    "k8s.io/apimachinery/pkg/api/errors"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/types"
    ctrl "sigs.k8s.io/controller-runtime"
    "sigs.k8s.io/controller-runtime/pkg/client"
    "sigs.k8s.io/controller-runtime/pkg/log"

    webserverv1 "github.com/example/nginx-operator/api/v1"
)

// NginxServiceReconciler reconciles a NginxService object
type NginxServiceReconciler struct {
    client.Client
    Scheme *runtime.Scheme
}

func (r *NginxServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    log := log.FromContext(ctx)
    
    // Fetch the NginxService instance
    nginxService := &webserverv1.NginxService{}
    err := r.Get(ctx, req.NamespacedName, nginxService)
    if err != nil {
        if errors.IsNotFound(err) {
            // Request object not found, could have been deleted
            return ctrl.Result{}, nil
        }
        // Error reading the object
        return ctrl.Result{}, err
    }
    
    // Check if the deployment already exists, if not create a new one
    found := &appsv1.Deployment{}
    err = r.Get(ctx, types.NamespacedName{Name: nginxService.Name, Namespace: nginxService.Namespace}, found)
    if err != nil && errors.IsNotFound(err) {
        // Define a new deployment
        dep := r.deploymentForNginx(nginxService)
        log.Info("Creating a new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
        err = r.Create(ctx, dep)
        if err != nil {
            log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
            return ctrl.Result{}, err
        }
        // Deployment created successfully - return and requeue
        return ctrl.Result{Requeue: true}, nil
    } else if err != nil {
        log.Error(err, "Failed to get Deployment")
        return ctrl.Result{}, err
    }
    
    // Ensure the deployment replicas is the same as the spec
    replicas := nginxService.Spec.Replicas
    if *found.Spec.Replicas != replicas {
        found.Spec.Replicas = &replicas
        err = r.Update(ctx, found)
        if err != nil {
            log.Error(err, "Failed to update Deployment", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)
            return ctrl.Result{}, err
        }
        // Spec updated - return and requeue
        return ctrl.Result{Requeue: true}, nil
    }
    
    // Update the NginxService status
    nginxService.Status.AvailableReplicas = found.Status.AvailableReplicas
    err = r.Status().Update(ctx, nginxService)
    if err != nil {
        log.Error(err, "Failed to update NginxService status")
        return ctrl.Result{}, err
    }
    
    return ctrl.Result{}, nil
}

// deploymentForNginx returns a NginxService Deployment object
func (r *NginxServiceReconciler) deploymentForNginx(m *webserverv1.NginxService) *appsv1.Deployment {
    ls := labelsForNginx(m.Name)
    replicas := m.Spec.Replicas

    dep := &appsv1.Deployment{
        ObjectMeta: metav1.ObjectMeta{
            Name:      m.Name,
            Namespace: m.Namespace,
        },
        Spec: appsv1.DeploymentSpec{
            Replicas: &replicas,
            Selector: &metav1.LabelSelector{
                MatchLabels: ls,
            },
            Template: corev1.PodTemplateSpec{
                ObjectMeta: metav1.ObjectMeta{
                    Labels: ls,
                },
                Spec: corev1.PodSpec{
                    Containers: []corev1.Container{{
                        Image: "nginx:" + m.Spec.Version,
                        Name:  "nginx",
                        Ports: []corev1.ContainerPort{{
                            ContainerPort: 80,
                        }},
                    }},
                },
            },
        },
    }
    
    // Set NginxService instance as the owner and controller
    ctrl.SetControllerReference(m, dep, r.Scheme)
    return dep
}

// labelsForNginx returns the labels for selecting the resources
func labelsForNginx(name string) map[string]string {
    return map[string]string{"app": "nginx", "webserver_cr": name}
}

func (r *NginxServiceReconciler) SetupWithManager(mgr ctrl.Manager) error {
    return ctrl.NewControllerManagedBy(mgr).
        For(&webserverv1.NginxService{}).
        Owns(&appsv1.Deployment{}).
        Complete(r)
}

In diesem Code:

  1. Die Reconcile-Methode ist das Herzstück des Controllers, die für jede Änderung am NginxService CR aufgerufen wird.
  2. Der Controller prüft, ob ein Deployment für den NginxService existiert, und erstellt eines, falls nicht.
  3. Er stellt sicher, dass die tatsächliche Anzahl der Replikate mit der gewünschten übereinstimmt und aktualisiert das Deployment bei Bedarf.
  4. Die CR-Status-Felder werden aktualisiert, um den aktuellen Zustand widerzuspiegeln.

55.7 Operator Deployment-Strategien

Es gibt verschiedene Wege, Operators in Kubernetes-Clustern bereitzustellen:

55.7.1 1. Direktes Deployment

Manuelles Anwenden der CRDs und Deployment des Operator-Controllers.

55.7.2 2. Operator Lifecycle Manager (OLM)

OLM ist ein Werkzeug zur Installation, Aktualisierung und Verwaltung von Operators in einem Kubernetes-Cluster. Es bietet:

Beispiel für ein OLM-Installationspaket:

apiVersion: operators.coreos.com/v1alpha1
kind: ClusterServiceVersion
metadata:
  name: postgresql-operator.v0.9.4
spec:
  displayName: PostgreSQL Operator
  version: 0.9.4
  description: Manages PostgreSQL databases on Kubernetes
  install:
    strategy: deployment
    spec:
      deployments:
      - name: postgresql-operator
        spec:
          replicas: 1
          ...

55.7.3 3. OperatorHub.io

OperatorHub.io ist ein zentraler Ort, an dem die Community Operators teilen kann. Viele Operators können direkt von hier installiert werden.

55.8 Best Practices für Operator-Entwicklung

55.8.1 1. Leichtgewichtigkeit

Operators sollten ressourcenschonend sein und nicht unnötig Cluster-Ressourcen verbrauchen.

55.8.2 2. Fokussierte Verantwortlichkeiten

Ein Operator sollte sich auf eine spezifische Anwendung oder einen spezifischen Dienst konzentrieren.

55.8.3 3. Fehlerbehandlung

Gute Operators sollten robust auf Fehler reagieren können und transparentes Logging bieten.

55.8.4 4. Status-Reporting

Der Operator sollte detaillierte Statusinformationen bereitstellen, damit Benutzer den Zustand der verwalteten Ressourcen leicht verstehen können.

55.8.5 5. Sicherheitsüberlegungen

55.8.6 6. Testbarkeit

Operators sollten gründlich getestet werden, einschließlich Unit-Tests und End-to-End-Tests.

55.9 Anwendungsfälle für Operators

Operators eignen sich besonders für:

55.9.1 1. Datenbanken

Datenbanken wie MongoDB, PostgreSQL, MySQL, Elasticsearch haben komplexe Betriebsabläufe wie Sharding, Replikation und Backup/Restore.

55.9.2 2. Monitoring-Systeme

Systeme wie Prometheus und Grafana erfordern spezielle Konfiguration und Integration.

55.9.3 3. Service-Meshes

Istio, Linkerd und andere Service-Meshes benötigen komplexe Konfiguration und Verwaltung.

55.9.4 4. Cloud-Native Anwendungen

Anwendungen, die für Cloud-Umgebungen entwickelt wurden und spezifische Skalierungs- oder Resilienzmuster implementieren.

55.9.5 5. Legacy-Anwendungen

Traditionelle Anwendungen, die nicht für Kubernetes entwickelt wurden, können durch Operators mit notwendiger Betriebslogik ausgestattet werden.

55.10 Herausforderungen und Einschränkungen

55.10.1 1. Komplexität

Die Entwicklung von Operators kann komplex sein und erfordert ein tiefes Verständnis sowohl der Kubernetes-API als auch der zu verwaltenden Anwendung.

55.10.2 2. Testbarkeit

Das Testen von Operators ist herausfordernd, da reale Cluster-Umgebungen und komplexe Zustandsänderungen simuliert werden müssen.

55.10.3 3. Wartbarkeit

Operators müssen mit Änderungen der Anwendung und der Kubernetes-API Schritt halten.

55.10.4 4. Upgrades

Die Aktualisierung von Operators selbst kann komplex sein, besonders wenn sie kritische zustandsbehaftete Anwendungen verwalten.