OnCallReady

Lesson 16.26 · Kubernetes: Networking & Storage · 18 min read

Gateway API: GatewayClass, Gateway, HTTPRoute

In plain words

Imagine a shopping mall. The mall owner decides what kind of entrances the building has (the GatewayClass: "revolving doors made by this company"). Building management runs the actual entrances, decides the opening hours and which shops may put up signs there (the Gateway: ports, hostnames, TLS, allowed namespaces). Each shop puts up its own signs pointing customers to its counters (the HTTPRoute: paths, headers, weights). A shop can't send customers into another shop's storeroom unless that shop has written a permission slip (ReferenceGrant).

With Ingress, all of that lived on one sheet with scribbled notes (annotations). Gateway API gives each role its own object, owned by the right team, with a status that says exactly why a route isn't working.

Why a successor to Ingress

Ingress has one object for everything. So everything a particular controller could do beyond host and path (weights, header rules, rewrites, timeouts) leaked into annotations - a different dialect per controller. And one team's Ingress could break a shared host for everyone. Gateway API is the newer, official replacement: the same job, split across objects owned by different people.

What you need to know already: Ingress, the controller, IngressClass, annotations and the retired ingress-nginx (16.19), LoadBalancer and MetalLB (16.6, 16.8), Services and EndpointSlices (16.1), namespaces and labels (15.26), HTTP headers (9.21).

Three objects, three owners

GatewayClass   cluster-scoped. "This kind of gateway, run by that controller."
               Owned by whoever installs the implementation (platform team).
Gateway        a namespace's actual entry point: listeners (port, protocol,
               hostname, TLS) and which namespaces may attach routes.
               Owned by the platform / cluster operators.
HTTPRoute      routing rules for one app: hostnames, matches, filters,
               weighted backends. Owned by the app team, in its own namespace.

"Cluster-scoped" = not inside any namespace (like nodes). A listener is one port + protocol the Gateway accepts traffic on. The GatewayClass plays the role IngressClass played; the Gateway is the proxy itself; the HTTPRoute is the rules part of an Ingress.

In Ingress every one of those concerns was the same object - and an annotation. Here each has its own API with validation, and a status that tells you exactly why it is not working.

# once NGINX Gateway Fabric is installed (the Gateway mission)
k get gatewayclass
NAME    CONTROLLER                                   ACCEPTED   AGE
nginx   gateway.nginx.org/nginx-gateway-controller   True       107s

CONTROLLER = which implementation handles the class; ACCEPTED True = that controller has taken it on.

Installing it

