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.

Generated diagram comparing annotation-heavy Ingress with the Gateway API attachment model using GatewayClass, Gateway, and HTTPRoute.
Gateway API separates infrastructure ownership from routing ownership, replacing annotation-heavy ingress behavior with explicit attachment and policy boundaries.

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 provider
  • Gateway: the actual traffic entry point, with listeners, ports, addresses, and TLS
  • HTTPRoute: 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:

  • HTTPRoute itself has been in the Standard channel since v0.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, and HTTPRoute are GA and in the Standard channel
  • GRPCRoute is GA and in the Standard channel since v1.1.0
  • HTTPRoute timeouts are in the Standard channel since v1.2.0
  • ReferenceGrant is in the Standard channel and is the required safety mechanism for cross-namespace references
  • TLSRoute is GA in the Standard channel since v1.5.0
  • TCPRoute is 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 HTTPRoute feature
  • 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 GatewayClass and Gateway
  • 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

Comments

Popular posts from this blog

Bootstrapping Kubernetes Clusters with Terraform and Argo CD: A Durable Two-Layer Approach

Argo CD Auto-Sync and Health Checks: An Operator's Guide to Safe GitOps Reconciliation

Kubernetes Multi-Tenancy with Namespaces and Network Policies: A Practical Guide for GitOps Teams