- Introduction
- Gateway API vs Ingress: An Architectural Comparison
- The Core Resources in Detail
- Migrating from Ingress to the Gateway API
- TLS Configuration and Certificate Management
- Traffic Splitting and Weighted Routing
- Cautions for Day-to-day Operations
- Troubleshooting
- Failure Cases and Recovery Procedures
- Migration Checklist
- Conclusion
- References

Introduction
For a long time the Ingress resource has been the de facto standard for routing external traffic to services inside a Kubernetes cluster. However, because Ingress was designed with nothing but simple HTTP routing in mind, the features production demands - advanced routing, traffic splitting, multi-protocol support - had no choice but to rely on per-controller annotations. That has produced a chronic portability problem: an annotation written for Ingress NGINX does not work on Traefik or HAProxy.
The Gateway API is the next-generation service networking API Kubernetes SIG-Network designed to solve those limitations at the root. Starting with the v1.0 GA in October 2023, v1.2 in November 2024 added WebSocket, timeout and retry support, and v1.4 in November 2025 promoted BackendTLSPolicy and ReferenceGrant to the Standard Channel, bringing it to production-level maturity. In particular, the Kubernetes community has declared the official end of life for Ingress NGINX as March 2026, after which no security patches or bug fixes will be provided. Migrating to the Gateway API is no longer a choice but a necessity.
In this article, we cover the whole picture: the Gateway API's architectural principles, the core resources in detail, the strategy for migrating from Ingress, TLS configuration, traffic splitting, and troubleshooting and failure recovery in day-to-day operations.
Gateway API vs Ingress: An Architectural Comparison
The Limits of Ingress
The Ingress resource mixes load balancer configuration and routing rules into a single resource. The concerns of the cluster operator and the application developer are not separated, and advanced features can only be implemented through non-standardized annotations.
The Gateway API's Role-based Design
The Gateway API adopts a role-oriented design that cleanly separates the concerns of the infrastructure provider, the cluster operator and the application developer.
| Comparison | Ingress | Gateway API |
|---|---|---|
| Resource structure | A single Ingress resource | Split into GatewayClass, Gateway and HTTPRoute |
| Role separation | None (all mixed into one resource) | Split across infrastructure provider / cluster operator / developer |
| Protocol support | HTTP/HTTPS only | HTTP, HTTPS, TCP, UDP, gRPC and TLS supported |
| Routing capability | Host/path based only | Matching on headers, query parameters and methods |
| Traffic splitting | Depends on annotations (non-standard) | Native weight-based splitting |
| TLS configuration | Basic termination only | Terminate, Passthrough, BackendTLSPolicy |
| Cross-namespace | Not possible | Safely supported through ReferenceGrant |
| Portability | Needs per-controller annotations | Portable through a standard API spec |
| Multi-tenancy | Weak | Per-Gateway namespace isolation supported |
| Status management | Limited | Accepted, Programmed and ResolvedRefs conditions |
Architecture Diagram
Infrastructure Provider
└─ GatewayClass: defines which controller implements the Gateway
│
Cluster Operator
└─ Gateway: configures the listeners (port, protocol, TLS)
│
Application Developer
└─ HTTPRoute / GRPCRoute / TCPRoute: defines the routing rules
│
└─ Service → Pod: handles the actual traffic
The Core Resources in Detail
1. GatewayClass
GatewayClass is a cluster-scoped resource defined by the infrastructure provider. It specifies which controller manages the Gateway, and it plays a role similar to Kubernetes' StorageClass.
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: envoy-gateway-class
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
description: 'Production gateway class based on Envoy Gateway'
The controllerName value for the main controllers:
| Controller | controllerName |
|---|---|
| Envoy Gateway | gateway.envoyproxy.io/gatewayclass-controller |
| NGINX Gateway Fabric | gateway.nginx.org/nginx-gateway-controller |
| Istio | istio.io/gateway-controller |
| Cilium | io.cilium/gateway-controller |
| Traefik | traefik.io/gateway-controller |
| Kong | konghq.com/kic-gateway-controller |
2. Gateway
Gateway is a namespace-scoped resource managed by the cluster operator. Through its listeners it defines the port, protocol, hostname and TLS settings by which traffic enters.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: production-gateway
namespace: gateway-infra
annotations:
cert-manager.io/cluster-issuer: 'letsencrypt-prod'
spec:
gatewayClassName: envoy-gateway-class
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: All
- name: https
protocol: HTTPS
port: 443
hostname: '*.example.com'
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: wildcard-tls-cert
namespace: gateway-infra
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
gateway-access: 'enabled'
The allowedRoutes setting forms the key security boundary in a multi-tenant environment. from: All means Routes from every namespace can bind to this listener, while from: Selector allows only namespaces carrying a particular label.
3. HTTPRoute
HTTPRoute is the routing-rule resource managed by the application developer. It provides a rich set of features as a standard API: path matching, header filtering, traffic splitting, redirects and URL rewriting.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: api-route
namespace: backend-app
spec:
parentRefs:
- name: production-gateway
namespace: gateway-infra
sectionName: https
hostnames:
- 'api.example.com'
rules:
- matches:
- path:
type: PathPrefix
value: /v2/users
headers:
- name: X-API-Version
value: '2'
filters:
- type: RequestHeaderModifier
requestHeaderModifier:
add:
- name: X-Forwarded-By
value: 'gateway-api'
backendRefs:
- name: users-service-v2
port: 8080
weight: 100
- matches:
- path:
type: PathPrefix
value: /v1
backendRefs:
- name: api-service-v1
port: 8080
weight: 90
- name: api-service-v2
port: 8080
weight: 10
4. ReferenceGrant (Cross-namespace References)
In the Gateway API, cross-namespace references are blocked by default. They have to be allowed explicitly through a ReferenceGrant. Promoted to v1 in v1.4, this resource enables flexible configuration while preserving the security boundary.
apiVersion: gateway.networking.k8s.io/v1
kind: ReferenceGrant
metadata:
name: allow-gateway-to-backend-secrets
namespace: gateway-infra
spec:
from:
- group: gateway.networking.k8s.io
kind: Gateway
namespace: gateway-infra
to:
- group: ''
kind: Secret
Migrating from Ingress to the Gateway API
Overview of the Migration Strategy
The migration must be done in stages. The Gateway API controller can run in parallel with the existing Ingress controller in the same cluster, so switching over service by service and validating as you go is the safe approach.
Step 1: Install the Gateway API CRDs and the Controller
# install the standard Gateway API CRDs (v1.4.x)
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.1/standard-install.yaml
# install with the experimental features (includes TCPRoute, UDPRoute, BackendTLSPolicy)
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.1/experimental-install.yaml
# confirm the CRDs are installed
kubectl get crd | grep gateway.networking.k8s.io
# example output:
# gatewayclasses.gateway.networking.k8s.io
# gateways.gateway.networking.k8s.io
# httproutes.gateway.networking.k8s.io
# referencegrants.gateway.networking.k8s.io
# grpcroutes.gateway.networking.k8s.io
How you install the controller depends on the implementation you chose. Here is the Envoy Gateway example:
# install Envoy Gateway
helm install envoy-gateway oci://docker.io/envoyproxy/gateway-helm \
--version v1.3.0 \
-n envoy-gateway-system \
--create-namespace
# confirm the installation
kubectl get pods -n envoy-gateway-system
kubectl get gatewayclass
Step 2: Automatic Conversion with the ingress2gateway Tool
The ingress2gateway tool analyzes your existing Ingress resources and converts them into Gateway API resources automatically. Roughly 30-40% of annotations need manual conversion, however, so you must review the output.
# install ingress2gateway
go install github.com/kubernetes-sigs/ingress2gateway@latest
# convert the current cluster's Ingress resources
ingress2gateway print --providers ingress-nginx \
--all-namespaces > gateway-resources.yaml
# review the converted resources (a manual check is mandatory)
cat gateway-resources.yaml
# apply the converted resources to the staging environment first
kubectl apply -f gateway-resources.yaml --dry-run=server
kubectl apply -f gateway-resources.yaml -n staging
Step 3: Parallel Operation and Traffic Cutover
Run the existing Ingress and the Gateway API side by side, switching over gradually, service by service.
# check the Gateway status - confirming Programmed: True is mandatory
kubectl get gateway production-gateway -n gateway-infra -o jsonpath='{.status.conditions}'
# check the HTTPRoute status
kubectl get httproute -A
# test before pointing DNS at the Gateway API endpoint
GATEWAY_IP=$(kubectl get gateway production-gateway -n gateway-infra \
-o jsonpath='{.status.addresses[0].value}')
curl -H "Host: api.example.com" https://$GATEWAY_IP/v1/health --resolve "api.example.com:443:$GATEWAY_IP"
# once verified, change DNS (CNAME or A record)
# delete the old Ingress resources once every service has been switched over
kubectl delete ingress api-ingress -n backend-app
Step 4: Clean Up the Old Ingress
Once every service has moved to the Gateway API, clean up the old Ingress controller. You must delete them one service at a time and confirm reachability after each one.
# delete the Ingress resources one at a time and validate
kubectl delete ingress api-ingress -n backend-app
# test service access immediately
curl -I https://api.example.com/v1/health
# once every Ingress is gone, remove the controller
kubectl get ingress -A # confirm no Ingress remains
helm uninstall ingress-nginx -n ingress-nginx
kubectl delete namespace ingress-nginx
TLS Configuration and Certificate Management
Downstream TLS (Client to Gateway)
Configure TLS termination on the Gateway listener. Integrating with cert-manager automates certificate issuance and renewal.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: tls-gateway
namespace: gateway-infra
annotations:
cert-manager.io/cluster-issuer: 'letsencrypt-prod'
spec:
gatewayClassName: envoy-gateway-class
listeners:
- name: https-wildcard
protocol: HTTPS
port: 443
hostname: '*.example.com'
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: wildcard-example-tls
allowedRoutes:
namespaces:
from: All
- name: https-specific
protocol: HTTPS
port: 443
hostname: 'admin.internal.com'
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: admin-tls-cert
allowedRoutes:
namespaces:
from: Same
cert-manager detects the annotation on the Gateway resource, creates a Certificate resource automatically, and stores the issued certificate in the Secret named in certificateRefs. Automatic renewal happens 30 days before the certificate expires.
Upstream TLS (Gateway to Backend)
BackendTLSPolicy, promoted to the Standard Channel in v1.4, lets you configure the TLS connection from the Gateway to the backend Pods. That is how you implement end-to-end encryption.
apiVersion: gateway.networking.k8s.io/v1alpha3
kind: BackendTLSPolicy
metadata:
name: backend-tls
namespace: backend-app
spec:
targetRefs:
- group: ''
kind: Service
name: secure-backend-service
validation:
caCertificateRefs:
- name: backend-ca-cert
group: ''
kind: ConfigMap
hostname: secure-backend.backend-app.svc.cluster.local
The BackendTLSPolicy and its target Service must be in the same namespace. A cross-namespace BackendTLSPolicy is not supported because of the trust boundary problem.
TLS Passthrough
This is the mode in which the Gateway does not terminate TLS and passes it straight through to the backend. Use it when the backend application has to handle TLS itself.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: passthrough-gateway
namespace: gateway-infra
spec:
gatewayClassName: envoy-gateway-class
listeners:
- name: tls-passthrough
protocol: TLS
port: 443
hostname: 'secure-app.example.com'
tls:
mode: Passthrough
allowedRoutes:
namespaces:
from: All
Traffic Splitting and Weighted Routing
Canary Deployment Traffic Splitting
Setting a weight on the HTTPRoute's backendRefs distributes traffic by ratio. Because the weight is a proportion, the values do not have to sum to 100; each backend's share is computed against the total of all weights.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: canary-route
namespace: backend-app
spec:
parentRefs:
- name: production-gateway
namespace: gateway-infra
hostnames:
- 'app.example.com'
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
# stable version: 90% of the traffic
- name: app-stable
port: 8080
weight: 90
# canary version: 10% of the traffic
- name: app-canary
port: 8080
weight: 10
A Script for Progressive Traffic Cutover
Here is an example automation script that adjusts the traffic ratio in stages during a canary deployment.
#!/bin/bash
# script that increases canary traffic progressively
ROUTE_NAME="canary-route"
NAMESPACE="backend-app"
STAGES=(10 25 50 75 100)
WAIT_MINUTES=5
for canary_weight in "${STAGES[@]}"; do
stable_weight=$((100 - canary_weight))
echo "[$(date)] canary traffic ratio: ${canary_weight}%"
kubectl patch httproute $ROUTE_NAME -n $NAMESPACE --type='json' \
-p="[
{\"op\": \"replace\", \"path\": \"/spec/rules/0/backendRefs/0/weight\", \"value\": $stable_weight},
{\"op\": \"replace\", \"path\": \"/spec/rules/0/backendRefs/1/weight\", \"value\": $canary_weight}
]"
echo "waiting ${WAIT_MINUTES} minutes... (monitoring the error rate)"
sleep $((WAIT_MINUTES * 60))
# check the error rate (example Prometheus query)
ERROR_RATE=$(kubectl exec -n monitoring prometheus-0 -- \
promtool query instant \
'rate(http_requests_total{service="app-canary",code=~"5.."}[5m]) / rate(http_requests_total{service="app-canary"}[5m]) * 100' \
2>/dev/null | grep -oP '[0-9.]+' | head -1)
if (( $(echo "$ERROR_RATE > 5" | bc -l 2>/dev/null) )); then
echo "error rate ${ERROR_RATE}% exceeded! rolling back"
kubectl patch httproute $ROUTE_NAME -n $NAMESPACE --type='json' \
-p='[
{"op": "replace", "path": "/spec/rules/0/backendRefs/0/weight", "value": 100},
{"op": "replace", "path": "/spec/rules/0/backendRefs/1/weight", "value": 0}
]'
echo "rollback complete. canary deployment aborted."
exit 1
fi
done
echo "canary deployment complete. 100% of traffic switched over successfully."
Header-based Traffic Splitting
Routing only requests carrying a particular header to the new version lets the QA team or internal users validate it first.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: header-based-route
namespace: backend-app
spec:
parentRefs:
- name: production-gateway
namespace: gateway-infra
hostnames:
- 'app.example.com'
rules:
# rule 1: route to v2 when the X-Canary header is present
- matches:
- headers:
- name: X-Canary
value: 'true'
backendRefs:
- name: app-v2
port: 8080
# rule 2: route the default traffic to v1
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: app-v1
port: 8080
Cautions for Day-to-day Operations
Monitoring Gateway Status
You must monitor the status conditions of the Gateway API resources. There are three key conditions.
| Condition | Meaning | When to check it |
|---|---|---|
| Accepted | The resource is syntactically/semantically valid and the controller accepted it | Right after the resource is created |
| Programmed | The configuration is fully reflected in the data plane | A mandatory check before cutting over traffic |
| ResolvedRefs | Every external object it references (Secret, Service and so on) is valid | With TLS configuration and cross-namespace references |
# check the Gateway status conditions
kubectl describe gateway production-gateway -n gateway-infra
# check the HTTPRoute status
kubectl get httproute -A -o custom-columns=\
'NAME:.metadata.name,HOSTNAMES:.spec.hostnames[*],PARENT:.spec.parentRefs[0].name,ACCEPTED:.status.parents[0].conditions[?(@.type=="Accepted")].status'
Managing the allowedRoutes Setting
In production you must use from: Selector or from: Same instead of from: All, so that which namespaces' Routes may bind to the Gateway is restricted explicitly. Ignoring this allows a malicious or mistaken HTTPRoute to bind to the production Gateway, which can result in traffic hijacking.
Avoiding Listener Hostname Collisions
Defining duplicate listeners with the same hostname on the same Gateway produces unpredictable behavior. Listener names must be unique, and you have to understand the precedence rules when a wildcard hostname (*.example.com) and a specific hostname (api.example.com) coexist. The specific hostname takes precedence over the wildcard.
The Order of Resource Cleanup
The order matters when cleaning up resources after the migration. Deleting the HTTPRoute first cuts traffic immediately. You must confirm the DNS change first, and delete the old Ingress resources before anything else.
Troubleshooting
Problem 1: The Gateway Is Stuck at Programmed: False
Cause: the GatewayClass controller is not running, or the TLS certificate reference is not valid.
# check the controller Pod status
kubectl get pods -n envoy-gateway-system
# check the Gateway events
kubectl describe gateway production-gateway -n gateway-infra | tail -20
# check whether the TLS Secret exists
kubectl get secret wildcard-tls-cert -n gateway-infra
# check the GatewayClass status
kubectl get gatewayclass envoy-gateway-class -o yaml
Fix: if the controller Pod is in CrashLoopBackOff, check its logs. If the TLS Secret is missing, check the cert-manager logs and inspect the Certificate resource's status.
Problem 2: The HTTPRoute Is Accepted but Traffic Is Not Routed
Cause: the Service named in backendRef does not exist, the port number does not match, or the Pods are not healthy.
# check the ResolvedRefs condition on the HTTPRoute status
kubectl get httproute api-route -n backend-app -o yaml | grep -A5 "ResolvedRefs"
# check whether the backend Service exists
kubectl get svc users-service-v2 -n backend-app
# check the endpoints (whether there are healthy Pods)
kubectl get endpoints users-service-v2 -n backend-app
# check the Pod status and logs
kubectl get pods -n backend-app -l app=users-service-v2
kubectl logs -n backend-app -l app=users-service-v2 --tail=50
Problem 3: A Cross-namespace Reference Fails
Cause: the ReferenceGrant is not configured correctly.
# list the ReferenceGrants
kubectl get referencegrant -A
# inspect the ReferenceGrant in a specific namespace
kubectl describe referencegrant -n gateway-infra
Fix: check that the namespace, group and kind in the from field match the referencing resource exactly. The ReferenceGrant has to be created in the namespace where the reference target (to) lives.
Problem 4: No Certificate Is Issued with the cert-manager Integration
# check the Certificate resource status
kubectl get certificate -n gateway-infra
# check the cert-manager logs
kubectl logs -n cert-manager deploy/cert-manager --tail=100
# check the Challenge status (ACME HTTP-01)
kubectl get challenge -A
# check the Order status
kubectl get order -A
Fix: check that the ClusterIssuer is configured correctly, that the ACME server is reachable, and that the port 80 listener needed for the HTTP-01 challenge is open.
Failure Cases and Recovery Procedures
Case 1: Traffic Lost During the DNS Cutover
Situation: the Ingress was deleted while the DNS TTL was still high, and traffic from some clients was lost.
Recovery procedure:
- Recreate the deleted Ingress resource immediately to restore the old path.
- Lower the DNS TTL to 300 seconds (5 minutes) or less, then wait at least 2 times the previous TTL.
- Reconfirm that the Gateway's Programmed status is True.
- Point DNS at the Gateway API endpoint.
- Monitor for at least 24 hours before deleting the old Ingress.
Prevention: lower the DNS TTL to 60-300 seconds before the migration and allow enough propagation time before cutting over.
Case 2: A Full Traffic Outage from a Weight Misconfiguration
Situation: during a canary deployment every backendRefs weight was set to 0, which produced 503 errors.
Recovery procedure:
# restore 100% of the traffic to the stable version immediately
kubectl patch httproute canary-route -n backend-app --type='json' \
-p='[
{"op": "replace", "path": "/spec/rules/0/backendRefs/0/weight", "value": 100},
{"op": "replace", "path": "/spec/rules/0/backendRefs/1/weight", "value": 0}
]'
# confirm it applied
kubectl get httproute canary-route -n backend-app -o yaml
Prevention: include validation logic in the automation script so that at least one backend keeps a weight greater than 0 whenever the weights change.
Case 3: A TLS Certificate Reference Fails Because a ReferenceGrant Is Missing
Situation: the Gateway and the TLS Secret were in different namespaces and it was deployed without a ReferenceGrant, so the Gateway never reached the Programmed state.
Recovery procedure:
- Check the ResolvedRefs condition in the Gateway's status.conditions.
- Create the missing ReferenceGrant in the namespace where the Secret lives.
- Confirm that the Gateway transitions to Programmed: True.
Case 4: Data Plane Interruption During a Controller Upgrade
Situation: while upgrading the Gateway API controller with Helm, the data plane Pods restarted and traffic was briefly interrupted.
Recovery procedure:
- You must back up the Gateway resources as YAML before upgrading.
- Check the controller's RollingUpdate strategy.
- Run a Helm rollback if an interruption occurs.
# back up before upgrading
kubectl get gateway,httproute,referencegrant -A -o yaml > gateway-backup.yaml
# Helm rollback
helm rollback envoy-gateway -n envoy-gateway-system
# check the data plane Pod status
kubectl get pods -n envoy-gateway-system -w
Prevention: set a PodDisruptionBudget for the controller upgrade, and validate it in a staging environment first.
Migration Checklist
Here is a checklist of what to confirm before, during and after the migration.
Preparation
- Are the Gateway API CRDs installed in the cluster (
kubectl get crd | grep gateway)? - Is the Gateway API controller you chose running correctly?
- Is the GatewayClass in the Accepted state?
- Is cert-manager at a version that supports the Gateway API integration (1.15+)?
- Has the DNS TTL been lowered to 300 seconds or less?
- Is the full list of existing Ingress resources documented?
- Has the YAML converted by ingress2gateway been reviewed by hand?
During the Migration
- Is the Gateway resource at Programmed: True?
- Is every HTTPRoute at Accepted: True?
- Is ResolvedRefs True on every HTTPRoute?
- Were the TLS certificates issued correctly (
kubectl get certificate)? - Is a ReferenceGrant configured for every cross-namespace reference?
- Has endpoint reachability been confirmed with curl or external monitoring?
- Has the DNS cutover to the Gateway API endpoint completed?
After the Migration
- Have all the old Ingress resources been deleted?
- Has the old Ingress controller been cleaned up?
- Is the monitoring dashboard collecting Gateway API metrics?
- Do the alert rules cover the Gateway status conditions?
- Has the incident response runbook been updated for the Gateway API?
- Is the Gateway resource YAML backup stored in version control such as Git?
Conclusion
The Gateway API is the future of Kubernetes networking. Its role-based design cleanly separates the concerns of infrastructure operators and application developers, its standardized API guarantees portability between controllers, and it provides native traffic splitting and advanced routing. With BackendTLSPolicy and ReferenceGrant promoted to GA in v1.4, nearly everything production needs is now in the Standard Channel.
With the official end of support for Ingress NGINX scheduled for March 2026, now is the time if you have not started migrating yet. Use the staged migration strategy, the ingress2gateway tool and the parallel operation approach covered in this article to make the switch safely. Most important of all: you must confirm the Gateway is at Programmed: True before you cut any traffic over. Not rushing, and instead switching over service by service while validating, is the safest approach.
References
- Kubernetes Gateway API official documentation
- Guide to migrating from Ingress to the Gateway API
- Gateway API v1.4 release blog
- Gateway API TLS configuration guide
- Gateway API traffic splitting guide
- cert-manager Gateway API integration
- Introducing the ingress2gateway tool
- Gateway API troubleshooting guide
- Guide to migrating from Ingress NGINX