Gateway API: HTTPRoute
Why routing got split across GatewayClass, Gateway, and HTTPRoute, and how that differs from Ingress.
Why the cluster entrance got split into GatewayClass, Gateway and HTTPRoute, how to describe a route, and how this differs from Ingress.
What's wrong with Ingress
Ingress has been around for ten years and isn't going anywhere. It's still GA, it's still used everywhere, and nothing in the cluster forbids it. But it has three pain points that pile up as a team grows.
First: one object describes everything at once. Hosts, paths, TLS, sometimes even backend settings through annotations. Whoever writes the Ingress has to know about the infrastructure (ports, certificates) and about the routing of their own Service.
Second: extensions through annotations.
nginx.ingress.kubernetes.io/..., traefik.ingress.kubernetes.io/....
Switch from one controller to another, and half your YAML turns into
garbage. That's exactly what happened: ingress-nginx, whose annotations
spread across every guide, reached end of maintenance in March 2026, and now those
lines need rewriting for a different implementation (more in the lesson
on Ingress).
Third: HTTP and HTTPS only. TCP, UDP, gRPC and mTLS get wrapped through the same annotations or a vendor-specific CRD.
Gateway API fixes this differently: instead of one object, it introduces three, each with its own owner.
Three roles, three objects
HTTPRoute references a plain Service of type ClusterIP, exactly the way Ingress did. What's new here is only the split of the entrance itself.
GatewayClass: traefik <- the cluster admin picked the implementation
|
v
Gateway: public <- infrastructure, namespace gateway-system
| listens on :80, *.example.com
v
HTTPRoute: helloroute <- application team, in its own namespace
| hostnames: [helloapp.example.com]
| backendRefs: [helloapp:8080]
v
Service helloapp -> Pod
GatewayClass defines which controller will handle traffic (Traefik, Cilium, Istio, NGINX Gateway Fabric and others). The cluster admin creates it once. A developer only needs it to make sure the right controller is actually installed:
kubectl get gatewayclass
NAME CONTROLLER ACCEPTED AGE
traefik traefik.io/gateway-controller True 42d
Gateway describes the actual entry point: what port it listens on,
which hosts, with what TLS. The infrastructure team creates it, usually
in a separate namespace like gateway-system. Many teams can share one
Gateway.
HTTPRoute holds the application's rules: which hosts and paths to
catch, where to send them. A developer creates it in their own
namespace. The route attaches to a Gateway through parentRefs.
The result is a split of responsibility. Infrastructure owns the entrance and decides who gets access to it. Application teams describe only their own routes and never touch the shared object.
A minimal HTTPRoute
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: helloroute
namespace: hello
spec:
parentRefs:
- name: public
namespace: gateway-system
hostnames:
- helloapp.example.com
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: helloapp
port: 8080
Key fields:
apiVersion: gateway.networking.k8s.io/v1is the GA version. It arrived with the Gateway API v1.0 release in October 2023. Gateway API lives outside Kubernetes core and installs as separate CRDs, so the available version depends on which CRDs are installed in the cluster, not on the Kubernetes version itself. If the cluster only knowsv1beta1orv1alpha2, its CRDs are outdated and need an update.parentRefs.namespaceis needed when the Gateway lives in another namespace. Without this field the route looks for a Gateway in its own namespace. The API accepts such a route silently, but the controller won't find a parent for it.hostnamesis an array. One route can carry several names.path.type: PathPrefix, exactly spelled this way. Ingress called the same typePrefix, and that's a common typo during migration. The path value must start with/.backendRefs[].portis the Service port, not the container port. For a Service this field is required: without it the API repliesMust have port for Service reference.backendRefs[]can list several backends with weights, more on that below.
A rule with no matches catches every request: the API fills in
PathPrefix / on its own. You can see this in
kubectl get httproute ... -o yaml after apply.
Who grants access: allowedRoutes and ReferenceGrant
A route from namespace hello attaches to a Gateway in
gateway-system. So that not every team can attach its own rules to
someone else's entrance, permission comes from the side being
referenced.
For linking a route to a Gateway, that's the allowedRoutes field on the
Gateway's listener:
spec:
gatewayClassName: traefik
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
shared-gateway: "true"
from takes three values: Same (only routes from the Gateway's own
namespace), All (from any namespace), and Selector (from namespaces
with the right labels). If allowedRoutes isn't set, Same applies. So
a route from a different namespace won't attach to the Gateway by
default, even if parentRefs is written correctly.
ReferenceGrant is needed in a different case: when a route sends
traffic to a Service in another namespace (backendRefs[].namespace),
or a Gateway pulls a TLS certificate from a Secret in another namespace.
A ReferenceGrant is created in the namespace of the side being
referenced, and lists who's allowed to reference it.
Weights and headers
In Gateway API, traffic splitting is built right into the spec:
backendRefs:
- name: helloapp-v1
port: 8080
weight: 90
- name: helloapp-v2
port: 8080
weight: 10
weight isn't a percentage. A backend's share equals its weight divided
by the sum of weights in that rule: 90 and 10 give the same result
as 9 and 1. With no weight set, it defaults to 1, so two backends
with no weight split traffic evenly. weight: 0 means nothing goes to
that backend.
Besides the path, a rule can also check headers, the method and query parameters:
rules:
- matches:
- headers:
- name: env
value: canary
backendRefs:
- name: helloapp-v2
port: 8080
The default comparison type for a header is Exact. Conditions inside
one matches element combine with AND, and different elements of the
matches list combine with OR.
If a request fits several rules, the controller picks the most specific
match: first an Exact path, then the longest PathPrefix, then a rule
with a method condition, then the one with more header conditions. The
order of rules in the YAML doesn't affect the choice.
With Ingress, this was done through annotations or two separate Ingress objects, which often led to races.
Filters: header edits, redirect, URL rewrite, mirror
rules[].filters holds request modifiers. The most common ones:
RequestHeaderModifier: add or remove headers on the request.ResponseHeaderModifier: the same for the response.URLRewrite: change the host or path before it reaches the backend. Handy when the outside path is/api/usersbut the Service expects/users.RequestRedirect: return a 301 or 302 without reaching the backend.RequestMirror: send a copy of the request to another backend for shadow testing.
An example of URLRewrite:
rules:
- matches:
- path: { type: PathPrefix, value: /api }
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /
backendRefs:
- name: api-service
port: 80
Comparison with Ingress
| What it does | Ingress | HTTPRoute |
|---|---|---|
| HTTP routing | yes | yes |
| TCP/UDP/TLS/gRPC | through vendor annotations/CRD | separate types: TCPRoute, UDPRoute, TLSRoute, GRPCRoute |
| Host | rules[].host | spec.hostnames[] |
| Path match | pathType: Prefix/Exact | path.type: PathPrefix/Exact/RegularExpression |
| Header conditions | through annotations | matches[].headers in the spec |
| Multiple weighted backends | through annotations | backendRefs[].weight in the spec |
| Cross-namespace backend | no | backendRefs.namespace + ReferenceGrant |
| Header edits/redirect | through annotations | filters in the spec |
| Standard across vendors | minimal | conformance tests |
| Status | GA, API frozen | GA since Gateway API v1.0, actively growing |
Ingress isn't going away. If one Ingress covers one Service and a single team owns it, there's no need to move. Gateway API earns its keep once more than two teams are involved, or non-HTTP protocols show up.
Where the ecosystem is heading
Gateway API has already become the direction the ecosystem is moving
in. The push came from the end of ingress-nginx: the controller running
half the clusters out there reached end of maintenance in March 2026, and teams had
to pick a replacement. Nearly every live implementation is betting on
Gateway API, and there's a tool for the move,
ingress2gateway,
which generates HTTPRoute objects from existing Ingress ones.
Ingress still needs to be understood either way: it's GA, not deprecated, and its manifests will keep showing up for years. But new designs target HTTPRoute. The foundation stays the same, Service and NetworkPolicy, and Gateway API sits one layer above, handling only inbound traffic.
How Gateway API gets into a cluster
A plain cluster has no Gateway API resources:
kubectl get httproute
error: the server doesn't have a resource type "httproute"
kubectl apply of a manifest with an HTTPRoute in such a cluster fails
with
no matches for kind "HTTPRoute" in version "gateway.networking.k8s.io/v1"
and a hint to ensure CRDs are installed first.
CRDs are installed from the project's release. The Standard channel holds the stable resources, Experimental adds the experimental ones (covered in the lesson on local cluster add-ons):
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.6.3/standard-install.yaml
customresourcedefinition.apiextensions.k8s.io/backendtlspolicies.gateway.networking.k8s.io created
customresourcedefinition.apiextensions.k8s.io/gatewayclasses.gateway.networking.k8s.io created
customresourcedefinition.apiextensions.k8s.io/gateways.gateway.networking.k8s.io created
customresourcedefinition.apiextensions.k8s.io/grpcroutes.gateway.networking.k8s.io created
customresourcedefinition.apiextensions.k8s.io/httproutes.gateway.networking.k8s.io created
...
CRDs only describe the objects. Traffic starts flowing once a
controller shows up in the cluster that has claimed a GatewayClass as
its own. Some controllers install the CRDs themselves: for example, the
Envoy Gateway Helm chart applies the Gateway API CRDs by default. Then a
separate apply isn't needed.
Status: what the controller writes
The API only checks the shape of the object: allowed values, required
fields, a path starting with /. Whether the Gateway from parentRefs
exists, whether it accepts the route, and whether a Service with that
port exists, is decided by the controller. It writes the answer to
status.parents[].conditions on the route:
Accepted=True: the Gateway agreed to handle the route.ResolvedRefs=True: allbackendRefswere found and resolved.
On a Gateway, the controller sets Accepted and Programmed. While
there's no controller, the Gateway sits at Unknown:
kubectl get gateway -A
NAMESPACE NAME CLASS ADDRESS PROGRAMMED AGE
gateway-system public traefik Unknown 1m
kubectl get gateway public -n gateway-system -o yaml then shows
reason: Pending and message: Waiting for controller. An HTTPRoute,
meanwhile, has no status block at all.
What commonly breaks
Prefixinstead ofPathPrefix. Copied from Ingress, forgot to rename it. The API rejects such a route right atapply:Unsupported value: "Prefix": supported values: "Exact", "PathPrefix", "RegularExpression".- No
namespaceinparentRefs. The route looks for a Gateway in its own namespace. The API lets this through, the controller won't find a parent, and the route never becomesAccepted. - The Gateway doesn't admit the route's namespace.
allowedRoutesdefaults toSame. Check the Gateway's spec:from, and if it'sSelector, the labels on your own namespace. - Container port in
backendRefs. It needs the Service port. The API lets this through, the controller setsResolvedRefs=False. - A backend in another namespace with no ReferenceGrant. Also
ResolvedRefs=False. - The Gateway isn't
Programmed.kubectl get gateway -Ashows the status. If the Gateway isUnknownorFalse, check that the controller is installed and the GatewayClass isACCEPTED True. v1beta1in a manifest. The cluster still accepts older manifests withv1beta1for HTTPRoute and Gateway: the v1.6.3 CRDs serve both versions. Usev1for new ones.
Commands for exploring
kubectl get gatewayclass
kubectl get gateway -A
kubectl get httproute -A
NAMESPACE NAME HOSTNAMES AGE
hello helloroute ["helloapp.example.com"] 2m
kubectl describe httproute helloroute -n hello
In the describe output, look at the Status, Parents, Conditions
block: that's where Accepted and ResolvedRefs show up with a reason.
Reference for any field is available right in the cluster:
kubectl explain httproute.spec.rules.backendRefs.weight
What you'll need for the tasks
kubectl get gatewayclass,kubectl get gateway -A,kubectl get httproute -A: what exists in the cluster and where it lives.kubectl get httproute <name> -n <namespace> -o yaml: what's actually recorded in a route after all the API's defaults.- HTTPRoute fields:
parentRefswithnamespace,hostnames,matcheswithpathandheaders,backendRefswithportandweight. allowedRouteson a Gateway, and namespace labels:kubectl get namespace --show-labels.kubectl explainfor Gateway API fields.- For the mini-project: Service and EndpointSlice, headless Service, NetworkPolicy from earlier lessons in the module.
Homework
Practice the theory on your own cluster: 2 steps with a check. Set up the cluster with the Kubernetes locally track, the tasks live in the k8s-practice repository.
./run.sh start k8s-gateway-basics # set up the starting state
./run.sh verify k8s-gateway-basics # check the current step
In short
- Gateway API splits the entrance into three objects: GatewayClass (which controller), Gateway (the entry point and TLS), HTTPRoute (the application's rules).
- HTTPRoute attaches to a Gateway through
parentRefsand sends traffic to a Service throughbackendRefson the Service's port. - Whether a route from another namespace gets admitted is decided by
the Gateway through
allowedRoutes. It defaults toSame. - In Gateway API, a prefix match is called
PathPrefix, notPrefix. - Backend weights, header conditions, redirects and header edits are all described right in the spec, with no vendor-specific annotations.
- Gateway API CRDs are installed separately from Kubernetes, so the available API version depends on what's installed in the cluster.
- The API checks the shape of a route, while references to a Gateway
and a Service are checked by the controller, which writes the result
to
status.
Done reading?
Mark the lesson as read to move your track progress forward.
Lesson marked as done.
Go through it with an AI agent
The agent explains anything unclear in plain words, gives you 3 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.
Tell the agent: use MCP izi, give me lesson k8s-network-08-gateway-api
Progress saved in this browser
Clear your history or open this on your phone, and the marks will be gone. Leave your email and they'll move to your account. No password needed.
Done. moved to your account. You can see your progress on any device. My progress