47

Kubernetes Gateway API: Ingress vs Gateway API

CKA prep • GatewayClass / Gateway / HTTPRoute, the role-oriented model, why it supersedes Ingress

Key terms

TermMeaning
Gateway APIThe GA successor to Ingress for L4/L7 routing
GatewayClassCluster resource naming the controller implementation (like IngressClass)
GatewayA request for an actual load balancer with listeners + ports
HTTPRouteHost/path/header routing rules attached to a Gateway
ListenerA port + protocol + hostname on a Gateway
Route attachmentHow a Route binds to a Gateway (via parentRefs + allowedRoutes)
Role-orientedSeparates 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 nodeMaps to (manifest field)What it does
Gateway listener :443Gateway.spec.listeners[].portThe HTTPS port the load balancer opens for inbound traffic
TLS Terminatelisteners.tls.mode: Terminate + certificateRefsGateway decrypts inbound HTTPS using the referenced cert, then forwards plain HTTP to the backend
Match HTTPRoute by hostnameHTTPRoute.spec.hostnamesSelects the route whose hostname equals the request host
Path / header matchrules.matches.path / .headersPicks the rule by URL path prefix and request header
backendRef cartservice:7070rules.backendRefsThe pod-backed Service that actually serves the request
Weighted split 20/80backendRefs[].weightSends a percentage of traffic to each backend (canary rollout)
kube-proxy to a ready podthe Service's EndpointsLoad-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 appOnline 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.

  1. 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
  1. App team A (storefront): an HTTPRoute for shop.boutique.example.com that 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
  1. 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
  1. 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
  1. 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 nodeMaps toWhat it does
Gateway web-gatewaykind: Gateway (infra ns)One shared, platform-owned load balancer terminating TLS on *.boutique.example.com
HTTPRoute storefrontkind: HTTPRoute (boutique ns)App team A's path split: /cart→cartservice, /checkout→checkoutservice, /→frontend
HTTPRoute catalog-apikind: HTTPRoute (boutique ns)App team B's host route: /products→productcatalogservice
parentRefsHTTPRoute.spec.parentRefsHow each route attaches itself to the shared Gateway
Match host + paththe Gateway's routing enginePicks the route by hostname, then the rule by path prefix
Service cartservice:7070, etc.the boutique microservicesReal 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/Accepted status conditions