A Practical Guide to Kubernetes Gateway API

In the early days of Kubernetes, Ingress was the standard mechanism for routing external traffic into a cluster. However, as microservice architectures matured, engineering teams scaled, and traffic governance requirements grew more complex, the inherent limitations of Ingress became increasingly obvious.

To address these structural problems at their root, the Kubernetes Special Interest Group for Networking (SIG-Network) introduced the Gateway API. Far from being an incremental patch on Ingress, the Gateway API fundamentally decouples traffic configuration across infrastructure, platform, and application domains at the schema level.

Designed for engineers and SREs looking to understand the Gateway API, this article starts from its core architecture, dissects the notorious “Annotation hell” of traditional Ingress, and walks through a practical refactoring example, a migration cheat sheet, and a controller selection guide.


What Is the Gateway API: From Subdivided Apartments to Modern Commercial Buildings

The fastest way to grasp the Gateway API is to examine how it redraws the boundaries of networking resource management.

Imagine a Kubernetes cluster as a physical building:

Traditional Ingress: An Aging Subdivided Apartment

Who manages the front door, who holds the keys, and what notices hang at the entrance are all crammed into a single lease agreement (a single Ingress resource). If a tenant wants to replace a lock (configure CORS) or update their doorplate (URL rewrite), they have to scribble in the margins of that contract (stuffing annotations in). A single errant scribble can easily break the entire building’s access control.

Gateway API: A Modern Commercial Office Building

This model of decoupled roles and separated responsibilities is the core design philosophy of the Gateway API.

The Three Core Roles and CRD Architecture

The Gateway API leverages Kubernetes Custom Resource Definitions (CRDs) to break down traffic governance into a clean, three-tiered structure:

 [1] Infrastructure Provider
 +-----------------------------------------------+
 | GatewayClass: envoy-gateway, cilium           |
 +-----------------------------------------------+
                         | Defines underlying
                         | implementation spec
                         v
 [2] Cluster Operator / SRE
 +-----------------------------------------------+
 | Gateway: Port 80/443, TLS cert, external IP   |
 +-----------------------------------------------+
            |                        |
            | Allows HTTPRoute       |
            | attachment             |
            v                        v
 [3] Application Developer
 +----------------------+ +----------------------+
 | HTTPRoute: orders    | | HTTPRoute: users     |
 | /orders -> order-svc | | /users -> user-svc   |
 +----------------------+ +----------------------+
            |                        |
            v                        v
 +----------------------+ +----------------------+
 | Service: order-svc   | | Service: user-svc    |
 +----------------------+ +----------------------+

These three core resources correspond to explicit role boundaries:

  1. GatewayClass: Managed by the Infrastructure Provider or cloud vendor. It declares the underlying technology powering the gateway (such as Envoy, Cilium, or a cloud provider’s ALB), conceptually analogous to StorageClass in Kubernetes storage.
  2. Gateway: Maintained by the Cluster Operator or platform engineering team. It represents the physical or virtual entry point receiving external traffic (typically mapping to a load balancer instance or IP address). It defines the listening ports (80 and 443), protocols (HTTP, HTTPS, TCP), and TLS certificate attachments.
  3. HTTPRoute: Authored independently by Application Developers (microservice teams). It defines concrete traffic routing logic, such as “route requests with path /api/v1/orders to order-svc.” Developers do not need to know the underlying load balancer’s physical IP; they simply declare which Gateway to attach to via spec.parentRefs.

Beyond the core HTTPRoute used in production, the specification also defines dedicated route types for other protocols—such as the generally available (GA) GRPCRoute, alongside Layer 4 transport routes like TCPRoute, UDPRoute, and TLSRoute—for teams to adopt as needed.


Why Ingress Falls Short: Three Structural Pain Points

Ingress was originally introduced in 2015 with Kubernetes 1.1. During the nascent stages of microservices, it offered a significantly more flexible and cost-effective Layer 7 (HTTP) routing mechanism than NodePort and LoadBalancer.

Yet in the decade since, the governance complexity of modern production environments has far outpaced what the original architecture anticipated, exposing three glaring structural pain points.

Pain Point 1: The Notorious “Annotation Hell”

In the official Ingress specification, resource attributes are Spartan: essentially just rules (domains and paths) and backend (target Services). Yet real-world production workloads inevitably demand advanced traffic capabilities:

