🚀 Platform Engineer Lektion 29/50 ~10 Min. Experte

CRDs, Operators und Platform Controllers

Custom Resources als Platform-API: Design, Lifecycle, Versionierung.

📝 Meine Notizen

Lernziele

Nach dieser Lektion kannst du erklären, wie CRDs die Kubernetes-API erweitern und wie der Reconcile-Loop eines Controllers funktioniert. Du kannst beurteilen, wann ein eigener Platform Controller gerechtfertigt ist und wann er Overengineering wäre, und du kennst die Design-Fragen, die über Erfolg einer CRD-basierten Plattform-API entscheiden: Schema-Design, Status-Reporting und Versionierung.

Das Problem: Deine Plattform braucht eine API, keine Skriptsammlung

In Lektion 27 hast du gesehen, wie Crossplane-Compositions Entwicklern eine einfache Schnittstelle für Infrastruktur geben. Aber was, wenn dein Self-Service-Konzept nicht in vorhandene Tools passt? Beispiel: Ein Team soll mit einem einzigen Objekt TenantNamespace einen Namespace inklusive Quotas, NetworkPolicies, RBAC-Bindings und Monitoring-Konfiguration bekommen — nach den Regeln deiner Organisation.

Der Reflex vieler Teams: ein Skript oder eine Pipeline, die das alles anlegt. Das funktioniert genau einmal — beim Anlegen. Was passiert, wenn jemand die Quota manuell ändert? Wenn sich der Organisationsstandard ändert und 200 bestehende Namespaces nachziehen müssen? Skripte haben kein Gedächtnis und keinen Soll-Zustand. Kubernetes hat für dieses Problem ein eingebautes Architekturmuster, das du dir zunutze machen kannst: deklarierte Ressourcen plus Controller, die kontinuierlich für deren Umsetzung sorgen.

CRDs: eigene Typen in der Kubernetes-API

Eine CustomResourceDefinition registriert einen neuen Ressourcentyp im API-Server. Danach verhält sich TenantNamespace wie jeder eingebaute Typ: kubectl get tenantnamespaces funktioniert, RBAC greift pro Typ und Verb, der API-Server validiert Objekte gegen das OpenAPI-Schema der CRD, alles landet in etcd und ist über Watches beobachtbar.

Das ist der entscheidende Punkt für Plattform-Design: Du bekommst Authentifizierung, Autorisierung, Validierung, Audit-Logging und Eventing geschenkt, weil du die API-Maschinerie von Kubernetes wiederverwendest, statt einen eigenen REST-Service mit eigener Datenbank zu bauen. Deine Plattform-API ist damit auch automatisch GitOps-fähig (Lektion 25) und Policy-fähig (Lektion 28) — es sind ja nur Kubernetes-Objekte.

Eine CRD allein tut allerdings nichts. Sie definiert nur das Vokabular. Ohne Controller ist ein TenantNamespace-Objekt ein Datensatz in etcd, mehr nicht.

Der Reconcile-Loop: Soll-Zustand statt Workflow

Ein Controller ist ein Prozess, der Objekte eines Typs beobachtet und die Realität an deren Spezifikation angleicht. Der Kern ist der Reconcile-Loop:

  1. Ein Ereignis kommt herein (Objekt erstellt, geändert, gelöscht — oder ein periodischer Resync).
  2. Der Controller liest den gewünschten Zustand aus spec.
  3. Er beobachtet den tatsächlichen Zustand (existiert der Namespace? stimmt die Quota?).
  4. Er führt die Differenz aus — erstellt, aktualisiert, löscht.
  5. Er schreibt das Ergebnis nach status.

Zwei Eigenschaften machen das Muster robust. Erstens Idempotenz: Reconcile muss beliebig oft laufen können, ohne Schaden anzurichten — der Controller fragt nicht „was ist passiert?”, sondern „wie soll es aussehen?”. Deshalb heilt sich das System selbst: Eine manuell verstellte Quota wird beim nächsten Durchlauf zurückgesetzt. Zweitens Level-based statt Edge-based: Der Controller verlässt sich nicht darauf, jedes einzelne Event zu sehen; verpasste Events sind unkritisch, weil der nächste Abgleich vom aktuellen Zustand ausgeht.

