Skip to content

Ingress and Gateway API

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.

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/v1
kind: Ingress
metadata:
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: 80

That 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).

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.

Terminal window
# An Ingress with no controller watching it accomplishes nothing
kubectl get ingress shop
kubectl get pods -n ingress-nginx # this is what actually does the routing

When 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/v1
kind: IngressClass
metadata:
name: nginx
spec:
controller: k8s.io/ingress-nginx

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/v1
kind: Gateway
metadata:
name: shop-gateway
spec:
gatewayClassName: example-gateway-class
listeners:
- name: http
protocol: HTTP
port: 80
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop-route
spec:
parentRefs:
- name: shop-gateway
hostnames:
- "shop.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /api
backendRefs:
- name: api-svc
port: 80

That 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 gatewayclass
NAME CONTROLLER ACCEPTED AGE
nginx gateway.nginx.org/nginx-gateway-controller True 16s
k8s/ingress-and-gateway-api/gateway.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: demo-gateway
spec:
gatewayClassName: nginx
listeners:
- name: http
protocol: HTTP
port: 80
$ kubectl get gateway demo-gateway
NAME CLASS ADDRESS PROGRAMMED AGE
demo-gateway nginx 10.96.71.123 True 18s

PROGRAMMED: 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:

k8s/ingress-and-gateway-api/httproute-weighted.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop-route
spec:
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: 20

Real 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 v2

44/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.

# 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/new

A 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 rejection
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop-route-bad
spec:
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.yaml
The HTTPRoute "shop-route-bad" is invalid: spec.rules[1]: Invalid value: RequestRedirect filter must not be used together with backendRefs

A 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.

k8s/ingress-and-gateway-api/referencegrant.yaml
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-gateway-to-other-ns
namespace: backend-ns
spec:
from:
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: default
to:
- group: ''
kind: Service
$ kubectl apply -f referencegrant.yaml
referencegrant.gateway.networking.k8s.io/allow-gateway-to-other-ns created

This 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.

$ kubectl apply --dry-run=server -f - <<'EOF'
apiVersion: gateway.networking.k8s.io/v1
kind: GRPCRoute
metadata:
name: demo-grpc
spec:
parentRefs:
- name: demo-gateway
rules:
- matches:
- method:
service: demo.v1.DemoService
method: Ping
backendRefs:
- name: api-v1
port: 80
EOF
grpcroute.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"]
External traffic through an Ingress controller to internal Services and Pods

When reviewing generated Ingress/Gateway API manifests, check for:

  • networking.k8s.io/v1beta1 Ingress using serviceName/servicePort — the pre-1.19 shape. Current Ingress is networking.k8s.io/v1 with backend.service.name/backend.service.port.number; the old API version and field shape were removed long ago and a real cluster rejects them.
  • A RequestRedirect filter with a leftover backendRefs in the same rule — proven above, real, immediate rejection (RequestRedirect filter must not be used together with backendRefs).
  • backendRefs weights 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 backendRef with no accompanying ReferenceGrant in 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

k8s/ingress-and-gateway-api/legacy-ingress.yaml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: shop-legacy
spec:
rules:
- host: shop.example.com
http:
paths:
- path: /api
backend:
serviceName: api-svc
servicePort: 80
Show 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).

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.

ChoiceBenefitCost
IngressOne object, simple, widely supported todayFrozen API (no new features), most popular controller retiring Mar 2026
Gateway APIExpressive (weights, header/path matches, filters, GRPCRoute, proven above), role-split ownershipMore objects to reason about; cross-namespace needs explicit ReferenceGrant, proven above
backendRefs weightsReal traffic splitting for canaries without a service mesh, proven aboveOnly converges to the configured ratio at higher request volume — proven above with a real 88/12 sample against a configured 80/20
RequestRedirect filterReal 301 redirects with path rewriting, proven above, no backend neededCannot be combined with backendRefs in the same rule — real, immediate rejection, proven above
  • “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 ReferenceGrant in the target namespace, or the reference is rejected.
  • “backendRefs weight 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 RequestRedirect filter can point at a backend too, for a fallback.” Proven above — the API server rejects RequestRedirect combined with backendRefs in the same rule outright.
Why does creating an Ingress object alone not route any traffic
What is the difference between host-based and path-based routing in an Ingress rule
In Gateway API, what does an HTTPRoute do
How do GatewayClass, Gateway, and HTTPRoute relate to each other
This is a real API server rejection. What caused it?
The HTTPRoute "shop-route-bad" is invalid: spec.rules[1]: Invalid value: RequestRedirect filter must not be used together with backendRefs
This is a real result from an HTTPRoute with backendRefs weight: 80 (api-v1) and weight: 20 (api-v2). What does 44/6 (88%/12%) instead of exactly 40/10 (80%/20%) prove?
$ for i in $(seq 1 50); do wget ...; done | sort | uniq -c
     44 v1
      6 v2