Because the Ingress schema lacked native support for these capabilities, Ingress controller implementations (such as NGINX, Traefik, HAProxy, and Emissary) were forced to invent proprietary metadata.annotations to bridge the gap. In day-to-day operations, this introduced three critical problems:

  1. No type safety; typos fail silently: Annotations are unstructured string key-value pairs. If you accidentally mistype proxy-connect-timeout as proxy-connet-timeout, the Kubernetes API Server accepts the resource without error. The configuration silently fails in production, going unnoticed until backends start timing out under load.
  2. Vendor lock-in and prohibitive migration costs: NGINX relies on regex capture groups in annotations for URL rewrites; Traefik requires proprietary Middleware CRDs; HAProxy uses yet another convention for header manipulation. Once a team decides to migrate from Ingress-NGINX to another solution, hundreds of Ingress manifests across the cluster must be completely rewritten.
  3. Layered black magic and spiraling maintenance overhead: A single Ingress manifest frequently accumulates dozens of lines of untyped annotations lacking any semantic constraints. They cannot be validated via standard unit tests or schema checkers, turning troubleshooting into a guessing game.

Core Comparison: String Annotations vs. Strongly Typed Specifications

Compare URL rewriting and canary splitting—the two declaration philosophies are night and day:

# Traditional Ingress: Relies on non-standard string annotations, lacks type checking, and syntax varies by controller
annotations:
  nginx.ingress.kubernetes.io/rewrite-target: /api/$1
  nginx.ingress.kubernetes.io/canary: "true"
  nginx.ingress.kubernetes.io/canary-weight: "20"
# Gateway API: First-class filters and native weights, strictly validated by the API Server schema
filters:
- type: URLRewrite
  urlRewrite:
    path:
      type: ReplacePrefixMatch
      replacePrefixMatch: /api
backendRefs:
- name: order-service-v1
  port: 8080
  weight: 80
- name: order-service-v2
  port: 8080
  weight: 20

In the Gateway API, regardless of which controller runs under the hood, filters.urlRewrite and backendRefs.weight carry vendor-neutral standardized semantics, strictly validated by the API Server at admission time.

Pain Point 2: Monolithic Resources Conflating Cross-Team Responsibilities

Under the Ingress architecture, a single YAML manifest must accommodate both infrastructure settings (such as TLS certificate Secret references and global hostnames) and application routing logic (such as microservice path mappings).

This design directly fuels organizational friction:

The Gateway API cleanly resolves this through the strict separation of Gateway (owned by SRE) and HTTPRoute (owned by application teams), paired with native Kubernetes RBAC to re-establish clean operational boundaries.

Pain Point 3: Severely Constrained Cross-Namespace Routing

In microservice architectures, different product teams typically operate within isolated namespaces.

By default, an Ingress can only route traffic to Services within its own namespace. Directing traffic from a single entry domain to Services across multiple namespaces often requires proprietary controller annotations or convoluted ExternalName service workarounds.

The Gateway API introduces native support for cross-namespace route binding with fine-grained authorization: a central Gateway controls which namespaces are permitted to attach routes via allowedRoutes.

When cross-namespace references extend to sensitive resources—such as a Gateway referencing a TLS Secret located in another namespace, or a Route forwarding to a backend Service in a different namespace—the target resource owner must explicitly grant permission using a ReferenceGrant, combining architectural flexibility with defense-in-depth security.


Hands-On Refactoring: Migrating from Ingress-NGINX to Gateway API

To ground these architectural concepts in practical code, let’s walk through an e-commerce platform running on shop.example.com and contrast how identical routing requirements are configured across both architectures.

Scenario Setup

Our objective is to expose an external entry point for an online shop with four common requirements:

  1. Cluster-wide HTTPS: Terminate TLS using the shop-tls-secret certificate.
  2. Static Frontend: Forward requests hitting the root path / to the static frontend service frontend-svc:80.
  3. User Service (URL Rewrite): External requests to /api/v1/users must have their prefix stripped and rewritten to /users before reaching user-svc:8080.
  4. Order Service (Canary Split): For requests hitting /api/v1/orders, roll out a new version order-svc-v2:8080, splitting traffic with 80% routed to order-svc-v1 and 20% routed to the canary order-svc-v2.

Before: The Compromises of Ingress-NGINX

In traditional Ingress-NGINX, because the core spec cannot express weighted splits within a single rule, engineers are forced to maintain two separate Ingress manifests:

File 1: Primary Ingress (shop-ingress-main.yaml snippet)