Für Aufräumarbeiten gibt es Finalizer: ein Marker am Objekt, der das endgültige Löschen blockiert, bis der Controller externe Abhängigkeiten (etwa eine Cloud-Ressource) entfernt und den Finalizer selbst abgeräumt hat. Gebaut werden Controller selten von Hand gegen die API — Frameworks wie Kubebuilder (Go, auf controller-runtime) oder Sprach-Alternativen wie Kopf (Python) übernehmen Watch-Mechanik, Caching und Retry-Logik. Der Begriff Operator bezeichnet im Kern dasselbe Muster, traditionell mit Fokus auf den Betrieb einer konkreten Anwendung (etwa ein Postgres-Operator, der Failover und Backups automatisiert); ein Platform Controller wendet das Muster auf die Selbstbedienungs-APIs deiner Plattform an.

API-Design: Schema, Status, Versionierung

Eine CRD ist ein API-Versprechen an deine Nutzer — und damit gelten dieselben Regeln wie für jede öffentliche API (Lektion 18 hat die Grundlagen gelegt):

Schema: Absicht abfragen, nicht Implementierung. Ein gutes TenantNamespace-Spec fragt nach team, environment, resourceTier: small|medium|large — nicht nach 40 Detailfeldern, die Kubernetes-Wissen voraussetzen. Die Übersetzung von Absicht in Details ist genau der Mehrwert des Controllers. Validierung gehört maximal ins Schema (Enums, Pattern, Pflichtfelder): Jede Eingabe, die der API-Server ablehnt, muss dein Controller nie behandeln.

Status: die halbe Developer Experience. Asynchrone Systeme ohne ehrliches Status-Reporting fühlen sich kaputt an. Die Konvention sind Conditions — typisierte Einträge wie Ready: False, Reason: QuotaConflict, Message: ... — plus aussagekräftige Events. Faustregel: Ein Entwickler muss mit kubectl describe allein erkennen können, ob seine Anfrage läuft, fertig ist oder warum sie hängt. Ein Controller, der bei Fehlern stumm bleibt, erzeugt genau die Support-Tickets, die Self-Service vermeiden sollte.

Versionierung: Breaking Changes sind teuer. CRDs tragen Versionen (v1alpha1, v1beta1, v1). Mehrere Versionen können parallel angeboten werden; eine Conversion übersetzt zwischen ihnen, gespeichert wird in der markierten Storage-Version. Praktisch heißt das: Felder hinzufügen ist billig, Felder umbenennen oder entfernen ist ein Migrationsprojekt über alle Nutzer. Starte mit v1alpha1 und kommuniziere klar, dass sich die API noch ändern darf — der Sprung zu v1 ist das Stabilitätsversprechen.

Und die ehrliche Gegenfrage vor jedem Eigenbau: Ein eigener Controller ist ein Softwareprodukt mit Lifecycle — Tests, Upgrades, On-Call. Wenn Crossplane-Compositions, ein vorhandener Operator oder schlicht ein Helm-Chart das Problem zu 80 % lösen, ist der Eigenbau meist die schlechtere Wahl. Der Controller lohnt sich dort, wo Organisationslogik kontinuierlich durchgesetzt werden muss und kein Standard-Tool sie abbildet.

Praxis: CRD anlegen und das fehlende Stück erleben

Ohne eine Zeile Go kannst du das Fundament greifbar machen:

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: tenantnamespaces.platform.example.com
spec:
  group: platform.example.com
  scope: Cluster
  names:
    plural: tenantnamespaces
    singular: tenantnamespace
    kind: TenantNamespace
  versions:
    - name: v1alpha1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              required: ["team", "resourceTier"]
              properties:
                team:
                  type: string
                resourceTier:
                  type: string
                  enum: ["small", "medium", "large"]
