The Gateway API
The Gateway API is the Kubernetes project's successor to Ingress. It does the same basic job, getting outside traffic to Services, but it was designed after years of Ingress experience: features such as redirects, header matching and traffic splitting are standard fields instead of controller-specific annotations, and the objects are split so that platform teams and app teams each own their part.
In this lesson you will install a Gateway API implementation on your kind cluster, create a Gateway and HTTPRoutes, and use the features that Ingress could only offer through annotations.
Three roles, three objects
| Object | Usually owned by | What it describes |
|---|---|---|
| GatewayClass | Infrastructure provider | A kind of gateway and the controller that runs it, like IngressClass |
| Gateway | Cluster operator | An actual entry point: which ports and protocols to listen on, TLS certificates, and which namespaces may attach routes |
| HTTPRoute | App developer | Routing rules: host names, paths, headers, and which Services receive the traffic |
The split matters in shared clusters. The operator controls the expensive, security-sensitive part (load balancer, certificates) once, and each team attaches its own routes without touching it. There are other route types too: GRPCRoute for gRPC services, and TLSRoute, TCPRoute and UDPRoute for non-HTTP traffic.
Installing it
The Gateway API is not built into Kubernetes. It has two pieces:
- The CRDs (Custom Resource Definitions) that add the new object types to your cluster. They are versioned separately; v1.7.0 was released on 29 October 2026. The standard channel contains the stable objects; the experimental channel adds objects and fields that may still change.
- An implementation: a controller plus a proxy, such as Envoy Gateway, Istio, Cilium, NGINX Gateway Fabric, Traefik, Kong or a cloud provider's own. The project's implementations page shows which ones pass its conformance tests.
Some implementations ask you to install the CRDs yourself:
kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.7.0/standard-install.yamlThis lesson uses Envoy Gateway, whose Helm chart installs the CRDs it needs together with its controller. Helm is a package manager for Kubernetes that you will study in Helm: Charts, Releases & Values; for now, install it (winget install Helm.Helm or brew install helm) and run:
helm install eg oci://docker.io/envoyproxy/gateway-helm --version v1.9.3 -n envoy-gateway-system --create-namespace
kubectl wait --timeout=5m -n envoy-gateway-system deployment/envoy-gateway --for=condition=AvailableCheck the Envoy Gateway documentation for its current version when you follow along.
GatewayClass and Gateway
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: eg
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: shop-gateway
spec:
gatewayClassName: eg
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: SamecontrollerName ties the class to Envoy Gateway. The Gateway asks for one HTTP listener on port 80, and allowedRoutes says that only routes in the Gateway's own namespace may attach (Same is the default; All and Selector open it to other namespaces). When Envoy Gateway sees this Gateway, it starts an Envoy proxy Deployment and a LoadBalancer Service for it.
An HTTPRoute
Priya's shop has the Services web and api from the Ingress lesson. This route sends /api to the API and everything else to the frontend:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop
spec:
parentRefs:
- name: shop-gateway
hostnames:
- shop.example.com
rules:
- matches:
- path:
type: PathPrefix
value: /api
backendRefs:
- name: api
port: 80
- backendRefs:
- name: web
port: 80parentRefs attaches the route to the Gateway. A rule without matches matches every path, and as with Ingress the most specific match wins.
kubectl apply -f shop-gateway.yaml -f shop-route.yaml
kubectl get gateway shop-gateway
kubectl get httproute shopgatewayclass.gateway.networking.k8s.io/eg created gateway.gateway.networking.k8s.io/shop-gateway created httproute.gateway.networking.k8s.io/shop created NAME CLASS ADDRESS PROGRAMMED AGE shop-gateway eg 172.18.0.6 True 45s NAME HOSTNAMES AGE shop ["shop.example.com"] 30s
This output comes from a kind cluster with cloud-provider-kind running, which hands out addresses for LoadBalancer Services; on a cloud cluster you get a public address instead. Without a load balancer the Gateway has no ADDRESS, but you can still test it by port-forwarding to the Envoy Service that Envoy Gateway created (it lives in envoy-gateway-system and carries the label gateway.envoyproxy.io/owning-gateway-name=shop-gateway):
ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system -l gateway.envoyproxy.io/owning-gateway-name=shop-gateway -o jsonpath='{.items[0].metadata.name}')
kubectl -n envoy-gateway-system port-forward service/$ENVOY_SERVICE 8888:80 &
curl -s -o /dev/null -w "%{http_code}\n" -H "Host: shop.example.com" http://localhost:8888/api/booksIf a route does not work, kubectl describe httproute shop shows its status conditions: Accepted tells you whether the Gateway accepted it, and ResolvedRefs whether its backend Services exist. This built-in feedback is a big improvement over Ingress.
Things Ingress needed annotations for
Redirect HTTP to HTTPS with a standard filter (on a route attached to the HTTP listener, while another listener serves HTTPS):
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301Match on a header, for example to send testers to a beta version:
- matches:
- headers:
- name: x-beta-tester
value: "yes"
backendRefs:
- name: web-beta
port: 80Split traffic by weight for a canary release: 90% of requests go to the stable version and 10% to the new one. Change the weights step by step as confidence grows:
- backendRefs:
- name: web-v1
port: 80
weight: 90
- name: web-v2
port: 80
weight: 10Other standard filters rewrite URLs (URLRewrite), add or remove headers (RequestHeaderModifier, ResponseHeaderModifier) and copy traffic to a test backend (RequestMirror).
Across namespaces
In a shared cluster, the Gateway often lives in an infra namespace and each team's routes in their own namespace. Two safety locks control this:
- The Gateway's
allowedRoutesdecides which namespaces may attach routes to it. - A route that sends traffic to a Service in another namespace needs a ReferenceGrant in that Service's namespace, explicitly allowing it. Without one, the route's
ResolvedRefscondition reports the reference as not permitted.
Both locks stop one team from accidentally (or deliberately) hijacking another team's traffic.
Common mistakes
- Applying routes before the CRDs exist. You get "no matches for kind HTTPRoute". Install the implementation (or the CRDs) first.
- A
parentRefsname or namespace that does not match the Gateway. The route stays unattached; check itsAcceptedcondition. - Forgetting
allowedRoutesor a ReferenceGrant when crossing namespaces. - Mixing CRD versions. Use the Gateway API version your implementation supports, and upgrade CRDs before upgrading the implementation when its notes say so.
Key points
- Gateway API splits routing into GatewayClass (provider), Gateway (operator) and HTTPRoute or other routes (app teams).
- It is installed as CRDs plus an implementation such as Envoy Gateway, Istio, Cilium or a cloud controller; v1.7.0 is the current release.
- HTTPRoutes match on host, path, headers and more, and attach to a Gateway through
parentRefs. - Redirects, rewrites, header changes, mirroring and weighted traffic splitting are standard features, not annotations.
allowedRoutesand ReferenceGrant control what may cross namespace boundaries; route status conditions show what went wrong.
Exercise
Write an HTTPRoute called results for host results.example.com, attached to shop-gateway, that sends / to the Service results-v1 (port 8080) for 80% of requests and to results-v2 (port 8080) for 20%, except that any request with the header x-team: qa always goes to results-v2.
Show solution
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: results
spec:
parentRefs:
- name: shop-gateway
hostnames:
- results.example.com
rules:
- matches:
- headers:
- name: x-team
value: qa
backendRefs:
- name: results-v2
port: 8080
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: results-v1
port: 8080
weight: 80
- name: results-v2
port: 8080
weight: 20A match without a path counts as the prefix /, so both rules match the same path. When paths tie, the rule with more header matches wins, so QA requests take the first rule. Everyone else is split 80/20 by weight.
Quick Quiz
1. Which Gateway API object does an app developer usually write?
Routes belong to app teams; Gateways and classes belong to operators and providers.
2. How do you send 10% of traffic to a new version with the Gateway API?
Weighted backends are a standard HTTPRoute feature.
3. An HTTPRoute in namespace team-a points at a Service in namespace team-b. What else is needed?
ReferenceGrant is the explicit permission for cross-namespace references.
4. Why do you get "no matches for kind HTTPRoute" when applying a route?
Gateway API objects come from CRDs that must be installed first.
Learn Kubernetes with a mentor
Free lessons get you started. Our Cloud & DevOps training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