Handles static frontend routing, user service URL rewriting, and the baseline order service:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: shop-ingress-main
  annotations:
    # URL rewriting requires enabling regexes and writing obscure capture groups
    nginx.ingress.kubernetes.io/use-regex: "true"
    nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
  tls:
  - hosts:
    - shop.example.com
    secretName: shop-tls-secret
  rules:
  - host: shop.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: frontend-svc
            port: { number: 80 }
      - path: /api/v1(/|$)(.*)
        pathType: ImplementationSpecific
        backend:
          service:
            name: user-svc
            port: { number: 8080 }
      - path: /api/v1/orders
        pathType: Prefix
        backend:
          service:
            name: order-svc-v1
            port: { number: 8080 }

File 2: Canary Shadow Ingress (shop-ingress-canary.yaml snippet)

To route 20% of traffic to the new version, you must duplicate an almost identical Ingress manifest annotated with canary tags.

When the Ingress-NGINX controller detects two Ingresses pointing to the same host, with one bearing canary: "true", it treats the second manifest as a canary shadow and distributes traffic according to canary-weight:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: shop-ingress-canary
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "20"
spec:
  tls:
  - hosts:
    - shop.example.com
    secretName: shop-tls-secret
  rules:
  - host: shop.example.com
    http:
      paths:
      - path: /api/v1/orders
        pathType: Prefix
        backend:
          service:
            name: order-svc-v2
            port: { number: 8080 }

This workaround carries obvious operational risk: when the primary Ingress’s TLS certificate or hostname changes, the canary shadow is easily missed in the update, causing outages. Moreover, regex capture groups like rewrite-target: /$2 are notoriously brittle and prone to subtle path-matching bugs.

After: Decoupled Refactoring with Gateway API

With the Gateway API, every configuration maps cleanly to the principle of “whoever manages it declares it.”

NOTE

The Gateway API is not an in-tree Kubernetes resource; it is distributed independently as CRDs. This is an intentional architectural decision: because Ingress was baked into Kubernetes core, its evolution was tethered to Kubernetes release cycles. Distributing the Gateway API as CRDs enables SIG-Network to iterate independently. You must install the standard CRDs before use (either via official releases or bundled automatically with your controller).

Step 1: Platform / SRE Team Defines the Gateway (gateway.yaml)

The SRE team declares only the listeners, TLS certificates, and namespace label selectors permitted to attach routes. This leverages a two-way handshake model: the Gateway uses allowedRoutes to restrict attachments to namespaces labeled app.kubernetes.io/part-of: shop, requiring application teams to label their namespaces accordingly:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: external-gateway
  namespace: infra-gateway
spec:
  gatewayClassName: envoy-gateway
  listeners:
  - name: https
    protocol: HTTPS
    port: 443
    hostname: "shop.example.com"
    tls:
      mode: Terminate
      certificateRefs:
      - name: shop-tls-secret
    allowedRoutes:
      namespaces:
        from: Selector
        selector:
          matchLabels:
            app.kubernetes.io/part-of: shop

Step 2: Frontend Team Defines the Root Route (frontend-route.yaml)

Frontend engineers maintain their static asset routing within their dedicated namespace:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: frontend-route
  namespace: frontend
spec:
  parentRefs:
  - name: external-gateway
    namespace: infra-gateway
  hostnames:
  - "shop.example.com"
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /
    backendRefs:
    - name: frontend-svc
      port: 80

Step 3: User Service Team Defines Path Rewriting (user-route.yaml)

No cryptic regular expressions—just the standardized URLRewrite filter:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: user-route
  namespace: user-app
spec:
  parentRefs:
  - name: external-gateway
    namespace: infra-gateway
  hostnames:
  - "shop.example.com"
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /api/v1/users
    filters:
    - type: URLRewrite
      urlRewrite:
        path:
          type: ReplacePrefixMatch
          replacePrefixMatch: /users
    backendRefs:
    - name: user-svc
      port: 8080

Step 4: Order Service Team Configures Canary Release (order-route.yaml)

No second shadow YAML required: traffic weights are declared directly within a single rule:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: order-route
  namespace: order-app
spec:
  parentRefs:
  - name: external-gateway
    namespace: infra-gateway
  hostnames:
  - "shop.example.com"
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /api/v1/orders
    backendRefs:
    - name: order-svc-v1
      port: 8080
      weight: 80
    - name: order-svc-v2
      port: 8080
      weight: 20

NOTE

