CRDs, Operators und Platform Controllers
Custom Resources als Platform-API: Design, Lifecycle, Versionierung.
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:
- Ein Ereignis kommt herein (Objekt erstellt, geändert, gelöscht — oder ein periodischer Resync).
- Der Controller liest den gewünschten Zustand aus
spec. - Er beobachtet den tatsächlichen Zustand (existiert der Namespace? stimmt die Quota?).
- Er führt die Differenz aus — erstellt, aktualisiert, löscht.
- 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 describeist 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.