Kubernetes Gateway API: Ingress vs Gateway API
CKA prep • GatewayClass / Gateway / HTTPRoute, the role-oriented model, why it supersedes Ingress
Key terms
| Term | Meaning |
|---|---|
| Gateway API | The GA successor to Ingress for L4/L7 routing |
| GatewayClass | Cluster resource naming the controller implementation (like IngressClass) |
| Gateway | A request for an actual load balancer with listeners + ports |
| HTTPRoute | Host/path/header routing rules attached to a Gateway |
| Listener | A port + protocol + hostname on a Gateway |
| Route attachment | How a Route binds to a Gateway (via parentRefs + allowedRoutes) |
| Role-oriented | Separates infra/cluster/app personas into different resources |
Problem & solution
Ingress is simple but cramped: it only does HTTP host/path routing, and every real feature (header routing, traffic splitting, TLS modes, gRPC) lives in vendor-specific annotations that do not port between controllers. There is also no clean split between the platform team that runs the load balancer and the app team that owns routes. Gateway API is the GA, expressive, portable, and role-oriented replacement.
Solution: Model routing as typed resources (GatewayClass, Gateway, HTTPRoute) split by persona, expressing host/path/header/weight routing in the spec instead of annotations, with a standard API that is portable across implementations.
The analogy
A modern port gate is split by role: the port authority installs the gate and owns the hardware and lanes, choosing a gate brand to build it, while each tenant writes only their own routing slips that say which trucks go to which pier through that shared gate. No tenant touches the gate itself, and the slips point at the destination piers. Gateway API splits routing the same way: the GatewayClass names the implementation, the Gateway is the operator-owned load balancer with its listeners, each HTTPRoute is an app team's own routing rules, and backendRefs point at the Services behind them.
The three core resources and their owners
Gateway API deliberately splits what used to be one Ingress object into three resources, each owned by a different team (persona). The infrastructure provider supplies the GatewayClass (which controller implements routing), the cluster operator creates the Gateway (the actual load balancer and its listening ports), and app developers write HTTPRoutes (the host/path rules that point at their Services). The diagram below shows how those three layers reference each other down to the pods.
Three personas, three resources — no shared annotation blob.
GatewayClass (the implementation)
Installed by the controller (Envoy Gateway, NGINX Gateway Fabric, Istio, Cilium, cloud providers). Analogous to IngressClass / a StorageClass.
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: example-gateway-class
spec:
controllerName: example.net/gateway-controller
Gateway (the load balancer + listeners)
A Gateway asks the chosen implementation for a real load balancer and declares the ports, protocols, and hostnames it listens on. This one opens an HTTP listener and a TLS-terminating HTTPS listener:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: web-gateway
namespace: infra
spec:
gatewayClassName: example-gateway-class
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: All # which namespaces may attach routes
- name: https
protocol: HTTPS
port: 443
hostname: "*.example.com"
tls:
mode: Terminate
certificateRefs:
- name: example-tls
HTTPRoute (the routing rules)
A route attaches to a Gateway via parentRefs and can do path, header, and
weighted backend routing directly in the spec. The backends here are real
Online Boutique
Services (frontend, cartservice, checkoutservice) — Google's widely-used
microservices demo, so the example mirrors a real app instead of placeholders.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: storefront
namespace: boutique
spec:
parentRefs:
- name: web-gateway
namespace: infra
hostnames:
- "shop.boutique.example.com"
rules:
- matches:
- path: { type: PathPrefix, value: /cart }
backendRefs:
- name: cartservice # Online Boutique cart microservice
port: 7070
- matches:
- path: { type: PathPrefix, value: /checkout }
headers:
- name: x-canary
value: "true"
backendRefs:
- name: checkoutservice-v2 # canary build of checkout
port: 5050
weight: 20
- name: checkoutservice # stable checkout
port: 5050
weight: 80
Ingress vs Gateway API
Gateway API was designed to fix Ingress's limits, so a direct comparison shows why it supersedes it:
+----------------------+--------------------------+---------------------------+ | | Ingress | Gateway API | +----------------------+--------------------------+---------------------------+ | scope | HTTP host/path only | HTTP, gRPC, TCP, TLS, UDP | | advanced routing | vendor annotations | header/weight in the spec | | portability | low (annotation lock-in) | high (standard API) | | personas | one resource | GatewayClass/Gateway/Route| | cross-namespace | awkward | first-class + ReferenceGrant| | status | stable but frozen | GA, actively extended | +----------------------+--------------------------+---------------------------+
Install and inspect
Gateway API ships as CRDs plus a controller; they are not built into core Kubernetes yet.
# install the standard-channel CRDs, then a controller of your choice
kubectl apply -f \
https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.0/standard-install.yaml
kubectl get gatewayclass
kubectl get gateway -A
kubectl get httproute -A
kubectl describe gateway web-gateway -n infra # check Programmed/Accepted conditions
Cross-namespace routing with ReferenceGrant
Routes in one namespace may target backends in another only if a
ReferenceGrant in the backend's namespace allows it — explicit, auditable
trust instead of annotation guesswork.
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-store-to-payments
namespace: payments
spec:
from:
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: store
to:
- group: ""
kind: Service
End-to-end: a request through the Gateway API
Every node below names the actual resource or field from the manifests above, so you can trace the request straight into the YAML.
Graph legend — each node maps to a concrete field and what it does:
| Graph node | Maps to (manifest field) | What it does |
|---|---|---|
| Gateway listener :443 | Gateway.spec.listeners[].port | The HTTPS port the load balancer opens for inbound traffic |
| TLS Terminate | listeners.tls.mode: Terminate + certificateRefs | Gateway decrypts inbound HTTPS using the referenced cert, then forwards plain HTTP to the backend |
| Match HTTPRoute by hostname | HTTPRoute.spec.hostnames | Selects the route whose hostname equals the request host |
| Path / header match | rules.matches.path / .headers | Picks the rule by URL path prefix and request header |
| backendRef cartservice:7070 | rules.backendRefs | The pod-backed Service that actually serves the request |
| Weighted split 20/80 | backendRefs[].weight | Sends a percentage of traffic to each backend (canary rollout) |
| kube-proxy to a ready pod | the Service's Endpoints | Load-balances the connection to one healthy pod |
End-to-end example: routing Google's Online Boutique through one Gateway
This uses a real, well-known app —
Online Boutique
(Google's microservices demo: frontend, cartservice, checkoutservice,
productcatalogservice, …). A platform team runs one TLS-terminating Gateway;
two app teams attach HTTPRoutes that split traffic by host and path to the real
boutique Services.
- Platform: install the CRDs, then define the GatewayClass and Gateway:
kubectl apply -f \
https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.0/standard-install.yaml
# deploy the real Online Boutique app (frontend, cartservice, checkoutservice, ...)
kubectl create namespace boutique
kubectl -n boutique apply -f \
https://raw.githubusercontent.com/GoogleCloudPlatform/microservices-demo/main/release/kubernetes-manifests.yaml
kubectl create namespace infra
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: prod-class
spec:
controllerName: example.net/gateway-controller
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: web-gateway
namespace: infra
spec:
gatewayClassName: prod-class
listeners:
- name: https
protocol: HTTPS
port: 443
hostname: "*.boutique.example.com"
tls:
mode: Terminate
certificateRefs:
- name: boutique-tls
allowedRoutes:
namespaces:
from: All
- App team A (storefront): an HTTPRoute for
shop.boutique.example.comthat splits by path across the real boutique frontend and cart/checkout services:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: storefront
namespace: boutique
spec:
parentRefs:
- name: web-gateway
namespace: infra
hostnames:
- "shop.boutique.example.com"
rules:
- matches:
- path: { type: PathPrefix, value: /cart }
backendRefs:
- name: cartservice
port: 7070
- matches:
- path: { type: PathPrefix, value: /checkout }
backendRefs:
- name: checkoutservice
port: 5050
- matches:
- path: { type: PathPrefix, value: / }
backendRefs:
- name: frontend
port: 80
- App team B (catalog API): a second HTTPRoute on a different host,
api.boutique.example.com, to the product catalog service:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: catalog-api
namespace: boutique
spec:
parentRefs:
- name: web-gateway
namespace: infra
hostnames:
- "api.boutique.example.com"
rules:
- matches:
- path: { type: PathPrefix, value: /products }
backendRefs:
- name: productcatalogservice
port: 3550
- Apply everything and confirm the Gateway is Programmed and both routes Accepted:
kubectl apply -f gatewayclass.yaml -f gateway.yaml -f storefront-route.yaml -f catalog-route.yaml
kubectl -n infra get gateway web-gateway \
-o jsonpath='{.status.conditions[?(@.type=="Programmed")].status}{"\n"}' # True
kubectl -n boutique get httproute storefront \
-o jsonpath='{.status.parents[0].conditions[?(@.type=="Accepted")].status}{"\n"}' # True
kubectl -n boutique get httproute catalog-api \
-o jsonpath='{.status.parents[0].conditions[?(@.type=="Accepted")].status}{"\n"}' # True
- Test both hosts against the Gateway address without changing DNS:
GW=$(kubectl -n infra get gateway web-gateway -o jsonpath='{.status.addresses[0].value}')
curl -k --resolve shop.boutique.example.com:443:$GW https://shop.boutique.example.com/
# expected: the Online Boutique storefront HTML from the frontend Service
curl -k --resolve api.boutique.example.com:443:$GW https://api.boutique.example.com/products
# expected: product list from the productcatalogservice
Graph legend — every node is a real boutique resource from the manifests above:
| Graph node | Maps to | What it does |
|---|---|---|
| Gateway web-gateway | kind: Gateway (infra ns) | One shared, platform-owned load balancer terminating TLS on *.boutique.example.com |
| HTTPRoute storefront | kind: HTTPRoute (boutique ns) | App team A's path split: /cart→cartservice, /checkout→checkoutservice, /→frontend |
| HTTPRoute catalog-api | kind: HTTPRoute (boutique ns) | App team B's host route: /products→productcatalogservice |
| parentRefs | HTTPRoute.spec.parentRefs | How each route attaches itself to the shared Gateway |
| Match host + path | the Gateway's routing engine | Picks the route by hostname, then the rule by path prefix |
| Service cartservice:7070, etc. | the boutique microservices | Real Online Boutique Services that serve each request |
Common pitfalls
These are the most common Gateway API mistakes and what each symptom points to:
- CRDs not installed -> kind Gateway/HTTPRoute unknown to the API
- GatewayClass has no ctrl -> Gateway stays not Programmed (nothing implements it)
- route won't attach -> allowedRoutes.namespaces or parentRefs mismatch
- cross-namespace backend -> missing ReferenceGrant in the backend namespace
- expecting Ingress annot. -> use spec fields (matches/weight), not annotations
- wrong apiVersion -> v1 for Gateway/HTTPRoute, v1beta1 for ReferenceGrant
Key takeaways
- Gateway API is the GA successor to Ingress: richer, portable, role-oriented.
- Three resources: GatewayClass (impl), Gateway (LB+listeners), HTTPRoute (rules).
- Advanced routing (headers, weighted splits, TLS modes) lives in the spec, not annotations.
- It ships as CRDs + a controller, not in core Kubernetes (yet).
- allowedRoutes + ReferenceGrant make cross-namespace routing explicit and safe.
- Check Accepted/Programmed conditions to confirm a Gateway is wired up.
Checklist
- [ ] Installed the Gateway API CRDs and a controller
- [ ] Created a GatewayClass and a Gateway with HTTP + HTTPS listeners
- [ ] Wrote an HTTPRoute with path + header matches and a weighted split
- [ ] Allowed a route from another namespace with a ReferenceGrant
- [ ] Contrasted Ingress annotations vs Gateway API spec fields
- [ ] Verified
Programmed/Acceptedstatus conditions