Gateway API and GatewayClass
The Gateway API is not in Kubernetes - it arrives as CRDs you install. This shows what that means concretely: before, the API group does not exist; after, the kinds are there and every object you create sits at Accepted=Unknown with no address, because CRDs are a vocabulary and not an implementation.
Networking and Discovery Guide 21 of 46 Intermediate
- Kubernetes1.36.4
- Cluster4 nodes
- Runtimecontainerd 2.2.6
- CNICalico v3.32.1
- Gateway APIv1.4.0 (standard)
- TimeAbout 15 min
- Reviewed23 August 2026
Written against the versions above. Gateway API v1.4.0 standard channel. It is developed by SIG-Network inside the Kubernetes project - which is why its group is gateway.networking.k8s.io and not a vendor domain - but it ships separately from the cluster and always has to be installed.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| CKA1001 | 192.168.0.175 | Ubuntu 26.04 LTS | Control Plane Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE01 | 192.168.0.176 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE02 | 192.168.0.177 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE03 | 192.168.0.178 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
Before you start
- A cluster where the Gateway API is not already installed, and permission to create CRDs - they are cluster-scoped.
- Two Services to point routes at. This session uses a
gwnamespace withblueandgreen.
-
The API is not in your cluster
Start by establishing what is not there. Count the Gateway API CRDs:
0And ask the API server for the group:
NAME SHORTNAMES APIVERSION NAMESPACED KINDA header and nothing under it. The Gateway API is not part of Kubernetes. That is the fact this whole guide exists to fix in people's heads. Ingress is built in -
networking.k8s.io/v1, present on every cluster. Gateway API is a set of CustomResourceDefinitions that somebody has to install, and until they do,kubectl applyof a Gateway fails outright because the kind does not exist.It is still a Kubernetes project - SIG-Network develops it, which is why the group is
gateway.networking.k8s.iorather than a company's domain. Being an official project and being installed are different things.bash Example session kubectl get crd -o name 2>/dev/null | grep -c gateway.networking.k8s.io || echo 000kubectl api-resources --api-group=gateway.networking.k8s.io 2>&1 | head -3NAME SHORTNAMES APIVERSION NAMESPACED KINDExpected resultZero CRDs, and an api-resources listing with a header and no rows.
Success conditionYou can say whether Gateway API ships with Kubernetes.
-
Installing the CRDs adds vocabulary, not behaviour
Apply the standard install and the kinds appear:
NAME SHORTNAMES APIVERSION NAMESPACED KIND backendtlspolicies btlspolicy gateway.networking.k8s.io/v1 true BackendTLSPolicy gatewayclasses gc gateway.networking.k8s.io/v1 false GatewayClass gateways gtw gateway.networking.k8s.io/v1 true GatewayNote
GatewayClassisNAMESPACED false- it is cluster-scoped, like IngressClass and StorageClass. Anything that names an implementation tends to be.Now the part that matters:
No resources foundThere are no GatewayClasses. The CRDs define what a Gateway *is*; they say nothing about who acts on one. A CRD is a schema the API server will now store and validate - a noun added to the cluster's vocabulary. Behaviour comes from a controller, and installing the API installed none.
bash Example session sleep 6; kubectl api-resources --api-group=gateway.networking.k8s.ioNAME SHORTNAMES APIVERSION NAMESPACED KINDbackendtlspolicies btlspolicy gateway.networking.k8s.io/v1 true BackendTLSPolicygatewayclasses gc gateway.networking.k8s.io/v1 false GatewayClassgateways gtw gateway.networking.k8s.io/v1 true Gatewaykubectl get gatewayclass --no-headers 2>&1 | head -3No resources foundExpected resultThe Gateway API kinds present, and no GatewayClass at all.
Success conditionYou can state the difference between a CRD and a controller.
-
An object that validates and does nothing
Create a Gateway naming a class nothing implements. It is accepted - the schema is valid, so the API server stores it:
orphan nothing-installed Unknown 12sconditions=Accepted=Unknown(Pending) Programmed=Unknown(Pending)Read those two conditions carefully, because
Unknownis doing real work here. NotFalse.Falsewould be a controller saying no.Unknownwith reasonPendingis the API's way of saying *nobody has answered at all* - the status was initialised and no controller has ever looked at it.That distinction is the whole diagnostic. A
Falsecondition means read the message, because something rejected your configuration.Unknown(Pending)forever means no controller is watching, and no amount of fixing the YAML will change it.The same happens with a properly named class once a controller's manifests are applied but its Pod has not become ready - the Gateway still reports nothing:
address= listeners=No address to send traffic to, and no listeners reporting.
Programmedis the condition that says actual dataplane configuration happened; until it isTrue, the Gateway is a document.bash Example session sleep 12; kubectl get gateway orphan --no-headersorphan nothing-installed Unknown 12skubectl get gateway orphan -o jsonpath='conditions={range .status.conditions[*]}{.type}={.status}({.reason}) {end}{"\n"}'conditions=Accepted=Unknown(Pending) Programmed=Unknown(Pending) sleep 30; kubectl get gateway shop -n gw --no-headersshop nginx Unknown 30skubectl get gateway shop -n gw -o jsonpath='address={.status.addresses[0].value} listeners={range .status.listeners[*]}{.name}:attached={.attachedRoutes}{" "}{end}{"\n"}'address= listeners=Expected resultA stored Gateway with both conditions Unknown(Pending), no address and no listeners.
Success conditionYou can tell Unknown(Pending) from False, and say what each implies.
-
A route attached to nothing
An HTTPRoute is the object that actually carries rules - paths, hostnames, backends - and it attaches itself to a Gateway through
parentRefs. Creating one also succeeds:shop-routes ["shop.cg.test"] 20sThe hostname is recorded. And the Gateway's view of it:
attachedRoutes=Empty. The route exists, names a real Gateway, and is attached to nothing - because attachment is something the *controller* records, and there isn't one working.
attachedRouteson the Gateway's listener is the field to read when a route appears to be ignored; a number there means the controller saw it.That is the shape of the whole API and the reason it is worth learning even before you use it. Ingress puts routing rules and implementation-specific behaviour in one object, with a pile of vendor annotations. Gateway API splits them by role: GatewayClass is the implementation, chosen by a cluster administrator; Gateway is a listener, owned by whoever runs the platform; HTTPRoute is a rule, owned by the team that owns the service. Weighted splits between backends - a canary - are expressed in the API itself rather than in an annotation, which is the concrete thing Ingress cannot do portably.
Which is also why the honest close to this guide is that nothing was routed. The controller never became ready, so the objects stayed exactly as described here. For KCNA that is the more useful outcome anyway: install the API, and you have the nouns; install a controller, and you have the behaviour.
bash Example session sleep 20; kubectl get httproute shop-routes -n gw --no-headersshop-routes ["shop.cg.test"] 20skubectl get gateway shop -n gw -o jsonpath='attachedRoutes={.status.listeners[0].attachedRoutes}{"\n"}'attachedRoutes=kubectl get svc -n gw --no-headers | awk '{print $1, $2, $4}'blue ClusterIP <none>green ClusterIP <none>Expected resultAn HTTPRoute stored with its hostname, and a Gateway reporting no attached routes.
Success conditionYou know which field tells you whether a controller has seen your route.
Troubleshooting
kubectl apply of a Gateway fails saying the kind is not recognised.
Why: The Gateway API CRDs are not installed. Unlike Ingress, the API does not ship with Kubernetes.
Fix:Install a release of the CRDs, then confirm with
kubectl api-resources --api-group=gateway.networking.k8s.io.A Gateway sits at Accepted=Unknown(Pending) and never changes.
Why: No controller is reconciling it. Unknown means nobody answered - as opposed to False, which is a controller refusing.
Fix:
kubectl get gatewayclass- if it is empty, or the class named in the Gateway is missing, install a controller that implements it. Then check the controller's own Pods are Ready.An HTTPRoute is ignored and no traffic matches it.
Why: It is not attached. A route names a parent Gateway, and the controller decides whether the attachment is permitted - namespace rules and listener hostnames both have to allow it.
Fix:Read
attachedRouteson the Gateway's listener. Zero or empty means the controller did not attach it; then check the route's parentRefs and the listener's allowedRoutes.A Gateway has conditions True but no address.
Why: Programmed can be True while the implementation is still obtaining an address, or where the implementation exposes itself by another means entirely.
Fix:Read the controller's own documentation for how it publishes an address, and check its Service - many run as a Deployment behind a LoadBalancer or NodePort Service of their own.