Gateway API vs Ingress: Why Modern Kubernetes Traffic Management Uses Attachment, Not Annotations
Gateway API vs Ingress: Why Modern Kubernetes Traffic Management Uses Attachment, Not Annotations
Ingress is still valid, but it stopped evolving. Gateway API gives Kubernetes teams a cleaner resource model for shared edge infrastructure, richer routing, and safer multi-namespace ownership.
TL;DR
Gateway API is the practical successor to Kubernetes Ingress for teams that need more than host and path routing. Instead of collapsing infrastructure, TLS, and application routing into one resource plus controller-specific annotations, it separates concerns across GatewayClass, Gateway, and HTTPRoute. That gives platform teams explicit entry points, application teams structured routing rules, and both sides a safer attachment model for shared gateways. The result is better portability, clearer ownership, richer HTTP routing, and a migration path that does not require an all-at-once cutover.
Ingress Solved the First Problem, Not the Current One
The Kubernetes docs now say the quiet part out loud: Kubernetes recommends Gateway API instead of Ingress, and the Ingress API is frozen. In practice, that means Ingress still works and is not going away, but it is no longer where new traffic-management capability is being standardized.
That matters because most real-world platform problems were never just "route /foo to Service A and /bar to Service B." Teams need:
- Shared edge infrastructure across namespaces and teams
- Clear ownership boundaries between platform and application teams
- Rich routing without vendor-specific annotations
- Safer cross-namespace references
- Consistent status and conformance signals across implementations
Ingress can do basic host and path routing well. The model starts to strain when you need explicit listeners, reusable traffic entry points, weighted backends, structured filters, or a clean separation between "who owns the load balancer" and "who owns the app routing rules."
This is what a typical annotation-heavy Ingress tends to look like:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: checkout
namespace: apps
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
ingressClassName: nginx
tls:
- hosts:
- shop.example.com
secretName: shop-example-com
rules:
- host: shop.example.com
http:
paths:
- path: /checkout
pathType: Prefix
backend:
service:
name: checkout-v1
port:
number: 8080
The problem is not that this YAML is invalid. The problem is that most of the interesting behavior lives in controller-specific annotations instead of a portable API.
Gateway API Splits the Model Along Real Ownership Boundaries
The upstream Gateway API project describes itself as the next generation of Kubernetes ingress, load balancing, and service mesh APIs, and its design is explicitly role-oriented. That role split is the first major difference from Ingress.
At minimum, the ingress use case is modeled through three stable resources:
GatewayClass: cluster-scoped infrastructure class owned by the controller or platform providerGateway: the actual traffic entry point, with listeners, ports, addresses, and TLSHTTPRoute: application routing rules that attach to a Gateway
GatewayClass is close to IngressClass conceptually, but it is a stronger contract. It tells you which controller implements the class and can carry a parametersRef for controller-specific configuration. Gateway then becomes the explicit declaration of entry points that were implicit in Ingress. HTTPRoute is where application teams describe matching and forwarding behavior.
Here is the same shape of traffic expressed with Gateway API instead of a single Ingress:
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: internet
spec:
controllerName: example.net/gateway-controller
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: edge
namespace: infra
spec:
gatewayClassName: internet
listeners:
- name: http
protocol: HTTP
port: 80
hostname: shop.example.com
- name: https
protocol: HTTPS
port: 443
hostname: shop.example.com
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: shop-example-com
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
shared-gateway-access: "true"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: checkout
namespace: apps
labels:
shared-gateway-access: "true"
spec:
parentRefs:
- name: edge
namespace: infra
sectionName: https
hostnames:
- shop.example.com
rules:
- matches:
- path:
type: PathPrefix
value: /checkout
backendRefs:
- name: checkout-v1
port: 8080
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: redirect-http-to-https
namespace: apps
labels:
shared-gateway-access: "true"
spec:
parentRefs:
- name: edge
namespace: infra
sectionName: http
hostnames:
- shop.example.com
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
port: 443
That extra verbosity is the point. You can now reason about who owns the edge, who owns TLS termination, and who owns the route rules without smuggling everything through one resource.
The Attachment Model Is the Real Upgrade
The most important Gateway API idea is not just "better HTTP routing." It is the attachment model.
With Ingress, the relationship between infrastructure and application routing is mostly implicit: the Ingress chooses a class, the controller merges rules, and annotations decide much of the behavior. With Gateway API, attachment is explicit and bidirectional:
- A Route requests attachment through
parentRefs - A Gateway listener decides which Route kinds and namespaces may attach through
allowedRoutes - A Route can bind to a specific listener using
sectionName - Cross-namespace references to backends or Secrets require a
ReferenceGrant
This matters in multi-team clusters because "can reference" and "is allowed to reference" are separate permissions.
The following example shows a shared Gateway in infra, an application route in apps, and a backend Service in shared-services. The Route can only use that cross-namespace backend because the owner of shared-services explicitly grants it:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: shared-edge
namespace: infra
spec:
gatewayClassName: internet
listeners:
- name: web
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
gateway-access: shared-edge
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: store
namespace: apps
labels:
gateway-access: shared-edge
spec:
parentRefs:
- name: shared-edge
namespace: infra
sectionName: web
hostnames:
- store.example.com
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: store-frontend
port: 8080
- name: shared-session-api
namespace: shared-services
port: 8080
weight: 1
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-apps-to-shared-session-api
namespace: shared-services
spec:
from:
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: apps
to:
- group: ""
kind: Service
This is a cleaner security story than broad controller permissions plus annotation conventions. The route owner, gateway owner, and backend owner each participate in the final attachment.
HTTPRoute Is Structured Enough to Replace a Lot of Annotations
The upstream migration guide calls out the reason many teams want Gateway API: it covers the basic Ingress feature set and also standardizes several capabilities that often used to be controller-specific annotations. HTTPRoute includes structured matches, filters, weighted backends, and timeouts.
From the official docs:
HTTPRouteitself has been in the Standard channel sincev0.5.0- HTTPRoute timeouts have been in the Standard channel since
v1.2.0 - Core filters must be supported; extended filters depend on implementation
That makes this kind of routing possible without hiding intent in string annotations:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: checkout-advanced
namespace: apps
spec:
parentRefs:
- name: edge
namespace: infra
sectionName: https
hostnames:
- shop.example.com
rules:
- matches:
- path:
type: PathPrefix
value: /api/checkout
headers:
- type: Exact
name: x-release-channel
value: beta
method: POST
filters:
- type: RequestHeaderModifier
requestHeaderModifier:
add:
- name: x-routed-by
value: gateway-api
timeouts:
request: 10s
backendRequest: 2s
backendRefs:
- name: checkout-v1
port: 8080
weight: 90
- name: checkout-v2
port: 8080
weight: 10
Several things are better here than the Ingress equivalent:
- Path, method, and header matching are typed fields instead of controller conventions
- Traffic splitting is a first-class API field on
backendRefs - Timeout configuration is structured instead of annotation-specific
- Status can report whether the Route was accepted by the target Gateway
One important caveat: Gateway API is portable, but not every implementation supports every extended feature. Upstream publishes per-version conformance reports for exactly this reason. Before you rely on timeouts, special filters, or non-HTTP route types, check your controller's conformance level instead of assuming feature parity.
Current Gateway API Maturity: What Is Stable and What Still Needs Verification
If you evaluate Gateway API as one giant feature bucket, you will either underuse it or overestimate it. The upstream versioning model is feature-based, not monolithic.
As of the current official docs:
GatewayClass,Gateway, andHTTPRouteare GA and in the Standard channelGRPCRouteis GA and in the Standard channel sincev1.1.0- HTTPRoute timeouts are in the Standard channel since
v1.2.0 ReferenceGrantis in the Standard channel and is the required safety mechanism for cross-namespace referencesTLSRouteis GA in the Standard channel sincev1.5.0TCPRouteis still documented as Experimental- The general policy attachment pattern is still marked Experimental
That last point is where teams get sloppy. "Gateway API supports policy attachment" is true at the design-pattern level, but it does not mean every policy CRD you encounter is portable or mature. Treat policy resources such as backend-traffic or backend-TLS policies as controller-specific until your implementation documents them clearly.
A practical rule:
- Design around Standard-channel resources first
- Use Experimental route types only when your controller and upgrade process can tolerate change
- Treat policy CRDs like any other extension: verify controller support, status behavior, and upgrade semantics before adopting them in production
Migration Guidance That Minimizes Surprises
The official Gateway API migration guide is useful, but the safest production migration is more operational than syntactic.
1. Inventory your annotations before you convert anything
Split your current Ingress configuration into three buckets:
- Portable today: hostnames, path rules, TLS termination, redirects, header manipulation, weighted backends
- Likely portable but controller-dependent: timeouts, special filters, gRPC-specific routing
- Still implementation-specific: custom auth, vendor rate limits, health-check knobs, bespoke annotations
If your existing behavior depends heavily on annotation-specific extensions, do not promise a one-file translation.
2. Install standard Gateway API CRDs and verify the controller you actually run
The upstream getting-started guide currently points to the standard install bundle:
kubectl apply --server-side -f \
https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.1/standard-install.yaml
kubectl get gatewayclass
Then verify your implementation against the official conformance matrix and your vendor docs.
3. Model the Gateway first, then attach one hostname or path group at a time
Ingress hides entry points. Gateway API makes them explicit. Start by creating the Gateway with the listeners, TLS config, and namespace attachment rules you want long term. Then move application rules into HTTPRoute resources incrementally instead of translating every Ingress into a single giant route object.
4. Use side-by-side migration, not a flag day
Gateway API is a good fit for parallel migration because you can stand up new listeners and Routes while the old Ingress continues serving traffic. If your controller supports weighted backends or you front everything with external DNS or load-balancer controls, you can shift hostname by hostname instead of cutting over the whole edge at once.
5. Leave extensions and policies for last
The upstream migration guide explicitly warns that some advanced features still depend on Gateway-specific extension points. That is where migrations get risky. First port the portable routing behavior. Only after that should you decide whether a given annotation maps to:
- A standard
HTTPRoutefeature - An implementation-specific filter or extension reference
- A policy CRD
- A behavior you should drop because it is no longer worth carrying forward
6. Use conversion tools as scaffolding, not truth
The official guide mentions ingress2gateway. That is useful for initial translation, but generated output still needs a human review for ownership boundaries, listener layout, cross-namespace references, and controller support.
Why This Model Scales Better for Platform Teams
Ingress was optimized for "get HTTP into the cluster." Gateway API is optimized for "let multiple teams share traffic infrastructure without pretending they all own the same thing."
That is why the resource split is worth the extra YAML:
- Platform teams can own
GatewayClassandGateway - App teams can own
HTTPRoute - Shared gateways can be guarded with explicit attachment rules
- Cross-namespace references require consent from the referenced namespace
- Feature maturity is documented per resource and per capability, not buried in controller release notes
If your cluster is simple and one team owns everything, Ingress can still be enough. If your environment has shared edge infrastructure, multiple namespaces, or a need to stop encoding routing features as annotations, Gateway API is the better control surface.
Frequently Asked Questions
Q: Should every existing Ingress be migrated immediately? A: No. Ingress is stable and supported; it is just frozen. Migrate when you need cleaner ownership, richer routing, better portability, or multi-namespace attachment controls that Ingress does not model well.
Q: Do I always need GatewayClass, Gateway, and HTTPRoute? A: For the ingress use case, yes, that is the core model. GatewayClass selects the controller, Gateway defines the entry points and listener policy, and HTTPRoute carries the application routing rules.
Q: Is Gateway API only for HTTP traffic? A: No. The project covers multiple protocols. Today, HTTPRoute, GRPCRoute, and TLSRoute are in the Standard channel, while TCPRoute is still documented as Experimental. That means your migration plan should be protocol-aware, not just HTTP-aware.
Q: What is the most common migration mistake? A: Treating Gateway API as a one-to-one YAML rewrite of Ingress. The better approach is to redesign the ownership model first, especially listeners, namespace attachment, and which teams are allowed to reference which backends.
Q: When should I use policy attachment? A: Only when you have confirmed that your controller supports the specific policy CRD you want and you understand its precedence and status behavior. The upstream policy attachment pattern is still experimental, so it should not be your default migration assumption.
Resources
- Kubernetes Ingress docs
- Gateway API introduction
- Gateway API getting started
- Migrating from Ingress
- GatewayClass reference
- Gateway reference
- HTTPRoute reference
- Cross-namespace routing guide
- ReferenceGrant reference
- Gateway API versioning and release channels
- Policy attachment reference
- Gateway API conformance reports
Comments
Post a Comment