Daten ohne Regeln sind Hoffnung: Wie CUE Konfiguration prüfbar macht
Die Konfigurationsdatei ist valides JSON. Der Parser schluckt sie ohne Kommentar. Und trotzdem startet die Anwendung nicht — weil der Port 99999 ist, das Feld env “DEV” heißt statt “dev”, der Timeout negativ ist und die Anzahl der Replicas auf null steht.
JSON schweigt. YAML auch. Und genau das ist das Problem: Fehler sind nicht sichtbar, sondern latent. Sie existieren — aber sie werden erst entdeckt, wenn es zu spät ist: beim Deploy, im Betrieb oder durch Nutzer.
Konfiguration ist heute oft ausführbarer Code — nur ohne Typprüfung. Kubernetes-Manifeste, CI/CD-Pipelines, Deployment-Definitionen: alles YAML oder JSON, alles ohne eingebaute Semantik für das, was erlaubt ist. Und genau hier entsteht ein systematisches Problem: Konfigurationsdateien wachsen, verzweigen sich über Umgebungen, werden kopiert und angepasst — bis niemand mehr weiß, was stimmt.
CUE ist eine Sprache, die das ändert. Nicht durch Magie, sondern durch ein präzises Prinzip: Daten und Regeln leben zusammen. CUE lohnt sich immer dann, wenn Konfiguration nicht nur gespeichert, sondern zuverlässig geprüft, zusammengeführt und für mehrere Zielsysteme erzeugt werden soll.
Das stille Versagen von JSON und YAML
In der Praxis sieht ein typisches Konfigurationsproblem so aus: Ein Projekt hat drei Environments — dev, staging, prod. Jede hat eine eigene config.json. Die Dateien wurden einmal sauber aufgesetzt und seitdem ad hoc erweitert.
Nach einem Jahr:
dev/config.jsonhat 14 Felderprod/config.jsonhat 11 Felderstaging/config.jsonhat 16 Felder, davon drei aus einer längst abgeschlossenen Experiment-Phase
Welche Felder sind noch aktuell? Welche Defaults gelten wo? Ist timeout in Sekunden oder Millisekunden? Muss logLevel in Großbuchstaben stehen? Niemand weiß es genau. Niemand hat es aufgeschrieben. Das Schema, sofern es je eines gab, lebt irgendwo im Anwendungscode — und wird dort geprüft, wenn es zu spät ist.
{
"port": 99999,
"env": "DEV",
"timeout": -1,
"replicas": 0,
"logLevel": "verbose"
}
Syntaktisch perfekt. Fachlich falsch auf fünf von fünf Feldern. Der Port liegt außerhalb des gültigen Bereichs, env hat nicht das erwartete Format, ein negativer Timeout macht keinen Sinn, null Replicas bedeuten dass nichts läuft, und verbose ist kein definiertes Log-Level.
JSON bemerkt keines dieser Probleme. YAML auch nicht. Beide sind Datenformate — sie beschreiben, wie Werte aussehen, nicht welche Werte erlaubt sind. YAML hat keine eingebaute Semantik für Regeln: Es beschreibt Daten, garantiert aber keine Invarianten. Die Validierung passiert deshalb woanders — im Anwendungscode, oft unvollständig, fast immer zu spät.
Was ist CUE?
CUE ist eine Open-Source-Sprache für Konfiguration, Validierung und Datentransformation. Die Abkürzung steht für “Configure, Unify, Execute”. Entwickelt von Marcel van Lohuizen, der zuvor an der Go-Toolchain bei Google gearbeitet hat — was sich in der Designphilosophie deutlich zeigt: präzise, typsicher, ohne Überraschungen.
Die einfachste Beschreibung: CUE ist eine Sprache, die Daten und Regeln in einer einzigen Notation vereint. Kein separates Schema-Dokument, das irgendwo separat gepflegt wird. Keine zusätzliche Validierungsbibliothek im Anwendungscode. Die Constraints leben direkt bei den Daten.
CUE ist kein Ersatz für Programmiersprachen. Es ist keine Datenbank. Es löst kein Businesslogik-Problem. Aber für das, wofür es gebaut wurde — Konfiguration sauber, prüfbar und über Umgebungen konsistent zu halten — tut es genau das, und zwar richtig.
Das Kernversprechen: Nicht nur beschreiben, wie Daten aussehen — sondern welche Daten erlaubt sind.
CUE ersetzt kein YAML — es kontrolliert es
Ein wichtiges Missverständnis vorweg: CUE wird in der Praxis selten direkt „anstelle” von YAML eingesetzt. Kubernetes, GitHub Actions, Helm — all diese Tools erwarten weiterhin ihr gewohntes Format.
CUE ist eine vorgeschaltete Schicht:
- CUE definiert Struktur, Regeln und Defaults
- daraus wird YAML oder JSON generiert
- das Zielsystem bekommt weiterhin sein natives Format
Der Unterschied: Dieses YAML ist garantiert valide. Nicht weil jemand es manuell geprüft hat, sondern weil CUE es gar nicht erst erzeugt, wenn Constraints verletzt werden.
Das ist der eigentliche Mehrwert: nicht YAML abschaffen, sondern verhindern, dass falsches YAML in ein System gelangt.
Das Kernkonzept
Daten und Constraints gleichzeitig
In JSON definiert man Werte. In CUE definiert man Werte und ihre Grenzen im selben Ausdruck. Der Typ ist keine separate Annotation, sondern Teil der Definition selbst.
port: int & >=1024 & <=65535
env: "dev" | "staging" | "prod"
Das erste Feld port ist ein Integer — aber nicht irgendein Integer. Nur Werte zwischen 1024 und 65535 sind gültig. Das zweite Feld env akzeptiert genau drei Strings. Alles andere schlägt fehl, sofort und explizit.
Zum Vergleich: Dieselbe Definition in JSON Schema würde so aussehen:
{
"type": "object",
"properties": {
"port": {
"type": "integer",
"minimum": 1024,
"maximum": 65535
},
"env": {
"type": "string",
"enum": ["dev", "staging", "prod"]
}
}
}
Dasselbe Ergebnis, aber getrennt von den Daten, schwerer zu lesen, schwerer zu pflegen. CUE hält beides zusammen und macht die Intention direkt lesbar.
Unification: Das Alleinstellungsmerkmal
Unification ist das Konzept, das CUE von anderen Ansätzen fundamental unterscheidet. Die Idee: Mehrere CUE-Definitionen können mit & zusammengeführt werden — und das Ergebnis ist nur gültig, wenn alle beteiligten Definitionen erfüllt sind.
Ein konkretes Beispiel für Umgebungskonfiguration:
// Gemeinsame Basis für alle Environments
#Base: {
port: int & >=1024 & <=65535
timeout: int & >0
replicas: int & >=1
logLevel: "debug" | "info" | "warn" | "error"
}
// Dev-spezifische Konfiguration
dev: #Base & {
port: 3000
timeout: 30
replicas: 1
logLevel: "debug"
}
// Prod-spezifische Konfiguration
prod: #Base & {
port: 8080
timeout: 10
replicas: 3
logLevel: "warn"
}
Beide Umgebungen teilen die Basis-Constraints. Wenn prod einen Port unter 1024 definiert, schlägt CUE sofort an — beim Zusammenführen, nicht beim Deploy. Kein Test, kein Laufzeitfehler nötig.
Unification ist kein blindes Überschreiben. Es ist das Zusammenführen von Aussagen zu einem konsistenten Ganzen — oder das explizite Scheitern, wenn das nicht möglich ist.
Wenn zwei Werte beim Zusammenführen in Konflikt geraten:
a: {
port: 8080
}
b: {
port: 9090
}
result: a & b
// Fehler: a.port (8080) und b.port (9090) können nicht gleichzeitig gelten
CUE meldet diesen Konflikt. Ein YAML-Override würde einfach überschreiben, ohne Kommentar.
Ein reales Beispiel mit Feature-Flags zeigt den Mehrwert noch direkter:
#FeatureFlags: {
newCheckout: bool
betaUI: bool
}
// Globale Defaults
defaults: #FeatureFlags & {
newCheckout: false
betaUI: false
}
// Staging überschreibt teilweise
staging: defaults & {
betaUI: true
}
Nur das, was sich ändert, wird überschrieben. Alles andere bleibt konsistent — ohne Copy-Paste, ohne Drift.
Partial Data: Unvollständige Konfiguration ist erlaubt
Ein Kernkonzept von CUE, das oft übersehen wird: CUE kann mit unvollständigen Daten arbeiten. Eine Datei muss nicht alle Werte enthalten — solange die Constraints erfüllt werden können.
Das ist entscheidend für Layered Config: Eine Basis-Datei definiert die Struktur und Defaults, eine Environment-Datei überschreibt spezifische Werte, eine Secrets-Datei ergänzt sensible Felder. CUE fasst alles zusammen und prüft das Ergebnis — nicht jede Schicht einzeln.
Das unterscheidet CUE massiv von JSON Schema: JSON Schema validiert eine vollständige Datei. CUE kann schrittweise zusammenführen und erst am Ende prüfen, ob das Gesamtergebnis valide ist.
Defaults und Ableitung
CUE unterstützt Standardwerte mit expliziter Syntax. Das * markiert einen Default:
port: *8080 | int & >=1024 & <=65535
host: *"localhost" | string
timeout: *30 | int & >0
debug: *false | bool
Der Wert 8080 ist der Default — aber er kann überschrieben werden, und zwar nur durch einen gültigen Integer im erlaubten Bereich. Ein String wäre nicht erlaubt, auch wenn der Default gesetzt ist. Constraints gelten immer, nicht nur wenn kein Default vorhanden ist.
Das ist praktisch für Konfigurationen, bei denen viele Felder einen sinnvollen Default haben sollen, aber für spezifische Deployments übersteuert werden können. Eine Konfiguration, die nur den Port setzt, bekommt alle anderen Felder mit sicheren Defaults — keine Überraschungen, keine vergessenen Pflichtfelder.
CUE im Vergleich
JSON
JSON ist ein Austauschformat. Es beschreibt Daten, keine Regeln. Für Transport, APIs, Speicherung — ideal. Für Konfiguration, die über mehrere Environments und Teams hinweg konsistent bleiben soll — unzureichend.
JSON hat keine Typen im Sinne von Constraints, keine Defaults, keine Ableitung, keine Zusammenführung. Was reinkommt, kommt raus. Ob es fachlich korrekt ist, weiß JSON nicht.
TypeScript
TypeScript prüft nur zur Compile-Zeit. Sobald Konfiguration als externe Datei kommt — JSON, YAML, .env — ist die Typprüfung weg. Was zur Laufzeit geladen wird, kennt TypeScript nicht. Deshalb braucht man zod, io-ts oder ähnliche Runtime-Validatoren, die das Schema ein zweites Mal beschreiben.
Ergebnis: Zwei Wahrheiten — Typen im Code, Daten außerhalb. Wer eines ändert, muss das andere manuell synchron halten.
JSON Schema
JSON Schema und CUE lösen oberflächlich dasselbe Problem — aber mit einem wesentlichen Unterschied: JSON Schema validiert. CUE validiert, vereinheitlicht und leitet ab — in einem einzigen Schritt, ohne separates Dokument.
Konkret bedeutet das:
- Schema und Daten sind bei JSON Schema immer getrennte Dokumente — wer eines ändert, muss das andere manuell synchron halten
- Die Syntax ist ausführlich und für Menschen schwer direkt zu lesen
- Unification — das Zusammenführen mehrerer Konfigurationen unter Beibehaltung aller Constraints — ist nicht vorgesehen
- Defaults müssen im Schema definiert und im Verarbeitungsschritt separat angewandt werden
JSON Schema ist eine gute Ergänzung zu JSON für Dokumentation und einfache Validierung. Für konsistente Multi-Environment-Konfiguration mit Ableitungen ist CUE die durchdachtere Lösung.
Kurzübersicht
| Tool | Daten | Regeln | Validierung | Fusion | Einsatzgebiet |
|---|---|---|---|---|---|
| JSON | ✓ | ✗ | ✗ | ✗ | Datenaustausch |
| YAML | ✓ | ✗ | ✗ | ✗ | Konfiguration |
| TypeScript | ✓ | teilweise | ✗ (Runtime) | ✗ | Anwendungscode |
| JSON Schema | ✓ | ✓ | ✓ | ✗ | Dokumentation |
| CUE | ✓ | ✓ | ✓ | ✓ | Konsistenz |
Wo CUE sinnvoll eingesetzt wird
Mehrere Environments konsistent halten
Das klassischste Anwendungsgebiet. Ein Projekt hat dev, staging, prod — manchmal auch test, local, preview. Alle teilen eine Grundstruktur, aber jede Umgebung hat andere Werte.
Mit CUE: Eine Basisdefinition, mehrere spezifische Overlays. Neue Felder werden einmal zur Basis hinzugefügt — alle Environments bekommen automatisch Defaults, sofern nicht explizit übersteuert. Fehlende Pflichtfelder werden sofort erkannt.
Ohne CUE: Vier YAML-Dateien, die sich über Monate auseinander entwickeln, bis niemand mehr den Überblick hat.
Kubernetes und Infrastructure as Code
Kubernetes-Konfigurationen sind das Paradebeispiel für YAML-Wachstum außer Kontrolle. Deployments, Services, ConfigMaps, Ingress, RoleBindings — hunderte Zeilen, zwischen denen sich Fehler verstecken.
CUE hat hierfür eigene Unterstützung: cue get k8s importiert Kubernetes-Schemas als CUE-Definitionen. Eigene Constraints lassen sich ergänzen:
#Deployment: {
spec: {
replicas: int & >=1
template: spec: containers: [...{
resources: limits: {
memory: string
cpu: string
}
}]
}
}
Ein Deployment ohne Resource Limits? CUE meldet den Fehler — bevor kubectl apply läuft.
Typische Fehler, die Kubernetes still akzeptiert und erst später indirekt scheitern lässt:
- vergessenes
resources.limits→ Pod startet, frisst unbegrenzt Memory, wird gekillt - falsche Struktur bei
env→ Umgebungsvariablen fehlen, Fehler erscheint im Log - inkonsistente Labels zwischen Deployment und Service → kein Traffic kommt an
CUE verhindert alle drei — nicht durch Kubernetes-Admission-Controller, sondern schon beim Zusammenstellen der Manifeste.
CUE vs Helm
Helm ist das Standard-Tool für Kubernetes-Packages. Es arbeitet mit Go-Templates: YAML-Dateien mit Template-Logik, aus denen fertige Manifeste gerendert werden.
CUE und Helm verfolgen unterschiedliche Ansätze:
- Helm: „Wie baue ich YAML?”
- CUE: „Welche YAML ist überhaupt erlaubt?”
Templates können alles erzeugen — auch Unsinn. CUE verhindert, dass Unsinn überhaupt entsteht. Helm prüft nicht, ob das generierte YAML fachlich korrekt ist. CUE kann das, weil Constraints Teil der Definition sind.
Timoni, ein moderner Helm-Nachfolger für Kubernetes-Packages, basiert vollständig auf CUE und zeigt, wohin die Entwicklung gehen kann.
CI/CD-Pipeline-Konfigurationen
GitHub Actions, GitLab CI, Tekton — alle nutzen YAML-Konfigurationen, die mit wachsender Projektgröße schnell komplex werden. CUE kann als Generierungsschicht dienen: CUE-Definitionen werden zu gültigem YAML exportiert.
cue export pipeline.cue --out yaml
Änderungen werden einmal in CUE gemacht, nicht in fünf Pipeline-Dateien. Die Constraints stellen sicher, dass das generierte YAML gültig ist — bevor es in die Pipeline geht.
API-Eingaben vorab validieren
Bevor Daten gespeichert oder verarbeitet werden, können sie gegen ein CUE-Schema geprüft werden. Nützlich für Webhook-Payloads, Formulardaten oder konfigurierbare Integrationen:
#UserInput: {
name: string
email: =~"[^@]+@[^@]+\\.[^@]+"
age: int & >=0 & <=150
role: "admin" | "editor" | "viewer"
}
CUE-Bibliotheken existieren für Go, Python und andere Sprachen — die Integration direkt im Anwendungscode ist damit möglich.
Konfiguration generieren
Eine CUE-Datei als zentrale Quelle der Wahrheit, aus der mehrere Formate generiert werden. Statt JSON, YAML und TOML manuell in Sync zu halten, gibt es eine einzige Definition — einmal definieren, überall valide ableiten:
cue export config.cue # JSON (Standard)
cue export config.cue --out yaml # YAML
cue export config.cue --out toml # TOML
Nützlich für Projekte, bei denen verschiedene Tools verschiedene Formate erwarten, aber alle dieselbe Konfiguration nutzen sollen. Kein manueller Abgleich, kein Drift zwischen den Formaten.
Feature-Flags und API-Contracts
Auch Feature-Flags lassen sich mit CUE strukturieren: erlaubte Flag-Namen, gültige Rollout-Stufen, Pflichtfelder je Umgebung. Das verhindert, dass ein Tippfehler im Flag-Namen still ignoriert wird, weil kein System den Wertebereich kennt.
Für API-Contracts funktioniert dasselbe Prinzip: CUE beschreibt, welche Inputs eine API akzeptiert — mit Typen, Enums und Ranges. Payloads lassen sich damit validieren, bevor sie verarbeitet werden, und Contracts lassen sich als CUE-Definitionen versionieren statt als informelle Dokumentation.
Wo CUE keinen Sinn macht
CUE ist stark bei Struktur, Constraints und Ableitungen. Es ist schwach bei Businesslogik, dynamischen Berechnungen und hochspezifischen Sonderfällen. Wer CUE überall einsetzen will, kauft Komplexität ohne Mehrwert.
Kleine, einfache Projekte mit einer einzigen Konfigurationsdatei und wenigen Feldern brauchen kein CUE. Eine config.json mit fünf statischen Feldern funktioniert ohne Validierungsschicht vollkommen.
Reine Datenspeicherung — Datenbanken, Caches, Event-Logs — ist nicht das Einsatzgebiet. Hier sind spezialisierte Formate und Systeme die richtige Wahl.
Komplexe Businesslogik gehört in Code. CUE kann Constraints und Typregeln abbilden, aber keine if-else-Logik mit Seiteneffekten, keine Datenbankabfragen, keine externen Abhängigkeiten, keine dynamischen Berechnungen zur Laufzeit. Wer das versucht, kämpft gegen die Sprache.
Hochstandardisiertes Tooling mit festen Erwartungen an native Konfigurationsformate — bestimmte CI-Provider, die direkt YAML parsen — braucht manchmal einfach das native Format. CUE als Generierungsschicht ist dann zwar möglich, aber ob der Aufwand lohnt, ist eine Einzelfallentscheidung.
Hochspezifische Sonderfälle mit nicht-uniformen Strukturen, bei denen jede Konfigurationsdatei grundlegend anders aussieht, profitieren weniger von Unification. CUE entfaltet seinen Mehrwert dort, wo Wiederholung und Konsistenz gefordert sind — nicht bei strukturell einmaligen Daten.
Ein vollständiges Beispiel
Die gleiche Web-Anwendung wie am Anfang — diesmal mit CUE.
Zuerst das Problem ohne Validierung:
{
"port": 99999,
"env": "DEV",
"timeout": -1,
"replicas": 0,
"logLevel": "verbose"
}
CUE-Schema für diese Konfiguration:
#AppConfig: {
port: int & >=1024 & <=65535
env: "dev" | "staging" | "prod"
timeout: int & >0
replicas: int & >=1
logLevel: "debug" | "info" | "warn" | "error"
}
config: #AppConfig
Validierung gegen die fehlerhafte JSON-Datei:
cue vet config.json schema.cue
Ausgabe:
port: invalid value 99999 (out of bound <=65535):
./config.json:2:12
env: "DEV" not one of ["dev", "staging", "prod"]:
./config.json:3:10
timeout: invalid value -1 (out of bound >0):
./config.json:4:13
replicas: invalid value 0 (out of bound >=1):
./config.json:5:14
logLevel: "verbose" not one of ["debug", "info", "warn", "error"]:
./config.json:6:14
Fünf Fehler — erkannt vor dem Deploy, mit genauen Zeilenangaben, ohne dass die Anwendung einmal gestartet werden musste.
Die korrigierte Konfiguration:
{
"port": 8080,
"env": "dev",
"timeout": 30,
"replicas": 1,
"logLevel": "info"
}
cue vet gibt nichts aus. Kein Output bedeutet: valide.
Typischer Workflow
Validierung passiert vor jedem Merge und Deploy — nicht danach. cue vet in GitHub Actions oder GitLab CI einhängen: ein fehlgeschlagener Schritt blockiert den Branch. Ungültige Konfiguration kommt nicht in Produktion.
CUE installieren und starten
Die Installation ist unkompliziert:
# macOS
brew install cue
# Linux / direkt
curl -fsSL https://cuelang.org/dl/v0.7.0/cue_v0.7.0_linux_amd64.tar.gz | tar xz
sudo mv cue /usr/local/bin/
# Version prüfen
cue version
Ein erstes CUE-Projekt:
# Neues Modul anlegen
cue mod init example.com/myconfig
# Schema schreiben
cat > schema.cue << 'EOF'
package config
port: *8080 | int & >=1024 & <=65535
env: *"dev" | "staging" | "prod"
timeout: *30 | int & >0
replicas: *1 | int & >=1
EOF
# Gegen eine JSON-Datei prüfen
cue vet -d '#AppConfig' config.json schema.cue
Die offizielle Dokumentation auf cuelang.org ist gut strukturiert und enthält interaktive Beispiele für alle Kernkonzepte. Wer einen strukturierten Einstieg mit Beispielen aus der Praxis bevorzugt: Der Kurs CUE lernen führt durch die Kernkonzepte bis hin zu echten Konfigurations-Setups.
Einordnung
CUE ist kein Ersatz für TypeScript. Kein Ersatz für JSON. Kein Ersatz für Datenbanken. Es ist ein Werkzeug für einen sehr spezifischen Bereich: Konfiguration, die konsistent, prüfbar und über Environments hinweg nachvollziehbar sein soll.
Das Problem, das CUE löst, ist real und wächst mit Projektgröße. Wer ein Projekt betreibt, das über mehrere Umgebungen deployed wird, wer Kubernetes-Konfigurationen pflegt, wer CI/CD-Pipelines für mehrere Teams verwaltet — der kennt das stille Versagen von YAML und JSON aus eigener Erfahrung.
CUE gibt diesem Problem eine Antwort, die sich nicht auf externe Tools, separaten Validierungscode oder manuelle Disziplin verlässt. Die Regeln sind Teil der Konfiguration selbst. Wer etwas Ungültiges schreibt, weiß es sofort — nicht beim Deploy.
Der Einstieg lohnt sich schon für ein einzelnes Projekt, bei dem Konfigurationsfehler zu Laufzeitproblemen geführt haben. Ein Schema, das cue vet besteht, ist eine Garantie, die JSON nie geben kann.
CUE verhindert keine Fehler im Code — aber es verhindert, dass falsche Zustände überhaupt entstehen. Das ist der Unterschied zwischen einem System, das scheitert, und einem, das gar nicht erst in einen ungültigen Zustand gerät.
In einer Welt, in der Infrastruktur immer stärker durch Konfigurationsdateien definiert wird, ist das kein Komfort-Feature. Es ist eine Grundlage.
CUE ist kein Datenformat, sondern ein constraints-first Werkzeug für Konfigurationsmodelle — und der Unterschied wird sichtbar, sobald die erste Produktionsumgebung aus einer Konfigurationsdatei startet, die kein Mensch mehr vollständig im Kopf hat.