kubectl apply -f crd.yaml
kubectl explain tenantnamespace.spec        # Schema ist API-Doku
cat <<'EOF' | kubectl apply -f -
apiVersion: platform.example.com/v1alpha1
kind: TenantNamespace
metadata:
  name: team-checkout-prod
spec:
  team: checkout
  resourceTier: medium
EOF
kubectl get tenantnamespaces                 # das Objekt existiert ...
kubectl get namespaces | grep checkout       # ... aber nichts ist passiert

Teste auch die Schema-Validierung: resourceTier: huge wird vom API-Server abgelehnt, bevor irgendein Controller involviert wäre. Das fehlende Reconcile selbst zu bauen ist der natürliche nächste Schritt — das Kubebuilder-Tutorial führt genau diesen Weg einmal komplett durch.

Typische Stolperfallen

Nicht-idempotentes Reconcile: Wer im Loop blind „create” aufruft, produziert beim zweiten Durchlauf Fehler oder Duplikate. Immer erst beobachten, dann nur die Differenz ausführen.

Spec als Konfigurations-Müllhalde: Jedes Feld, das du aufnimmst, musst du für immer unterstützen. Eine CRD mit 50 optionalen Feldern ist keine Abstraktion mehr, sondern Kubernetes mit Umweg.

Status vernachlässigt: Ein Controller, der Fehler nur in seine eigenen Logs schreibt, ist für Nutzer eine Black Box — kubectl describe muss die Wahrheit zeigen.

Vergessene Finalizer: Ein Finalizer, den kein laufender Controller mehr entfernt (z. B. nach Deinstallation), lässt Objekte ewig im Terminating-Zustand hängen — ein Klassiker, der ganze Namespace-Löschungen blockiert.

Interview-Vorbereitung

Auf „Was ist ein Operator/Controller und wann baust du selbst einen?” antwortest du in vier Schritten: Muster (CRD = API-Erweiterung, Controller = Reconcile-Loop von Soll nach Ist) → Eigenschaften (idempotent, level-based, selbstheilend) → Plattform-Nutzen (eigene Self-Service-API mit geschenkter Auth-, Audit- und GitOps-Integration) → Build-Kriterium (nur bei kontinuierlich durchzusetzender Organisationslogik, die kein Standard-Tool abbildet — sonst Composition, fertiger Operator oder Helm-Chart).

Typische Follow-ups:

  • „Warum muss Reconcile idempotent sein?” — Es läuft beliebig oft (Resyncs, Retries, Neustarts); nur der Abgleich von Soll und Ist ist gegen Wiederholung immun.
  • „Wie meldet ein Controller Fehler an Nutzer?” — Status-Conditions und Events am Objekt, nicht nur Logs; kubectl describe ist das Nutzer-Interface.
  • „Wie änderst du eine CRD, ohne Nutzer zu brechen?” — Additive Änderungen bevorzugen; sonst neue Version parallel anbieten, Conversion bereitstellen, Migration kommunizieren.
  • „CRD/Controller oder eigener REST-Service?” — CRD erbt Kubernetes-Auth, -Audit, -CLI und GitOps-Fähigkeit; ein eigener Service lohnt nur, wenn die Nutzer gar nicht Kubernetes-zentriert arbeiten.

Zusammenfassung

CRDs erweitern die Kubernetes-API um eigene Typen mit Schema-Validierung; Controller machen daraus lebendige Systeme, indem ihr idempotenter Reconcile-Loop kontinuierlich Soll und Ist angleicht. Für Plattform-Teams ist das die Maschinerie hinter eigenen Self-Service-APIs — mit geschenkter Authentifizierung, Audit und GitOps-Integration, aber den vollen Kosten eines eigenen Softwareprodukts. Gutes Design heißt: Absicht statt Details im Spec, ehrliche Conditions im Status, disziplinierte Versionierung. Ob fertiger Operator, Composition oder Eigenbau — ausgeliefert und verteilt werden all diese Bausteine als Pakete. Wie diese Package-Schicht aus Helm, Kustomize und OCI-Artefakten aussieht, klärt die nächste Lektion.