These kinds are not built into Kubernetes. They come as CRDs (custom resource definitions: files that add new object kinds to the API server, like MetalLB's IPAddressPool in 16.6). So you install two things:

  1. the Gateway API CRDs (the "standard channel" release), then
  2. an implementation: a controller plus the proxy it runs. Here that is NGINX Gateway Fabric; others are Envoy Gateway, Istio, Cilium, Traefik, kgateway, and cloud providers' own.

Without the CRDs, kubectl does not know the kind: error: resource mapping not found for name: "web" ... no matches for kind "Gateway" in version "gateway.networking.k8s.io/v1" ensure CRDs are installed first.

A Gateway

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: edge
  namespace: gw-lab
spec:
  gatewayClassName: nginx
  listeners:
  - name: http
    port: 80
    protocol: HTTP
    allowedRoutes:
      namespaces:
        from: Same          # Same (default) | All | Selector

One listener, plain HTTP on port 80. allowedRoutes says which namespaces' HTTPRoutes may attach: only this one (Same), any (All), or those whose labels match a selector (Selector).

NGINX Gateway Fabric provisions (creates for you) a proxy per Gateway - a Deployment and a LoadBalancer Service named <gateway>-<class> in the Gateway's namespace - so on this cluster MetalLB gives it an address:

# gw-lab: the Gateway above (the Gateway mission)
k get gateway -n gw-lab
NAME   CLASS   ADDRESS          PROGRAMMED   AGE
edge   nginx   10.64.0.241   True         15s
k get pods,svc -n gw-lab -l gateway.networking.k8s.io/gateway-name=edge
NAME                              READY   STATUS    RESTARTS   AGE
pod/edge-nginx-7s8wm9zp26-5vh5p   1/1     Running   0          13s
NAME                 TYPE           CLUSTER-IP      EXTERNAL-IP      PORT(S)        AGE
service/edge-nginx   LoadBalancer   10.96.204.113   10.64.0.241   80:31746/TCP   14s

ADDRESS = where clients send traffic; PROGRAMMED True = the proxy is configured and running. PROGRAMMED False with reason AddressNotAssigned means the implementation could not get an address - on bare metal, no MetalLB pool.

An HTTPRoute

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: shop
  namespace: gw-lab
spec:
  parentRefs:
  - name: edge                  # attach to this Gateway (optionally sectionName: http)
  hostnames: [shop.lab]
  rules:
  - matches:
    - headers:
      - name: x-canary
        value: "true"
    backendRefs:
    - name: v2
      port: 80
  - backendRefs:                # no matches = PathPrefix /
    - name: v1
      port: 80
      weight: 90
    - name: v2
      port: 80
      weight: 10

The parts:

parentRefs    which Gateway (and optionally which listener, sectionName) to attach to
hostnames     the Host headers this route handles
rules         a list; each has matches (when) and backendRefs (where to send)
backendRefs   Services + ports; weight = share of requests

So: requests with header x-canary: true go to v2; all others are split 90/10 between v1 and v2. That is a canary release: a new version gets a small slice of real traffic first, and you watch it before sending more.

Things Ingress needed annotations for, built in:

The most specific match wins (longer paths, then more header matches).

Status is the debugger

Every route records, per Gateway it tried to attach to, a list of conditions: a type, status True/False, a machine reason and a human message. grep -A20 '^status' shows the 20 lines after status::

# the HTTPRoute above (the Gateway mission)
k get httproute shop -n gw-lab -o yaml | grep -A20 '^status'
status:
  parents:
  - conditions:
    - message: The Route is accepted
      reason: Accepted
      status: "True"
      type: Accepted
    - message: All references are resolved
      reason: ResolvedRefs
      status: "True"
      type: ResolvedRefs
    controllerName: gateway.nginx.org/nginx-gateway-controller
    parentRef:
      name: edge

The two conditions to check: Accepted (the Gateway took the route) and ResolvedRefs (every Service it points at was found and allowed).

The reasons you will meet when one is False:

NotAllowedByListeners        the Gateway's allowedRoutes does not admit your namespace
NoMatchingListenerHostname   your hostnames do not fit any listener's hostname
NoMatchingParent             wrong Gateway name, or its class is not handled
BackendNotFound              the Service does not exist
RefNotPermitted              a Service in another namespace, without permission (below)

Crossing namespaces: ReferenceGrant

An HTTPRoute in gw-lab pointing at a Service in payments is refused (ResolvedRefs False, RefNotPermitted, and the proxy answers 500) until the target namespace opts in with a ReferenceGrant:

apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
  name: allow-gw-lab-routes
  namespace: payments          # lives with the thing being referenced
spec:
  from:
  - group: gateway.networking.k8s.io
    kind: HTTPRoute
    namespace: gw-lab
  to:
  - group: ""
    kind: Service

Read it as: "HTTPRoutes from namespace gw-lab may point at Services in payments". (group: "" is the core API group, where Service lives.)

The owner of the Service decides who may route to it - the security model Ingress never had (an Ingress could only point at Services in its own namespace at all).

Ingress vs Gateway, in one table

Ingress                               Gateway API
one object                            GatewayClass / Gateway / HTTPRoute (+GRPCRoute, TLSRoute...)
controller features in annotations    typed fields (weights, headers, rewrites, redirects)
same-namespace backends only          cross-namespace with ReferenceGrant
status: an address                    per-route, per-listener conditions with reasons
GA, stable, not deprecated            GA (v1) since 2023, the future

(GA = "generally available": the stable, supported version of an API.)

ingress2gateway (a tool from the Kubernetes project) converts Ingress manifests, including common ingress-nginx annotations - the usual first step of a migration off the retired controller.

What you can now do:

Why it helps

Gateway API is where new platforms are going, especially with ingress-nginx retired, and you'll likely help design or migrate one. The role split maps directly onto a platform team's job: you own GatewayClasses and Gateways, app teams own HTTPRoutes in their namespaces, and you control which namespaces may attach.

It also changes debugging: instead of reading controller logs to guess why a route is ignored, you read status.parents[].conditions and get reasons like NotAllowedByListeners or RefNotPermitted. Canary releases with weights, header-based routing and rewrites are typed fields, so reviewing a teammate's routing PR stops being an annotation-dialect puzzle. It is also where new exam and interview questions come from.

FAQ

Is Gateway API built into Kubernetes?

The API is an official Kubernetes project, but its CRDs are not installed with the cluster. You install a Gateway API release (the standard channel) and then an implementation: NGINX Gateway Fabric, Envoy Gateway, Istio, Cilium, Traefik, kgateway, or a cloud provider's own. Without the CRDs, applying a Gateway fails with "no matches for kind Gateway ... ensure CRDs are installed first".

Why is my HTTPRoute not attached to the Gateway?

Read its status. NotAllowedByListeners means the Gateway's allowedRoutes doesn't admit your namespace (default is Same). NoMatchingListenerHostname means your hostnames don't fit any listener's hostname. NoMatchingParent means a wrong Gateway name or section, or the class isn't handled. k get httproute -o yaml and look under status.parents.

What is a ReferenceGrant for?

It lets the owner of a resource allow references to it from another namespace. An HTTPRoute in gw-lab pointing at a Service in payments is refused (ResolvedRefs False, RefNotPermitted) until a ReferenceGrant in payments allows HTTPRoutes from gw-lab to reference Services. The target decides who may route to it, a control Ingress never had.

How do I do a canary release with Gateway API?

Give one rule several backendRefs with weights, for example v1 at 90 and v2 at 10, and shift the weights as confidence grows. You can also add a rule that matches a header such as x-canary: true and sends only that traffic to v2, so testers can hit the new version deliberately. No service mesh or controller-specific annotations needed.

Should we migrate all Ingresses to Gateway API now?

If you run ingress-nginx, you need a plan, because the project gets no more security fixes. Gateway API v1 has been GA since 2023, and ingress2gateway converts Ingress manifests including common ingress-nginx annotations. Migrate host by host, run both side by side with separate addresses, and compare behaviour (regex matching and rewrites are where differences hide).

In an interview Mid

What problems does Gateway API solve compared with Ingress?

Ingress is one object for everything, so anything beyond host and path (weights, header matches, rewrites, timeouts) leaked into controller-specific annotations, and one team's Ingress could break a shared host. Gateway API splits the job across objects with different owners:

Two more wins: status conditions say exactly why something is not working (Accepted, ResolvedRefs, with reasons like NotAllowedByListeners), and a route to another namespace's Service needs that namespace's ReferenceGrant - the Service owner decides.

It comes as CRDs plus an implementation; with ingress-nginx retired, it is the default for new platforms.

Also asked: Name the main Gateway API resources and who typically owns each. · How do you do a weighted canary release with an HTTPRoute? · What is a ReferenceGrant for?

Practise this lesson in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.