Multi-Route Conflict Precedence: All three routes above declare the same hostname shop.example.com, and the frontend route matches the root path /. The Gateway API specification establishes deterministic Longest Prefix Match and rule precedence. Consequently, /api/v1/orders and /api/v1/users are each matched precisely to their own service—completely eliminating the override risk of undefined merge ordering across multiple YAML manifests that characterized the Ingress era.

Migration Comparison Table

The following table summarizes the paradigm shift from Ingress to the Gateway API:

Use CaseTraditional Ingress (e.g., NGINX)Gateway API StandardArchitectural & Operational Benefits
Ports & TLS CertificatesRedundantly declared across manifests in spec.tlsCentralized in Gateway.spec.listeners under platform controlStrict security boundaries; app developers never touch Secrets
URL Rewritingrewrite-target annotation paired with regex capturesfilters.type: URLRewrite with ReplacePrefixMatchDeclarative, intuitive syntax strictly validated by the API schema
Canary Traffic SplittingRequires a duplicate shadow Ingress annotated with canary: "true"Direct weight declaration within rules[].backendRefsSingle source of truth; zero shadow manifests to desync
Cross-Team & Cross-NamespaceLimited to routing Services in the same Namespace by defaultGateway defines allowedRoutes; Routes bind across namespacesNative multi-tenancy for microservices with clear RBAC isolation
Header MatchingDependent on proprietary controller annotationsFirst-class support via rules[].matches[].headersNative support for A/B testing and advanced canary criteria
RedirectsProprietary annotations (e.g., permanent-redirect)Standardized filters.type: RequestRedirectConsistent syntax across all conforming controllers
Observability & DiagnosticsRelies on controller container logs; no native status feedbackRich built-in status.conditions (e.g., Accepted, Programmed)Instant declarative feedback without grepping centralized controller logs

Official Migration Tooling: ingress2gateway

If your existing cluster has accumulated a large number of Ingress resources, rewriting them by hand is both time-consuming and error-prone. Kubernetes SIG-Network maintains an official automated conversion utility: ingress2gateway.

Key Tool Capabilities

Common CLI Examples

# 1. Install the CLI tool (via Go or GitHub Releases)
go install github.com/kubernetes-sigs/ingress2gateway@latest

# 2. Translate a single Ingress manifest and preview Gateway API resources
ingress2gateway print --providers ingress-nginx --input-file shop-ingress-main.yaml

# 3. Batch convert all Ingresses in the default namespace from a live cluster
ingress2gateway print --providers ingress-nginx --namespace default > migrated-gateway.yaml

NOTE

ingress2gateway automatically handles over 80% of standard routing, TLS, and basic Rewrite/Redirect rules; esoteric Lua scripts or deeply customized vendor plugins, however, still require manual verification and canary testing.


Evolution and Production Readiness

Following years of real-world battle-testing across the cloud-native community, the Gateway API officially reached General Availability (GA) with v1.0.0 in late 2023, establishing GatewayClass, Gateway, and HTTPRoute as backward-compatible, stable specifications.

The subsequent v1.1 release promoted GRPCRoute to GA and introduced session persistence as an experimental feature, while backend TLS verification (BackendTLSPolicy) only reached GA in v1.4.

Current Landscape

In today’s Kubernetes ecosystem (2026), the Gateway API is unquestionably production-ready. Having been validated across multiple minor releases, it has been adopted as a first-class standard by major cloud platforms and open-source networking projects alike. It is no longer an experimental initiative, but the current industry standard steadily superseding Ingress.


Under the Gateway API architecture, Kubernetes defines the API specification (CRDs), while the actual data-plane packet forwarding is executed by a Gateway Controller. When selecting an implementation for your infrastructure, consider these community-tested options:

Key Open-Source Implementations

TIP

Guidance for Existing Ingress-NGINX Users: The kubernetes/ingress-nginx project was officially retired by Kubernetes, with maintenance halted since March 2026, and has no direct in-place upgrade path to the Gateway API. When evaluating migrations, consider either CNCF’s flagship Envoy Gateway or F5’s official NGINX Gateway Fabric.

Pragmatic Adoption Guidelines (The KISS Principle)


Conclusion

The core value of the Gateway API lies in restoring traffic governance to a modern declarative architecture grounded in “clear role boundaries and strong schema validation.”

Trading the unstructured string annotations of annotation hell for API Server-validated routing filters and cross-namespace authorization boundaries measurably reduces production configuration mistakes and eliminates ticketing friction between teams.

Whether you are deploying greenfield clusters or executing a gradual dual-track migration, mastering this next-generation standard is an essential step toward higher platform engineering maturity.