TCPRoute wird Standard, dein Controller bleibt experimentell
09.08.2026
Thema: alle Beiträge zu Kubernetes
Gateway API 1.6 hebt TCPRoute und UDPRoute in den Standard-Kanal. Was das API wirklich kann, warum der Kanalwechsel die eigentliche Arbeit ist und wer es heute schon nachweisen kann.
Inhaltsverzeichnis
- Warum Layer 4 der Ort war, an dem Portabilität aufhörte
- Was TCPRoute wirklich ist
- Der Kanalwechsel ist die eigentliche Arbeit
- Der Nachweis, den es noch nicht gibt
- Was daraus folgt
- Fazit
Ich erzeuge einen leeren k3s-Cluster und schaue mir an, was darin liegt, bevor ich irgendetwas installiert habe:
$ kubectl get crd | grep -E 'ingressroute(tcp|udp)'
ingressroutetcps.traefik.io 2026-08-06T15:14:25Z
ingressrouteudps.traefik.io 2026-08-06T15:14:25Z
Zwei herstellereigene Ressourcen für Layer-4-Routing, mitgeliefert, ungefragt. Beide in der Version v1alpha1, und das seit Jahren.
Das ist der Zustand, den Gateway API 1.6 beendet. Die Antwort vorweg: TCPRoute und UDPRoute sind ab dieser Version im Standard-Kanal und tragen die Version v1. Portables Routing für rohes TCP und UDP existiert damit als verbindliche Spezifikation. Nutzbar ist es heute trotzdem fast nur über den Experimental-Kanal, und das lässt sich nachrechnen, statt es zu glauben. Der Rest dieses Beitrags klärt beides: was das API wirklich kann und wer es beweisen kann.
Warum Layer 4 der Ort war, an dem Portabilität aufhörte
Ingress war immer HTTP. Host, Pfad, TLS-Terminierung, mehr stand nie in der Spezifikation. Für alles andere, also Postgres, MySQL, DNS, MQTT, SIP, Spieleserver, IoT-Telemetrie, gab es keine Kubernetes-Antwort, sondern vier herstellereigene.
Ein Service vom Typ LoadBalancer je Dienst. Funktioniert überall, kostet aber in der Cloud einen eigenen Lastverteiler pro Dienst, und alles Interessante daran steckt in Annotationen, deren Namen der Provider bestimmt.
Die ConfigMap bei ingress-nginx. Der Controller kannte den Schalter --tcp-services-configmap, dahinter lag eine ConfigMap, in der der Schlüssel der externe Port war und der Wert das Ziel:
apiVersion: v1
kind: ConfigMap
metadata:
name: tcp-services
namespace: ingress-nginx
data:
"9000": "default/example-go:8080"
Das ist Routing als Zeichenkette. Kein eigenes API-Objekt, also keine Rechtevergabe pro Namespace, kein Statusfeld, keine Schemaprüfung. Wer die ConfigMap schreiben darf, schreibt für alle. Und der Weg dorthin führte zusätzlich über den Controller-Start, weil der Port auch im Service des Controllers stehen musste.
Die CRD bei Traefik, also die beiden Ressourcen von oben. Gateway plus VirtualService mit einem tcp-Block bei Istio.
Vier Wege, vier Datenmodelle. Ein Controller-Wechsel bedeutete: alles neu schreiben. Das ist Lock-in, ohne dass eine Lizenz sich ändern muss, allein über die Form der Konfiguration.
Wie teuer das wird, hat das Ökosystem gerade erlebt. ingress-nginx ist im März 2026 in den Ruhestand gegangen: keine Releases, keine Fehlerbehebungen, keine Patches für Sicherheitslücken mehr. Der mit Abstand meistgenutzte Ingress-Controller, und mit ihm der verbreitetste Weg, TCP durch einen Cluster-Rand zu bekommen. Wer seine L4-Konfiguration in dieser ConfigMap hatte, migriert sie nicht, er schreibt sie neu.
Was TCPRoute wirklich ist
Der Aufbau ist zweiteilig und folgt dem rollenorientierten Modell des restlichen API. Das Plattform-Team besitzt das Gateway und damit die Ports:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: edge
spec:
gatewayClassName: example
listeners:
- name: postgres
protocol: TCP
port: 5432
allowedRoutes:
kinds:
- kind: TCPRoute
Das Anwendungs-Team besitzt die Route und hängt sie an einen benannten Listener:
apiVersion: gateway.networking.k8s.io/v1
kind: TCPRoute
metadata:
name: postgres
spec:
parentRefs:
- name: edge
sectionName: postgres
rules:
- backendRefs:
- name: postgres
port: 5432
Das ist die ganze Ressource. Und das ist wörtlich gemeint, denn ein Blick ins Schema statt in die Ankündigung zeigt, wie klein dieses API absichtlich ist:
$ kubectl explain tcproute.spec | grep -E '^ [a-z]'
parentRefs <[]Object>
rules <[]Object> -required-
Zwei Felder. Interessanter sind die Grenzen im CRD, die man in der Doku nicht sieht:
| Feld | Grenze |
|---|---|
spec.rules |
minItems: 1, maxItems: 1 |
rules[].backendRefs |
minItems: 1, maxItems: 16 |
spec.parentRefs |
maxItems: 32 |
Genau eine Regel. Nicht als Vorgabe für den Anfang, sondern als Schemagrenze. Der Apiserver setzt sie durch:
$ kubectl apply -f zwei-regeln.yaml
The TCPRoute "zwei-regeln" is invalid:
* spec.rules: Too many: 2: must have at most 1 item
Das ist kein Mangel, sondern der ehrlichste Teil der Spezifikation. Wer von HTTPRoute kommt, sucht reflexhaft nach matches und findet nichts. Auf Layer 4 gibt es auch nichts zu finden: kein Host-Header, keinen Pfad, keine Methode, kein Query-Argument. Zum Zeitpunkt der Routing-Entscheidung liegt nur die TCP-Verbindung vor. Die einzige Information, die die Entscheidung tragen kann, ist der Port, auf dem sie angekommen ist. Genau eine Regel ist die korrekte Abbildung dieser Tatsache. Amüsanterweise steht in der Feldbeschreibung des CRD immer noch „Rules are a list of TCP matchers and actions”, ein Überbleibsel aus der HTTPRoute-Vorlage, dem kein einziges Feld im Schema entspricht.
Was es trotzdem gibt, zeigt sich, wenn man die angelegte Ressource zurückliest und die eingesetzten Standardwerte betrachtet:
$ kubectl get tcproute postgres -o yaml
spec:
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: edge
sectionName: postgres
rules:
- backendRefs:
- group: ""
kind: Service
name: postgres
port: 5432
weight: 1
weight: 1. Bis zu 16 Backends je Regel, jedes mit einem Gewicht. Gewichtete Verteilung auf Verbindungsebene ist also Teil des Standards, ein Canary-Rollout für eine Datenbank-Replik oder einen Spieleserver braucht keine Fremdmechanik. Der Rest der Beschreibung ist ebenfalls strenger, als man erwartet: Zeigt ein backendRef ins Leere, muss die Implementierung Verbindungen aktiv ablehnen, und zwar gewichtsgerecht. Ein Backend mit 80 Prozent Gewicht, das nicht existiert, lehnt 80 Prozent der Verbindungen ab. Stilles Verschlucken ist nicht vorgesehen.
Die praktische Folge der einen Regel ist ein Listener-Port je Backend. Das Gateway erlaubt 64 Listener, und seit dem ListenerSet lassen sich weitere 64 je Set anhängen, auch aus anderen Namespaces, sofern das Gateway das ausdrücklich zulässt. Wer stattdessen mehrere Backends über einen Port erreichen will, braucht ein Unterscheidungsmerkmal im Datenstrom, und das einzige, das TLS von sich aus mitbringt, ist SNI. Dafür gibt es TLSRoute, die bereits mit 1.5 in den Standard-Kanal gewandert ist.
Der Kanalwechsel ist die eigentliche Arbeit
Gateway API liefert zwei Installationsbündel: den Standard-Kanal mit dem, was als stabil gilt, und den Experimental-Kanal mit denselben Ressourcen plus zusätzlichen Feldern. Der Unterschied zwischen beiden ist bei diesem Release größer als üblich.
Nach der Installation des Standard-Kanals sieht der Cluster so aus:
$ kubectl apply -f .../v1.6.1/standard-install.yaml
$ kubectl api-resources --api-group=gateway.networking.k8s.io
NAME APIVERSION NAMESPACED KIND
grpcroutes gateway.networking.k8s.io/v1 true GRPCRoute
httproutes gateway.networking.k8s.io/v1 true HTTPRoute
tcproutes gateway.networking.k8s.io/v1 true TCPRoute
tlsroutes gateway.networking.k8s.io/v1 true TLSRoute
udproutes gateway.networking.k8s.io/v1 true UDPRoute
Und dann die Zeile, auf die es ankommt:
$ kubectl get --raw /apis/gateway.networking.k8s.io | jq '[.versions[].version]'
["v1", "v1beta1"]
Kein v1alpha2. Nicht als veraltet markiert, sondern gar nicht ausgeliefert. Die alte Version steht zwar noch im CRD, aber mit served: false. Damit schlägt jedes bestehende Manifest hart fehl:
$ kubectl apply -f alte-route.yaml
error: resource mapping not found for name: "postgres" namespace: "":
no matches for kind "TCPRoute" in version "gateway.networking.k8s.io/v1alpha2"
Das ist der laute Teil, und laut ist gut. Der leise Teil ist der wichtigere: Es trifft nicht nur deine YAML-Dateien, sondern jeden Controller, der eine TCPRoute noch unter v1alpha2 beobachtet. Dessen Informer bekommt keine Objekte mehr, nicht weil keine da wären, sondern weil die Version, nach der er fragt, nicht mehr existiert. Der Controller läuft weiter und sieht null Routen.
Der Experimental-Kanal liefert v1alpha2 weiterhin aus, dort ist es nur als veraltet gekennzeichnet. Naheliegender Gedanke also: beides installieren. Das geht nicht, und zwar mit Ansage:
$ kubectl apply -f .../v1.6.1/experimental-install.yaml
Error from server (Invalid): Installing experimental CRDs on top of standard
channel CRDs is prohibited by default. Uninstall ValidatingAdmissionPolicy
safe-upgrades.gateway.networking.k8s.io to install experimental CRDs on top
of standard channel CRDs.
Das Bündel bringt seit Neuestem eine ValidatingAdmissionPolicy mit, die auf CRDs der eigenen Gruppe achtet und das Mischen der Kanäle sowie den Rückbau auf alte Bündelversionen unterbindet. Der Ausweg steht in der Meldung selbst, was ich für die richtige Umgangsform halte: eine Leitplanke, kein Zaun.
Ein Detail am Rande, das die Grenze dieser Leitplanke zeigt: Drei Ressourcen des Experimental-Bündels sind trotz des Abbruchs entstanden.
$ kubectl api-resources --api-group=gateway.networking.x-k8s.io
NAME APIVERSION KIND
xbackends gateway.networking.x-k8s.io/v1alpha1 XBackend
xbackendtrafficpolicies gateway.networking.x-k8s.io/v1alpha1 XBackendTrafficPolicy
xmeshes gateway.networking.x-k8s.io/v1alpha1 XMesh
Sie liegen in einer anderen API-Gruppe, und die Policy prüft nur gateway.networking.k8s.io. Damit sind wir bei der zweiten Neuerung dieses Release, die in der Ankündigung untergeht, aber langfristig die klügere ist: Wirklich experimentelle Ressourcen wohnen jetzt in gateway.networking.x-k8s.io und tragen ein X im Namen. Bisher war die Reife einer Ressource nur daran zu erkennen, aus welcher Installationsdatei ihr CRD stammte, was man einem Cluster hinterher nicht mehr ansieht. Jetzt steht sie in jedem einzelnen Manifest, in der apiVersion und im kind. Das ist die Art von Änderung, die niemandem auffällt und in zwei Jahren viel Verwirrung verhindert.
Der Nachweis, den es noch nicht gibt
Der Apiserver nimmt die Route bereitwillig an. Passiert ist damit nichts:
$ kubectl get tcproute postgres -o jsonpath='{.status}'
$
Leer. Kein status-Block, weil ihn kein Controller geschrieben hat. Ein CRD ist ein Formular, kein Verhalten. Die Frage ist also nicht, ob TCPRoute in deinem Cluster existiert, sondern ob jemand es liest.
Gateway API beantwortet das an zwei Stellen, und beide sind öffentlich. Die erste steht im Cluster selbst, denn jede GatewayClass meldet, was sie kann:
kubectl get gatewayclass -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.supportedFeatures[*].name}{"\n"}{end}'
Die Namen, nach denen du suchst, sind schlicht TCPRoute und UDPRoute, beide seit 1.6 im Standard-Kanal der Konformitätsprüfung. Steht der Name nicht drin, kann der Controller es nicht, unabhängig davon, was in der CRD-Liste liegt.
Die zweite Stelle ist interessanter, weil sie sich zählen lässt. Implementierungen reichen ihre Konformitätsberichte als YAML in dasselbe Repository ein, in dem auch die Spezifikation liegt, unter conformance/reports. Für 1.6 liegen dort sieben Berichte, für 1.5 waren es fünfzehn. Ausgewertet nach Profil:
| Implementierung | Version | Kanal | GATEWAY-TCP | GATEWAY-UDP |
|---|---|---|---|---|
| Cilium | 1.20.0 | experimental | bestanden | bestanden |
| NGINX Gateway Fabric | 2.7.0-pre-release | experimental | bestanden | bestanden |
| kgateway | 2.4.0-rc.1 | experimental | bestanden | nicht eingereicht |
| agentgateway | 1.3.1 | experimental | nicht eingereicht | nicht eingereicht |
| Airlock Microgateway | 5.1.0 | experimental | nicht eingereicht | nicht eingereicht |
| Traefik | 3.7.10 | experimental | nicht eingereicht | nicht eingereicht |
| GKE Gateway | 1.6.0-gke | standard | nicht eingereicht | nicht eingereicht |
Drei Zahlen springen heraus. Erstens bestehen genau drei Implementierungen die TCP-Prüfung, und zwei davon sind Vorabversionen, ein Release-Kandidat und ein Pre-Release. Zweitens wurde jede einzelne bestandene TCP- und UDP-Prüfung gegen den Experimental-Kanal gefahren. Drittens gibt es genau einen Bericht gegen den Standard-Kanal, und der deckt nur HTTP ab.
Anders gesagt: Für die Aussage „TCPRoute funktioniert im Standard-Kanal” existiert derzeit kein einziger eingereichter Nachweis. Das ist kein Vorwurf. Das Release ist wenige Tage alt, und die Implementierungen brauchen naturgemäß einen Zyklus. Es ist einfach der Unterschied zwischen „steht in der Spezifikation” und „läuft in deinem Cluster”, und dieser Unterschied ist gerade jetzt gut messbar.
Was daraus folgt
Schreibe neue L4-Routen als TCPRoute und UDPRoute in v1, auch wenn dein Controller sie heute noch nicht liest. Das Zieldatenmodell steht fest, es ist stabil versioniert, und ein Manifest, das der Apiserver annimmt, ist bereits die Migration. Der Controller holt auf, dein YAML muss es dann nicht mehr.
Behandle den Kanalwechsel als eigenes Vorhaben, nicht als Nebenwirkung eines Upgrades. Prüfe vorher, gegen welche API-Version dein Controller tatsächlich beobachtet, nicht welche CRDs installiert sind. Wer heute produktiv TCPRoute fährt, fährt es auf v1alpha2 und damit auf dem Experimental-Kanal. Der Sprung auf den Standard-Kanal nimmt diese Version weg.
Prüfe supportedFeatures und den Konformitätsbericht, nicht die Feature-Matrix auf der Herstellerseite. Beides liegt öffentlich, beides ist maschinenlesbar, und beides sagt dir etwas anderes als eine Marketingseite. Das ist der praktische Wert daran, dass Spezifikation und Nachweis im selben offenen Repository liegen.
Fazit
- TCPRoute und UDPRoute sind seit Gateway API 1.6 im Standard-Kanal und tragen
gateway.networking.k8s.io/v1. Damit gibt es erstmals eine herstellerunabhängige Beschreibung für rohes TCP- und UDP-Routing am Cluster-Rand. - Das API ist bewusst minimal: genau eine Regel je Route, keine Matcher, weil Layer 4 nichts zum Matchen hat. Die Routing-Entscheidung ist der Listener-Port. Bis zu 16 gewichtete Backends je Regel sind trotzdem drin.
- Der Standard-Kanal liefert
v1alpha2nicht mehr aus. Alte Manifeste schlagen laut fehl, alte Controller sehen still keine Routen mehr. Das Mischen beider Kanäle unterbindet eine mitgelieferteValidatingAdmissionPolicy. - Echte experimentelle Ressourcen liegen ab jetzt in der eigenen Gruppe
gateway.networking.x-k8s.iomitX-Präfix. Reife ist damit am Manifest ablesbar und nicht mehr nur an der Herkunft der CRD-Datei. - Von sieben Konformitätsberichten für 1.6 bestehen drei die TCP-Prüfung, zwei davon als Vorabversion, und alle gegen den Experimental-Kanal. Für den Standard-Kanal gibt es bislang keinen TCP-Nachweis.
Der eigentliche Gewinn dieses Release ist nicht das TCP-Routing. Das konnte jeder Ingress-Controller vorher schon, teils seit Jahren, teils komfortabler. Der Gewinn ist, wem die Beschreibung des Routings gehört.
IngressRouteTCP ist seit Jahren v1alpha1 und wird es bleiben, weil sie einem Hersteller gehört und dessen Versionsplan folgt. Wechselst du den Controller, wirfst du sie weg. Eine TCPRoute wirfst du nicht weg, du hängst sie an ein anderes Gateway. Genau darin unterscheidet sich eine Plattform, die du bedienst, von einer, die dir gehört: nicht darin, ob sie heute läuft, sondern darin, was von deiner Arbeit übrig bleibt, wenn ein Baustein darunter verschwindet.
Wenn ingress-nginx eine Lehre hinterlässt, dann diese. Der Controller war Open Source, das hat ihn nicht gerettet. Was jetzt hilft, ist, dass die Konfiguration selbst nicht mehr ihm gehört.
© 2026 Trutz Software Consulting GmbH. CC BY 4.0: Dieser Text darf mit Namensnennung weiterverwendet und bearbeitet werden, auch kommerziell. Code-Beispiele darin stehen unter Apache 2.0. Ausgenommen sind Marke und Logo sowie gekennzeichnete Zitate Dritter. Was das genau heißt.
Newsletter
Erhalte praxisnahe Tutorials, Best Practices und echte Lösungen für die häufigsten DevOps-Probleme direkt in Dein Postfach.
Datenschutz: Deine E-Mail-Adresse wird DSGVO-konform in Deutschland gespeichert. Keine Spam, nur relevante Inhalte. Eine E-Mail alle 1 bis 2 Wochen.