Replacing Ingress with Kubernetes Gateway API — A Practical Guide to Declaratively Controlling Traffic Routing with HTTPRoute, GRPCRoute, and ReferenceGrant
nginx.ingress.kubernetes.io/rewrite-target: /$1 — Familiar annotation, right?
When I first learned Kubernetes, it took me quite a while to understand just this one line. Ingress felt less like a feature and more like a battle to memorize annotations. The bigger problem was that it only worked in NGINX Ingress. Switching to Contour meant completely different annotations, and Traefik spoke yet another dialect. Under the banner of "Kubernetes standard," every implementation was effectively running its own DSL.
In March 2026, the NGINX Ingress Controller was officially deprecated. A controller deployed across a large number of clusters was advised to migrate due to security flaws and design limitations. This was more than a simple deprecation notice — it was a clear signal from the Kubernetes community that Ingress would no longer be extended. And the alternative that stepped into its place is Gateway API.
In this post, we'll walk through three resources — HTTPRoute, GRPCRoute, and ReferenceGrant — with real YAML examples, and explore how to apply them to practical scenarios like canary deployments and cross-namespace service sharing. We'll also cover implementation selection criteria, migration strategies, and drawbacks.
Standard vs Experimental: Understanding the Two Channels First
Every resource in Gateway API belongs to one of two channels. Which channel a resource is in determines whether it's ready for production, so let's clarify this upfront.
| Channel | Meaning | Representative Resources |
|---|---|---|
| Standard | API stability guaranteed, long-term support, recommended for production | HTTPRoute, GRPCRoute, Gateway, GatewayClass, ReferenceGrant |
| Experimental | API subject to change, use with caution in production | TCPRoute, TLSRoute, UDPRoute, BackendLBPolicy |
All resources needed for HTTP, HTTPS, and gRPC traffic are in the Standard channel. TCP, TLS, and UDP routing are still Experimental, so you'll need to verify implementation support and API stability separately before using them in production.
Core Concepts
GatewayClass · Gateway · Route: A Role-Oriented Three-Tier Model
The core design philosophy of Gateway API is role-oriented separation. While Ingress crammed L7 routing rules, TLS configuration, and implementation-specific extensions all into a single resource, Gateway API divides responsibility across three tiers.
- GatewayClass: Defines "which controller to use." This is where decisions like the infra team choosing Envoy Gateway or the cloud team choosing AWS Load Balancer Controller live.
- Gateway: Defines "where and how to receive traffic." Port, protocol, and TLS configuration go here. Managed by the cluster operations team.
- Route resources: Define "which service to send incoming traffic to." Each application team manages this independently within their own namespace.
This structure naturally aligns with RBAC in multi-tenant environments. App teams can freely modify HTTPRoutes without needing access to Gateways, and operations teams don't need to intervene in app teams' Route configurations.
# Managed by the infra team: decides which controller to use
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: envoy-gateway
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controllerThe allowedRoutes setting on a Gateway controls which namespaces' Routes can attach to it.
# Managed by the cluster operations team: load balancer and listener configuration
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: prod-gateway
namespace: infra
spec:
gatewayClassName: envoy-gateway
listeners:
- name: https
protocol: HTTPS
port: 443
tls:
mode: Terminate
certificateRefs:
- name: prod-tls-cert
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
gateway-access: "true"With Selector mode in the example above, only Routes from namespaces labeled gateway-access: "true" can attach to this Gateway. The label must be applied to the app team's namespace for it to work.
kubectl label namespace team-a gateway-access=trueCaution — the default value of
allowedRoutesisSame. If this field is not set, only Routes in the same namespace as the Gateway can attach. In a multi-tenant setup, if your Routes are declared correctly but aren't connecting, check this setting first.
HTTPRoute: HTTP Routing Without Annotations
HTTPRoute is the core resource of Gateway API. Use parentRefs to specify which Gateway to attach to, hostnames for Host header matching, and rules for the actual routing logic. The example below demonstrates path-based routing combined with URL rewriting.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: my-app-route
namespace: team-a
spec:
parentRefs:
- name: prod-gateway
namespace: infra
hostnames:
- "api.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /v2/users
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /users
backendRefs:
- name: user-service
port: 8080What you used to solve with nginx.ingress.kubernetes.io/rewrite-target in Ingress is now declared explicitly with a URLRewrite filter. No need to memorize annotation names — the intent is clear through structured YAML fields.
GRPCRoute: Dedicated Routing for gRPC Services
gRPC routing was a real headache in the Ingress era. There were HTTP/2 upgrade negotiation issues and the tedium of manually writing regex matches for paths in the form /package.ServiceName/MethodName. GRPCRoute solves this in a gRPC-native way. It was promoted to the Standard channel in v1.1, so it's ready for production use.
apiVersion: gateway.networking.k8s.io/v1
kind: GRPCRoute
metadata:
name: payment-grpc-route
namespace: team-payment
spec:
parentRefs:
- name: prod-gateway
namespace: infra
hostnames:
- "grpc.example.com"
rules:
- matches:
- method:
type: Exact
service: payment.PaymentService
method: ProcessPayment
backendRefs:
# Route ProcessPayment to the priority-handling server
- name: payment-priority-service
port: 50051
- matches:
- method:
type: Exact
service: payment.PaymentService
# omitting method matches all methods of PaymentService
backendRefs:
# Route remaining PaymentService methods to the general server
- name: payment-service
port: 50051The service and method fields let you express gRPC intent directly. Because HTTP/2 is assumed as the baseline, you don't need to separately configure upgrade negotiation.
As shown in the second rule, omitting method matches all methods of that service. In this example, ProcessPayment requests are routed to payment-priority-service by the more specific first rule, while all other PaymentService methods are handled by payment-service via the second rule.
ReferenceGrant: Safe Cross-Namespace References
What happens when team A's HTTPRoute wants to reference team B's Service directly? Gateway API blocks this by default. A one-sided reference is not enough — explicit consent from both sides is required. That's the role of ReferenceGrant.
ReferenceGrant belongs to the Standard channel but uses apiVersion: gateway.networking.k8s.io/v1beta1. Channel and apiVersion are separate concepts. The Standard channel means API stability is guaranteed; the v1beta1 version label can be maintained independently of that.
# Placed in the shared-services namespace
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-team-a
namespace: shared-services
spec:
from:
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: team-a
to:
- group: ""
kind: Service
name: auth-serviceThe structure requires an explicit declaration that "another team may reference resources in my namespace." Without this ReferenceGrant, the controller silently blocks traffic forwarding, as shown in the failure path in the diagram.
Caution — debugging when traffic isn't flowing: Run
kubectl get httproute <name> -o yamland check the.status.parents[].conditionsfield. Rejection reasons such as a missing ReferenceGrant or anallowedRoutesmismatch are recorded in the status fields. Controllers often reject silently without separate error logs, making this field the first clue for debugging.
Practical Applications
Weight-Based Canary Deployment
The most common pattern when deploying a new version. Using the weight field inside backendRefs in an HTTPRoute, you can declaratively control traffic distribution.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: my-app-canary
namespace: team-a
spec:
parentRefs:
- name: prod-gateway
namespace: infra
hostnames:
- "app.example.com"
rules:
- backendRefs:
- name: my-app-v1
port: 8080
weight: 90
- name: my-app-v2
port: 8080
weight: 10Integrating with Argo Rollouts lets you adjust this weight automatically. The gatewayAPI plugin automatically steps through 25% → 50% → 75% → 100%, and rolls back immediately upon error detection. It also fits naturally into GitOps workflows.
Header-Based Canary Testing
A useful pattern when you want only the QA team to test a new version without exposing it to real user traffic. Requests with the traffic: test header are routed to v2, while all others remain on v1.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: my-app-header-canary
namespace: team-a
spec:
parentRefs:
- name: prod-gateway
namespace: infra
hostnames:
- "app.example.com"
rules:
- matches:
- headers:
- name: traffic
value: test
backendRefs:
- name: my-app-v2
port: 8080
- backendRefs:
- name: my-app-v1
port: 8080The Gateway API spec defines that more specific matches automatically take priority. A rule with an additional header condition is more specific than one with only a path, so requests carrying the traffic: test header are automatically routed to v2 by the spec's precedence rules. This behavior is guaranteed by match specificity, not by rule ordering.
gRPC Multi-Service Routing
A scenario where multiple gRPC services behind the same Gateway are routed separately by team.
# team-order namespace — owned by the order team
apiVersion: gateway.networking.k8s.io/v1
kind: GRPCRoute
metadata:
name: order-grpc-route
namespace: team-order
spec:
parentRefs:
- name: prod-gateway
namespace: infra
hostnames:
- "grpc.example.com"
rules:
- matches:
- method:
type: Exact
service: order.OrderService
# omitting method matches all methods of OrderService
backendRefs:
- name: order-service
port: 50051The payment team (team-payment) and the order team (team-order) each manage their own GRPCRoutes in their respective namespaces while sharing the same Gateway. This structure delivers both team independence and the benefits of shared infrastructure.
Cross-Namespace Shared Services
A scenario where a common authentication service lives in the shared-services namespace and multiple teams use it as a backend.
# HTTPRoute in the team-a namespace
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: team-a-route
namespace: team-a
spec:
parentRefs:
- name: prod-gateway
namespace: infra
rules:
- matches:
- path:
type: PathPrefix
value: /auth
backendRefs:
- name: auth-service
namespace: shared-services
port: 8080
group: ""
kind: Service# ReferenceGrant in the shared-services namespace
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-teams
namespace: shared-services
spec:
from:
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: team-a
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: team-b
to:
- group: ""
kind: Service
name: auth-serviceMultiple namespaces can be listed in the from array. This naturally creates a workflow where any new team wanting to use auth-service must request the shared-services team to add an entry to the ReferenceGrant.
Migration Strategy: From Ingress to Gateway API
Assessing the Current State with Ingress2Gateway
kubectl-ingress2gateway is the official tool that automatically converts existing Ingress resources to Gateway API resources. The command below gives you a preview of converting your cluster's current Ingress to HTTPRoute.
kubectl ingress2gateway printHowever, the scope of automatic conversion is limited. The items that cannot be converted represent the core of your actual migration work.
| Automatically Convertible | Requires Manual Work |
|---|---|
| Host-based routing | nginx.ingress.kubernetes.io/limit-rps (Rate Limiting) |
| Path-based routing | nginx.ingress.kubernetes.io/auth-url (external auth integration) |
| TLS termination | nginx.ingress.kubernetes.io/configuration-snippet (Lua/NGINX custom config) |
| Default backend configuration | Regex-based path rewrite annotations |
If you rely heavily on NGINX-specific annotations, a significant number of items may be unconvertible. Those will need to be replaced with the implementation's Policy API (e.g., Envoy Gateway's BackendTrafficPolicy) or the ExtensionRef mechanism.
Incremental Migration Approach
The two APIs can coexist in the same cluster without replacing existing Ingress right away. A realistic approach is to start new services on HTTPRoute or GRPCRoute while keeping existing services on Ingress and migrating teams sequentially. Depending on your organization's size and level of annotation dependency, this can take considerable time — it's best to assess the migration scope first, then set a timeline.
Pros and Cons
Implementation Comparison
| Implementation | Data Plane | Rate Limiting | Update Propagation Speed | Notes |
|---|---|---|---|---|
| Envoy Gateway | Envoy Proxy | Native support | Fast | Rate Limiting via Policy API |
| Istio Ambient | Envoy + ztunnel | Policy-based | Millisecond-level | Sidecar hop eliminated, L7 via waypoint |
| Cilium | eBPF | Not supported | Fast | Best L4 throughput; traffic drops reported under high route churn |
| Kong Gateway | nginx | Plugin-based | Millisecond-level | Enterprise plugin ecosystem |
| NGINX Gateway Fabric | NGINX | Limited | Seconds of delay | Mature L7 processing |
| Traefik | Custom | Middleware | Seconds of delay | Suitable for small-scale environments |
Advantages
| Item | Details |
|---|---|
| Role separation and RBAC alignment | The GatewayClass·Gateway·Route three-tier model naturally aligns with the permission boundaries of infra, operations, and app teams |
| Declarative configuration | Structured CRD fields instead of annotations make GitOps workflow integration straightforward |
| Multi-protocol support | HTTP·HTTPS·gRPC (Standard) / TCP·TLS·UDP (Experimental) handled through a single API |
| Standard traffic features | Weighted splitting, mirroring, and header rewriting are defined at the spec level, ensuring portability across implementations |
| Implementation portability | Swap only the GatewayClass to switch controllers without modifying Route resources |
Disadvantages and Considerations
| Item | Details |
|---|---|
| Behavioral differences between implementations | Significant variation across implementations in Rate Limiting and update propagation speed |
| Cilium under high load | Traffic drops reported in high route churn environments — validate at your load scale |
| Migration complexity | Beyond simple conversion, requires reviewing namespace design, ReferenceGrant policies, and annotation replacement strategies |
| Initial learning curve | Additional learning needed for the three-tier model, Standard/Experimental channel distinctions, and per-implementation extension policies |
| ReferenceGrant management overhead | A separate resource is required for each cross-namespace reference, increasing the number of objects to manage at scale |
Caution: Implementation-Specific Extension Features
BackendTrafficPolicy (Rate Limiting) is an Envoy Gateway-specific CRD. When switching to another implementation, this part requires separate handling. The expectation that "swapping GatewayClass fixes everything" applies only to standard Route resources. Implementation-specific Policy APIs do not port over.
Closing Thoughts
Gateway API is the current standard for Kubernetes traffic routing. Early 2026 — with the NGINX Ingress Controller deprecation and the Ingress2Gateway v1.0 release happening back to back — was a clear turning point, and GRPCRoute has already settled into the Standard channel as of v1.1. The era of annotation-based Ingress is fading, and role separation, declarative configuration, and implementation portability are the core values of Gateway API.
Here are three steps to get started today:
-
Assess your current state with Ingress2Gateway: Running
kubectl-ingress2gateway printconverts your existing Ingress and separates what can be automatically converted from what requires manual work. The unconverted items tell you the scope of your migration. -
Apply HTTPRoute to one new service: You don't have to replace existing Ingress right away. Applying HTTPRoute to a newly deployed service is something you can do right now. The two APIs coexist, so incremental migration is possible.
-
Choose an implementation that fits your purpose: If you need Rate Limiting, consider Envoy Gateway. If service mesh integration matters, look at Istio Ambient. For cloud-native integration, evaluate GKE Gateway Controller or AWS Load Balancer Controller. Since only the GatewayClass needs to change while Route resources remain intact, switching later is also an option.
References
- Gateway API Official Docs — Getting Started
- HTTPRoute API Reference
- GRPCRoute API Reference
- GEP-709: Cross Namespace References — ReferenceGrant Design Doc
- Gateway API v1.5 Release Blog
- Gateway API v1.4 Release Blog
- Announcing Ingress2Gateway 1.0
- From Ingress to Gateway API — Microsoft Azure Architecture Blog
- Kubernetes Gateway API in 2026: Envoy · Istio · Cilium · Kong Comparison
- Kubernetes Ingress vs Gateway API: What to Use in 2026
- HTTP Traffic Splitting — Gateway API Official Guide
- gRPC Routing — Gateway API Official Guide
- Kubernetes Gateway API — Amazon Web Services Case Study
- Gateway API Provider Support In 2026: A Critical Evaluation of Implementations
- gateway-api GitHub Repository