- Published on
Kubernetes Gateway API Production Guide: From Ingress Migration to HTTPRoute, GRPCRoute, and Envoy Gateway Deployment
- Authors
- Name
- Introduction
- Gateway API vs Ingress Comparison
- Envoy Gateway Installation and Configuration
- HTTPRoute In Depth
- GRPCRoute Production Configuration
- TLS Termination and Certificate Management
- Envoy Gateway Rate Limiting
- Cross-Namespace Routing with ReferenceGrant
- Migrating from Ingress to Gateway API
- Monitoring Setup
- Operational Notes
- Failure Cases and Recovery Procedures
- Conclusion
- References

Introduction
Kubernetes Ingress has long been the standard method for routing external traffic into the cluster. However, its limitations have been clear: dependency on implementation-specific annotations, all configuration concentrated in a single resource, and lack of support for non-HTTP protocols like gRPC and TCP. The Kubernetes community designed the Gateway API to address these issues, and as of v1.2, both HTTPRoute and GRPCRoute are GA (Generally Available) and production-ready.
With the official retirement of Ingress NGINX announced (best-effort maintenance only after March 2026), migrating to Gateway API has become a necessity rather than an option. This guide covers everything needed for production deployment using Envoy Gateway as the implementation: HTTPRoute, GRPCRoute, TLS termination, traffic splitting, header-based routing, rate limiting, and monitoring, all with practical code examples.
Gateway API vs Ingress Comparison
Architectural Differences
Gateway API is designed with a role-oriented resource model that clearly separates concerns between infrastructure operators and application developers.
| Comparison | Ingress | Gateway API |
|---|---|---|
| Resource Model | Single Ingress resource | GatewayClass / Gateway / Route separation |
| Role Separation | Not possible | Infra team / Platform team / Dev team |
| Protocol Support | HTTP/HTTPS only | HTTP, gRPC, TCP, UDP, TLS |
| Configuration | Implementation-specific annotations | Standardized spec |
| Cross-namespace | Not supported | Supported via ReferenceGrant |
| Traffic Splitting | Annotation-dependent | Native weight-based |
| Header Matching | Varies by implementation | Included in standard spec |
| GA Status | Stable but retiring | v1.2 GA (HTTPRoute, GRPCRoute) |
Resource Hierarchy
Let us examine the 3-tier resource model of Gateway API.
# Tier 1: GatewayClass - Managed by infrastructure provider (cluster-scoped)
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: envoy-gateway
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
---
# Tier 2: Gateway - Managed by platform team (namespace-scoped)
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: production-gateway
namespace: infra-gateway
spec:
gatewayClassName: envoy-gateway
listeners:
- name: https
protocol: HTTPS
port: 443
tls:
mode: Terminate
certificateRefs:
- name: wildcard-tls-cert
- name: http
protocol: HTTP
port: 80
---
# Tier 3: HTTPRoute - Managed by dev teams (per application namespace)
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: api-route
namespace: app-team-a
spec:
parentRefs:
- name: production-gateway
namespace: infra-gateway
sectionName: https
hostnames:
- 'api.example.com'
rules:
- matches:
- path:
type: PathPrefix
value: /v1/users
backendRefs:
- name: user-service
port: 8080
Envoy Gateway Installation and Configuration
Installation via Helm
# Install Envoy Gateway Helm chart
helm install eg oci://docker.io/envoyproxy/gateway-helm \
--version v1.3.0 \
-n envoy-gateway-system \
--create-namespace \
--set config.envoyGateway.logging.level.default=info
# Verify installation
kubectl get pods -n envoy-gateway-system
kubectl get gatewayclass
# Verify CRDs
kubectl get crd | grep gateway.networking.k8s.io
GatewayClass and Gateway Creation
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: envoy-production
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
parametersRef:
group: gateway.envoyproxy.io
kind: EnvoyProxy
name: production-config
namespace: envoy-gateway-system
---
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
name: production-config
namespace: envoy-gateway-system
spec:
provider:
type: Kubernetes
kubernetes:
envoyDeployment:
replicas: 3
pod:
annotations:
prometheus.io/scrape: 'true'
prometheus.io/port: '19001'
container:
resources:
requests:
cpu: '500m'
memory: '512Mi'
limits:
cpu: '2000m'
memory: '2Gi'
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: production-gateway
namespace: infra-gateway
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
gatewayClassName: envoy-production
listeners:
- name: https-wildcard
protocol: HTTPS
port: 443
hostname: '*.example.com'
tls:
mode: Terminate
certificateRefs:
- name: wildcard-example-tls
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
gateway-access: 'true'
- name: http-redirect
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: Same
HTTPRoute In Depth
Path-Based Routing
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: app-routes
namespace: production
spec:
parentRefs:
- name: production-gateway
namespace: infra-gateway
sectionName: https-wildcard
hostnames:
- 'app.example.com'
rules:
# Exact match - highest priority
- matches:
- path:
type: Exact
value: /healthz
backendRefs:
- name: health-check-service
port: 8080
# PathPrefix match - API version routing
- matches:
- path:
type: PathPrefix
value: /api/v2
backendRefs:
- name: api-v2-service
port: 8080
- matches:
- path:
type: PathPrefix
value: /api/v1
backendRefs:
- name: api-v1-service
port: 8080
# RegularExpression match
- matches:
- path:
type: RegularExpression
value: '/users/[0-9]+/profile'
backendRefs:
- name: user-profile-service
port: 8080
Header-Based Routing
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: header-based-routing
namespace: production
spec:
parentRefs:
- name: production-gateway
namespace: infra-gateway
hostnames:
- 'api.example.com'
rules:
# Route to different backends based on header values
- matches:
- headers:
- name: x-api-version
value: 'beta'
- name: x-user-tier
value: 'premium'
backendRefs:
- name: api-beta-premium
port: 8080
# A/B testing via header-based routing
- matches:
- headers:
- name: x-experiment-group
value: 'treatment'
backendRefs:
- name: api-experiment
port: 8080
# Default routing (no match conditions)
- backendRefs:
- name: api-stable
port: 8080
Traffic Splitting (Canary Deployment)
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: canary-route
namespace: production
spec:
parentRefs:
- name: production-gateway
namespace: infra-gateway
hostnames:
- 'app.example.com'
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
# Stable version: 90% traffic
- name: app-stable
port: 8080
weight: 90
# Canary version: 10% traffic
- name: app-canary
port: 8080
weight: 10
HTTP Redirects and URL Rewrites
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: redirect-and-rewrite
namespace: production
spec:
parentRefs:
- name: production-gateway
namespace: infra-gateway
hostnames:
- 'app.example.com'
rules:
# HTTP -> HTTPS redirect
- matches:
- path:
type: PathPrefix
value: /
filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: url-rewrite
namespace: production
spec:
parentRefs:
- name: production-gateway
namespace: infra-gateway
hostnames:
- 'api.example.com'
rules:
# /old-api/* -> /new-api/* path rewrite
- matches:
- path:
type: PathPrefix
value: /old-api
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /new-api
backendRefs:
- name: api-service
port: 8080
GRPCRoute Production Configuration
Basic GRPCRoute Setup
GRPCRoute was promoted to GA in Gateway API v1.1, and in v1.2 the legacy v1alpha2 version was completely removed.
apiVersion: gateway.networking.k8s.io/v1
kind: GRPCRoute
metadata:
name: order-service-route
namespace: production
spec:
parentRefs:
- name: production-gateway
namespace: infra-gateway
sectionName: https-wildcard
hostnames:
- 'grpc.example.com'
rules:
# Service-based matching
- matches:
- method:
service: 'order.v1.OrderService'
backendRefs:
- name: order-service-grpc
port: 50051
# Method-based matching
- matches:
- method:
service: 'order.v1.OrderService'
method: 'CreateOrder'
backendRefs:
- name: order-write-service
port: 50051
# Header-based matching
- matches:
- headers:
- name: x-region
value: 'asia'
method:
service: 'order.v1.OrderService'
backendRefs:
- name: order-service-asia
port: 50051
GRPCRoute Traffic Splitting
apiVersion: gateway.networking.k8s.io/v1
kind: GRPCRoute
metadata:
name: grpc-canary
namespace: production
spec:
parentRefs:
- name: production-gateway
namespace: infra-gateway
hostnames:
- 'grpc.example.com'
rules:
- matches:
- method:
service: 'payment.v1.PaymentService'
backendRefs:
- name: payment-service-stable
port: 50051
weight: 95
- name: payment-service-canary
port: 50051
weight: 5
TLS Termination and Certificate Management
cert-manager Integration
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- http01:
gatewayHTTPRoute:
parentRefs:
- name: production-gateway
namespace: infra-gateway
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-example-tls
namespace: infra-gateway
spec:
secretName: wildcard-example-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- '*.example.com'
- 'example.com'
mTLS Configuration (Envoy Gateway BackendTLSPolicy)
apiVersion: gateway.networking.k8s.io/v1alpha3
kind: BackendTLSPolicy
metadata:
name: backend-mtls
namespace: production
spec:
targetRefs:
- group: ''
kind: Service
name: secure-backend
validation:
caCertificateRefs:
- name: backend-ca-cert
group: ''
kind: ConfigMap
hostname: secure-backend.production.svc.cluster.local
Envoy Gateway Rate Limiting
Global Rate Limiting
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
name: global-rate-limit
namespace: infra-gateway
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: production-gateway
rateLimit:
type: Global
global:
rules:
- clientSelectors:
- headers:
- name: x-api-key
type: Distinct
limit:
requests: 100
unit: Minute
- limit:
requests: 1000
unit: Minute
Per-Route Rate Limiting
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
name: api-rate-limit
namespace: production
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: api-route
rateLimit:
type: Global
global:
rules:
- clientSelectors:
- headers:
- name: x-user-tier
value: 'free'
limit:
requests: 10
unit: Minute
- clientSelectors:
- headers:
- name: x-user-tier
value: 'premium'
limit:
requests: 1000
unit: Minute
Cross-Namespace Routing with ReferenceGrant
# Allow HTTPRoutes in app-team-a namespace to reference
# Services in shared-services namespace
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-cross-ns-routing
namespace: shared-services
spec:
from:
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: app-team-a
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: app-team-b
to:
- group: ''
kind: Service
Migrating from Ingress to Gateway API
Using the ingress2gateway Tool
# Install ingress2gateway
go install github.com/kubernetes-sigs/ingress2gateway@latest
# Convert existing Ingress resources to Gateway API
ingress2gateway print \
--input-file existing-ingress.yaml \
--providers ingress-nginx \
--all-resources
# Convert directly from cluster
ingress2gateway print \
--providers ingress-nginx \
--all-resources \
--namespace production
Migration Strategy: Parallel Operation
# Existing Ingress (keep running)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: legacy-app-ingress
namespace: production
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: 'true'
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: app-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app-service
port:
number: 8080
---
# New Gateway API HTTPRoute (parallel deployment)
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: app-route
namespace: production
spec:
parentRefs:
- name: production-gateway
namespace: infra-gateway
hostnames:
- 'app.example.com'
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: app-service
port: 8080
Step-by-Step Migration Procedure
# Step 1: Verify Gateway API CRDs are installed
kubectl get crd gateways.gateway.networking.k8s.io
# Step 2: Install Envoy Gateway
helm install eg oci://docker.io/envoyproxy/gateway-helm \
--version v1.3.0 \
-n envoy-gateway-system --create-namespace
# Step 3: Create GatewayClass and Gateway
kubectl apply -f gateway-class.yaml
kubectl apply -f gateway.yaml
# Step 4: Convert existing Ingress to HTTPRoute
ingress2gateway print --providers ingress-nginx --all-resources > routes.yaml
# Step 5: Deploy converted HTTPRoutes (parallel operation)
kubectl apply -f routes.yaml
# Step 6: Switch DNS to new Gateway LoadBalancer
GATEWAY_IP=$(kubectl get gateway production-gateway -n infra-gateway \
-o jsonpath='{.status.addresses[0].value}')
echo "Update DNS A record to: $GATEWAY_IP"
# Step 7: Monitor traffic then remove legacy Ingress
kubectl delete ingress legacy-app-ingress -n production
Monitoring Setup
Prometheus Metrics Collection
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: envoy-gateway-metrics
namespace: envoy-gateway-system
spec:
selector:
matchLabels:
app.kubernetes.io/name: envoy
endpoints:
- port: metrics
interval: 15s
path: /stats/prometheus
---
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: gateway-api-alerts
namespace: monitoring
spec:
groups:
- name: gateway-api
rules:
- alert: GatewayHighErrorRate
expr: |
sum(rate(envoy_http_downstream_rq_xx{envoy_response_code_class="5"}[5m])) by (envoy_http_conn_manager_prefix)
/
sum(rate(envoy_http_downstream_rq_total[5m])) by (envoy_http_conn_manager_prefix)
> 0.05
for: 5m
labels:
severity: critical
annotations:
summary: 'Gateway error rate exceeds 5%'
- alert: GatewayHighLatency
expr: |
histogram_quantile(0.99,
sum(rate(envoy_http_downstream_rq_time_bucket[5m])) by (le, envoy_http_conn_manager_prefix)
) > 1000
for: 5m
labels:
severity: warning
annotations:
summary: 'Gateway p99 latency exceeds 1s'
Grafana Dashboard Queries
# Key Envoy metrics
# Requests per second
sum(rate(envoy_http_downstream_rq_total[5m])) by (envoy_http_conn_manager_prefix)
# Error rate (5xx)
sum(rate(envoy_http_downstream_rq_xx{envoy_response_code_class="5"}[5m]))
# Average response time
sum(rate(envoy_http_downstream_rq_time_sum[5m])) / sum(rate(envoy_http_downstream_rq_time_count[5m]))
# Active connections
envoy_http_downstream_cx_active
Operational Notes
1. Resource Limits Configuration
In production environments, you must configure resource limits for Envoy Proxy. Running with defaults can lead to OOM (Out of Memory) during traffic spikes.
2. Gateway Listener Limits
Adding too many listeners to a single Gateway causes the Envoy configuration to become bloated, increasing reload times. It is recommended to separate Gateways by domain or team.
3. ReferenceGrant Least Privilege
Cross-namespace references should only be allowed when necessary, and target namespaces and resources should be specified as precisely as possible.
4. HTTPRoute Priority
When multiple HTTPRoutes match the same path, you must understand the priority rules:
- Longest hostname wins
- Longest path wins
- Exact match takes precedence over PathPrefix
- For identical conditions, the earliest created resource wins
5. v1alpha2 Removal Handling
Gateway API v1.2 removed the v1alpha2 versions of GRPCRoute and ReferenceGrant. If you are using v1alpha2, you must migrate to v1.
Failure Cases and Recovery Procedures
Case 1: TLS Certificate Expiration
# Symptom: 503 errors, TLS handshake failures
# Diagnosis
kubectl get certificate -n infra-gateway
kubectl describe certificate wildcard-example-tls -n infra-gateway
# Recovery: Force cert-manager renewal
kubectl delete certificaterequest -n infra-gateway --all
kubectl annotate certificate wildcard-example-tls \
-n infra-gateway \
cert-manager.io/renew-before="720h" --overwrite
# Emergency: Manual certificate replacement
kubectl create secret tls wildcard-example-tls \
--cert=fullchain.pem --key=privkey.pem \
-n infra-gateway --dry-run=client -o yaml | kubectl apply -f -
Case 2: Gateway Controller Failure
# Symptom: New HTTPRoutes not being applied
# Diagnosis
kubectl get pods -n envoy-gateway-system
kubectl logs -n envoy-gateway-system deploy/envoy-gateway -f
# Check Envoy Proxy status
kubectl get pods -l app.kubernetes.io/name=envoy -A
# Recovery: Restart controller
kubectl rollout restart deployment/envoy-gateway -n envoy-gateway-system
# Verify Gateway status
kubectl get gateway production-gateway -n infra-gateway -o yaml
Case 3: Traffic Black Hole from Invalid HTTPRoute
# Symptom: All requests to a specific path return 404
# Diagnosis: Check HTTPRoute status
kubectl get httproute -A
kubectl describe httproute app-route -n production
# Check Accepted/ResolvedRefs conditions in status
# Cause: backendRef points to a non-existent service
# Recovery: Fix service name and reapply
kubectl apply -f corrected-httproute.yaml
# Verify Envoy config sync
kubectl exec -n envoy-gateway-system deploy/envoy-gateway -- \
curl -s localhost:19000/config_dump | python3 -m json.tool | head -100
Case 4: Rate Limiting Malfunction
# Symptom: Normal users receiving 429 Too Many Requests
# Diagnosis
kubectl get backendtrafficpolicy -A
kubectl describe backendtrafficpolicy global-rate-limit -n infra-gateway
# Check Redis-based global rate limiter status
kubectl logs -n envoy-gateway-system deploy/envoy-ratelimit
# Recovery: Temporarily remove rate limiting policy
kubectl delete backendtrafficpolicy global-rate-limit -n infra-gateway
# Reapply corrected policy after stabilization
kubectl apply -f corrected-rate-limit.yaml
Conclusion
Kubernetes Gateway API overcomes the limitations of Ingress and enables systematic role-based traffic management. With HTTPRoute and GRPCRoute both GA in v1.2, and the retirement of Ingress NGINX imminent, now is the ideal time to migrate.
Choosing Envoy Gateway as the implementation gives you access to powerful Envoy features like rate limiting, authentication, and global load balancing through the standardized Gateway API interface. Use a parallel operation strategy for safe migration, and always configure monitoring and alerting to ensure production stability.