Ingress and Gateway API
The idea in one sentence
Section titled “The idea in one sentence”Ingress describes Layer-7 HTTP(S) routing rules for getting external traffic to internal Services, but those rules do nothing until an Ingress controller is actually running to implement them.
Ingress: routing rules, not a router
Section titled “Ingress: routing rules, not a router”A LoadBalancer Service works well for one application, but provisioning a separate cloud load balancer per app gets expensive and does not let you route by hostname or URL path. Ingress solves this at Layer 7: one entry point that inspects the HTTP request and routes based on host and path.
apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: shop annotations: nginx.ingress.kubernetes.io/rewrite-target: /spec: ingressClassName: nginx rules: - host: shop.example.com http: paths: - path: /api pathType: Prefix backend: service: name: api-svc port: number: 80 - path: / pathType: Prefix backend: service: name: frontend-svc port: number: 80That manifest expresses two things at once: host-based routing (only requests for shop.example.com match this rule at all) and path-based routing (/api goes to one Service, everything else goes to another).
An Ingress object alone does nothing
Section titled “An Ingress object alone does nothing”This is the part that trips people up: creating an Ingress object does not, by itself, cause any traffic to be routed anywhere. Ingress is just a declaration of intent — it requires an Ingress controller, a separate piece of software actually running in the cluster (an nginx-based controller, a cloud provider’s controller, or others), watching Ingress objects and programming a real proxy or load balancer to match. No controller running means the Ingress resource just sits there, inert.
# An Ingress with no controller watching it accomplishes nothingkubectl get ingress shopkubectl get pods -n ingress-nginx # this is what actually does the routingWhen more than one controller is installed in a cluster, ingressClassName (backed by an IngressClass object) tells Kubernetes which controller should implement a given Ingress:
apiVersion: networking.k8s.io/v1kind: IngressClassmetadata: name: nginxspec: controller: k8s.io/ingress-nginxGateway API: a more expressive successor
Section titled “Gateway API: a more expressive successor”Gateway API is a newer, role-oriented model for the same problem, built from three resources instead of one:
- GatewayClass — the implementation, analogous to
IngressClass(provided by a controller vendor). - Gateway — a listener: an actual point of entry (protocol, port, hostname), typically owned by the platform/infrastructure team.
- HTTPRoute (and GRPCRoute) — the routing rules attached to a Gateway, typically owned by application teams.
apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata: name: shop-gatewayspec: gatewayClassName: example-gateway-class listeners: - name: http protocol: HTTP port: 80---apiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutemetadata: name: shop-routespec: parentRefs: - name: shop-gateway hostnames: - "shop.example.com" rules: - matches: - path: type: PathPrefix value: /api backendRefs: - name: api-svc port: 80That split matters in practice: a platform team can own and lock down the Gateway (which ports and TLS certs are exposed) while individual application teams safely own their own HTTPRoute objects without needing access to the shared listener at all. Gateway, GatewayClass, and HTTPRoute (along with GRPCRoute) have reached General Availability at gateway.networking.k8s.io/v1, and this is now the Kubernetes project’s own recommendation, not just an ecosystem trend: the Ingress API is frozen — generally available and not going away, but receiving no further features — and the docs explicitly say to use Gateway API instead of Ingress for anything new.
That recommendation has teeth. ingress-nginx, historically the most widely deployed Ingress controller, announced its own retirement in November 2025: best-effort maintenance only until March 2026, with no further releases, bugfixes, or security updates after that. Existing installations keep working and the Helm charts/images stay published, but a new cluster today should pick a Gateway API implementation rather than standing up ingress-nginx.
Gateway API, proven end to end: GatewayClass, weighted HTTPRoute, filters
Section titled “Gateway API, proven end to end: GatewayClass, weighted HTTPRoute, filters”A real Gateway API controller — NGINX Gateway Fabric — was installed on this module’s own kind cluster via its official Helm chart (helm install ngf oci://ghcr.io/nginx/charts/nginx-gateway-fabric --create-namespace -n nginx-gateway --set nginx.service.type=NodePort, per the NGINX Gateway Fabric install docs), on top of the Gateway API standard channel v1.6.2 CRDs. A real GatewayClass appeared immediately:
$ kubectl get gatewayclassNAME CONTROLLER ACCEPTED AGEnginx gateway.nginx.org/nginx-gateway-controller True 16sapiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata: name: demo-gatewayspec: gatewayClassName: nginx listeners: - name: http protocol: HTTP port: 80$ kubectl get gateway demo-gatewayNAME CLASS ADDRESS PROGRAMMED AGEdemo-gateway nginx 10.96.71.123 True 18sPROGRAMMED: True and a real ADDRESS — NGF stood up an actual nginx data-plane Deployment and Service backing this Gateway. Wiring in weighted backendRefs and a header filter:
apiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutemetadata: name: shop-routespec: parentRefs: - name: demo-gateway hostnames: - 'shop.example.com' rules: - matches: - path: type: PathPrefix value: /api filters: - type: RequestHeaderModifier requestHeaderModifier: add: - name: X-Routed-Via value: gateway-api backendRefs: - name: api-v1 port: 80 weight: 80 - name: api-v2 port: 80 weight: 20Real weighted split, from inside the cluster, curling the Gateway’s own Service IP directly with the routed Host header — 50 requests:
$ for i in $(seq 1 50); do wget -q -T 3 -O- --header="Host: shop.example.com" http://10.96.71.123/api; echo; done | sort | uniq -c 44 v1 6 v244/6 out of 50 (88%/12%) is close to the configured 80/20, not exact — a real, honest demonstration that weight sets a ratio the implementation samples against, not a guaranteed exact split at low request volume; a much larger sample converges closer to 80/20. This is the real distinction between “weights” and “percentages” this lesson’s Trade-offs table below is built on.
RequestRedirect filter, proven
Section titled “RequestRedirect filter, proven”# excerpt: second rule of k8s/ingress-and-gateway-api/httproute-weighted.yaml above - matches: - path: type: PathPrefix value: /old filters: - type: RequestRedirect requestRedirect: path: type: ReplacePrefixMatch replacePrefixMatch: /new statusCode: 301$ wget -T 3 -S --header="Host: shop.example.com" http://10.96.71.123/old HTTP/1.1 301 Moved Permanently Server: nginx Location: http://shop.example.com:80/newA real 301 with a real Location header rewritten exactly per ReplacePrefixMatch: /new — proof the filter executes as documented, no backend even involved for this rule.
RequestRedirect and backendRefs together: a real, immediate rejection
Section titled “RequestRedirect and backendRefs together: a real, immediate rejection”The first attempt at the redirect rule above kept a leftover backendRefs entry (copy-pasted from the weighted rule) — the API server rejected it outright, not silently ignoring the conflict:
# k8s/ingress-and-gateway-api/httproute-bad-redirect.yaml — @expect-error RequestRedirect filter combined with backendRefs in the same rule; real API server rejectionapiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutemetadata: name: shop-route-badspec: parentRefs: - name: demo-gateway rules: - matches: - path: type: PathPrefix value: /old filters: - type: RequestRedirect requestRedirect: path: type: ReplacePrefixMatch replacePrefixMatch: /new statusCode: 301 backendRefs: - name: api-v1 port: 80$ kubectl apply -f httproute-bad-redirect.yamlThe HTTPRoute "shop-route-bad" is invalid: spec.rules[1]: Invalid value: RequestRedirect filter must not be used together with backendRefsA real, immediate Invalid rejection — Gateway API’s own validation (a ValidatingAdmissionPolicy, per the CRDs installed above) catches this shape mismatch at apply time, which is exactly the kind of AI-generated-code mistake worth watching for: a redirect rule doesn’t route to a backend at all, so a backendRefs copied over from a sibling rule is nonsensical, and the API server says so directly rather than silently keeping (or silently dropping) one of the two.
ReferenceGrant: real, cross-namespace
Section titled “ReferenceGrant: real, cross-namespace”apiVersion: gateway.networking.k8s.io/v1beta1kind: ReferenceGrantmetadata: name: allow-gateway-to-other-ns namespace: backend-nsspec: from: - group: gateway.networking.k8s.io kind: HTTPRoute namespace: default to: - group: '' kind: Service$ kubectl apply -f referencegrant.yamlreferencegrant.gateway.networking.k8s.io/allow-gateway-to-other-ns createdThis real, applied object is what would let an HTTPRoute in default reference a Service that lives in backend-ns — without it, Gateway API rejects the cross-namespace backendRef outright. Unlike Ingress (which lets a backend reference cross namespaces implicitly through whatever the controller allows), Gateway API requires this explicit opt-in on the target namespace’s side — a deliberate security tightening.
GRPCRoute, real dry-run
Section titled “GRPCRoute, real dry-run”$ kubectl apply --dry-run=server -f - <<'EOF'apiVersion: gateway.networking.k8s.io/v1kind: GRPCRoutemetadata: name: demo-grpcspec: parentRefs: - name: demo-gateway rules: - matches: - method: service: demo.v1.DemoService method: Ping backendRefs: - name: api-v1 port: 80EOFgrpcroute.gateway.networking.k8s.io/demo-grpc created (server dry run)GRPCRoute is real and GA on this cluster’s Gateway API v1.6.2 install — the same Gateway/GatewayClass backs both HTTP and gRPC routing, matched on service/method instead of path.
flowchart LR ext["External client"] --> ctrl["Ingress controller\n(e.g. nginx)"] ctrl -->|"host/path rules"| ing["Ingress object"] ing --> svcA["Service: api-svc"] --> podA["Pods"] ing --> svcB["Service: frontend-svc"] --> podB["Pods"]
Reading AI-generated code
Section titled “Reading AI-generated code”When reviewing generated Ingress/Gateway API manifests, check for:
networking.k8s.io/v1beta1Ingress usingserviceName/servicePort— the pre-1.19 shape. Current Ingress isnetworking.k8s.io/v1withbackend.service.name/backend.service.port.number; the old API version and field shape were removed long ago and a real cluster rejects them.- A
RequestRedirectfilter with a leftoverbackendRefsin the same rule — proven above, real, immediate rejection (RequestRedirect filter must not be used together with backendRefs). backendRefsweights presented as if they were percentages guaranteed on every request — proven above, a real 50-request sample landed at 88/12 against a configured 80/20, not exact; weights are a ratio the implementation samples against.- A cross-namespace
backendRefwith no accompanyingReferenceGrantin the target namespace, assumed to “just work” the way a same-namespace Ingress backend would — Gateway API requires the explicit opt-in proven above. - A new cluster’s Ingress setup reaching for ingress-nginx by habit, without acknowledging its March 2026 retirement (already covered above).
🐛 Spot the bug
apiVersion: networking.k8s.io/v1beta1kind: Ingressmetadata:name: shop-legacyspec:rules: - host: shop.example.com http: paths: - path: /api backend: serviceName: api-svc servicePort: 80Show the answer
This manifest uses apiVersion: networking.k8s.io/v1beta1 with backend.serviceName/backend.servicePort — the Ingress shape from before Kubernetes 1.19. That API version was removed years ago; applying this to any current cluster (1.22+) gets a real no matches for kind "Ingress" in version "networking.k8s.io/v1beta1" error, since the API server no longer serves that group/version at all — this isn’t a deprecation warning, it’s a hard removal. The fix: networking.k8s.io/v1, with pathType required and the backend written as backend.service.name/backend.service.port.number (or, per this lesson’s own recommendation given the Ingress API’s frozen status and ingress-nginx’s retirement, migrate to Gateway API’s HTTPRoute instead of updating in place).
Mental Model
Section titled “Mental Model”Gateway API deliberately splits what Ingress bundled into one object: who owns the shared listener (Gateway, platform team) is now a different object, with different RBAC, from who owns routing rules (HTTPRoute, app teams) — and cross-namespace references are opt-in via ReferenceGrant (proven above) rather than implicit, a deliberate security tightening versus Ingress’s looser model. weight on a backendRef is a sampling ratio the data plane honors probabilistically, not a hard percentage guarantee per request — proven above with real, imperfect-but-converging numbers.
Trade-offs
Section titled “Trade-offs”| Choice | Benefit | Cost |
|---|---|---|
| Ingress | One object, simple, widely supported today | Frozen API (no new features), most popular controller retiring Mar 2026 |
| Gateway API | Expressive (weights, header/path matches, filters, GRPCRoute, proven above), role-split ownership | More objects to reason about; cross-namespace needs explicit ReferenceGrant, proven above |
backendRefs weights | Real traffic splitting for canaries without a service mesh, proven above | Only converges to the configured ratio at higher request volume — proven above with a real 88/12 sample against a configured 80/20 |
RequestRedirect filter | Real 301 redirects with path rewriting, proven above, no backend needed | Cannot be combined with backendRefs in the same rule — real, immediate rejection, proven above |
Common Misconceptions
Section titled “Common Misconceptions”- “Ingress and Gateway API are interchangeable, pick whichever.” The Kubernetes project itself now recommends Gateway API for anything new (Ingress frozen, ingress-nginx retiring) — already cited in this lesson.
- “An HTTPRoute in namespace A can reference a Service in namespace B just like same-namespace.” Proven above — it requires an explicit
ReferenceGrantin the target namespace, or the reference is rejected. - “
backendRefsweight values are exact percentages.” Proven above with a real sample — they’re relative weights sampled probabilistically; a small request count can land noticeably off the configured ratio. - “A
RequestRedirectfilter can point at a backend too, for a fallback.” Proven above — the API server rejectsRequestRedirectcombined withbackendRefsin the same rule outright.