Kubernetes Gateway API vs Ingress - When to Migrate and How
Ingress has been the standard for HTTP routing in Kubernetes since 2015. It works, but it’s showing its age. Advanced features require controller-specific annotations, there’s no native traffic splitting, and multi-team setups often become a mess of conflicting configurations.
Gateway API is the official successor - a complete redesign that addresses these limitations while remaining portable across implementations. It went GA in October 2023 with v1.0, and most major ingress controllers now support it.
This post compares both APIs, explains when migration makes sense, and provides practical migration patterns.
TL;DR
- Gateway API is the successor to Ingress, not a replacement for service meshes
- It’s GA since v1.0 (October 2023) with HTTPRoute, Gateway, and GatewayClass stable
- Key improvements: role-oriented design, native traffic splitting, header-based routing, cross-namespace support
- Migrate when you need features Ingress can’t provide natively
- Both can coexist - migrate incrementally
Code Repository: All code from this post is available at github.com/moabukar/blog-code/gateway-api-vs-ingress
The Problem with Ingress
Ingress was designed for simple HTTP routing. It handles the basics well:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: simple-ingress
spec:
ingressClassName: nginx
rules:
- host: app.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
But real-world requirements quickly exceed what Ingress can express natively:
Traffic splitting for canary deployments? Annotations.
# NGINX-specific - won't work with Traefik or HAProxy
metadata:
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "20"
Header-based routing? Annotations.
# Again, controller-specific
metadata:
annotations:
nginx.ingress.kubernetes.io/canary-by-header: "X-Canary"
Request/response header modification? You guessed it - annotations.
The result is configuration that’s:
- Not portable - switch controllers and rewrite everything
- Not discoverable - no schema, no validation, just strings
- Not composable - one Ingress per namespace, no cross-team sharing
What Gateway API Changes
Gateway API redesigns the model with three key resources:
┌─────────────────────────────────────────────────────────────────┐
│ GatewayClass │
│ (Managed by Infrastructure Provider) │
│ Defines controller + config │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Gateway │
│ (Managed by Cluster Operator) │
│ Defines listeners, ports, TLS, allowed routes │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ HTTPRoute / GRPCRoute │
│ (Managed by Application Developer) │
│ Defines routing rules, backends, traffic policies │
└─────────────────────────────────────────────────────────────────┘
This separation isn’t just aesthetic - it enables different teams to manage different layers without stepping on each other.
GatewayClass - The Infrastructure Layer
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: istio
spec:
controllerName: istio.io/gateway-controller
GatewayClass is like StorageClass for networking. It defines which controller handles Gateways of this class. Platform teams deploy this once; application teams reference it.
Gateway - The Cluster Operator Layer
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: shared-gateway
namespace: infra
spec:
gatewayClassName: istio
listeners:
- name: https
protocol: HTTPS
port: 443
hostname: "*.example.com"
tls:
mode: Terminate
certificateRefs:
- name: wildcard-cert
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
gateway-access: "true"
Key points:
- Listeners define what traffic the Gateway accepts
- allowedRoutes controls which namespaces can attach routes
- hostname can use wildcards, enabling multi-tenant setups
- The Gateway owner controls who can expose services through it
HTTPRoute - The Application Developer Layer
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: api-route
namespace: team-a
spec:
parentRefs:
- name: shared-gateway
namespace: infra
hostnames:
- "api.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /v1
backendRefs:
- name: api-v1
port: 80
weight: 90
- name: api-v2
port: 80
weight: 10
Application developers create HTTPRoutes in their namespace. They reference a Gateway (potentially in another namespace) and define routing rules. The Gateway owner already approved their namespace via allowedRoutes.
Feature Comparison
Here’s what each API supports natively (without annotations):
FEATURE INGRESS GATEWAY API
======= ======= ===========
Path-based routing ✓ ✓
Host-based routing ✓ ✓
TLS termination ✓ ✓
Traffic splitting (weights) ✗ (annotation) ✓
Header-based routing ✗ (annotation) ✓
Header modification ✗ (annotation) ✓
Request mirroring ✗ (annotation) ✓
Cross-namespace routing ✗ ✓
Role-based resource model ✗ ✓
gRPC-native routing ✗ ✓ (GRPCRoute)
TCP/UDP routing ✗ ✓ (TCPRoute/UDPRoute)
Multiple controllers per cluster ✗ (awkward) ✓
Native Traffic Splitting
This is often the killer feature that drives migration. In Ingress, canary deployments require controller-specific annotations. In Gateway API, it’s a first-class concept:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: canary-route
spec:
parentRefs:
- name: production-gateway
hostnames:
- "app.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: app-stable
port: 80
weight: 95
- name: app-canary
port: 80
weight: 5
Shift traffic by changing weights. No annotations, no controller-specific syntax.
Header-Based Routing
Route specific users to canary based on headers:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: header-canary
spec:
parentRefs:
- name: production-gateway
hostnames:
- "app.example.com"
rules:
# Internal testers go to canary
- matches:
- headers:
- name: X-Internal-Tester
value: "true"
backendRefs:
- name: app-canary
port: 80
# Everyone else gets stable
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: app-stable
port: 80
Cross-Namespace Routing
In large organisations, platform teams manage ingress infrastructure while application teams manage their services. Gateway API supports this natively.
Platform team (infra namespace):
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: shared-gateway
namespace: infra
spec:
gatewayClassName: envoy
listeners:
- name: https
protocol: HTTPS
port: 443
hostname: "*.prod.example.com"
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
environment: production
Team A (team-a namespace):
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: team-a-api
namespace: team-a
labels:
environment: production
spec:
parentRefs:
- name: shared-gateway
namespace: infra
hostnames:
- "api.prod.example.com"
rules:
- backendRefs:
- name: api-service
port: 80
Team B (team-b namespace):
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: team-b-dashboard
namespace: team-b
labels:
environment: production
spec:
parentRefs:
- name: shared-gateway
namespace: infra
hostnames:
- "dashboard.prod.example.com"
rules:
- backendRefs:
- name: dashboard-service
port: 80
Teams don’t need to coordinate or share manifests. The Gateway controls access via namespace selectors.
Request/Response Modification
Header manipulation is native in Gateway API:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: modified-route
spec:
parentRefs:
- name: api-gateway
rules:
- matches:
- path:
type: PathPrefix
value: /api
filters:
- type: RequestHeaderModifier
requestHeaderModifier:
add:
- name: X-Request-ID
value: "${request_id}"
set:
- name: X-Forwarded-Proto
value: https
remove:
- X-Debug-Header
- type: ResponseHeaderModifier
responseHeaderModifier:
add:
- name: X-Frame-Options
value: DENY
- name: Strict-Transport-Security
value: "max-age=31536000; includeSubDomains"
backendRefs:
- name: api-service
port: 80
When to Migrate
Migrate when:
- You need traffic splitting for canary/blue-green deployments
- Multiple teams share ingress infrastructure
- You’re tired of controller-specific annotations
- You need gRPC routing (GRPCRoute is cleaner than Ingress hacks)
- You want header-based routing without annotations
- You’re deploying new clusters and want to start clean
Stay with Ingress when:
- Your current setup works and you don’t need advanced features
- Your ingress controller doesn’t support Gateway API yet
- You have heavy investment in Ingress tooling (GitOps, policies)
- You’re running older Kubernetes (Gateway API needs 1.24+)
Reality check: Both can coexist. You don’t need to migrate everything at once. Run Gateway API for new services, keep Ingress for existing ones.
Migration Strategy
Step 1: Check Controller Support
Most major controllers support Gateway API:
CONTROLLER GATEWAY API STATUS NOTES
========== ================== =====
NGINX Gateway Fabric GA Separate product from NGINX Ingress
Istio GA Full support since 1.16
Envoy Gateway GA CNCF project
Traefik GA Since v3.0
Contour GA Full support
Cilium GA With Cilium Gateway API
Kong GA Kong Gateway Operator
AWS ALB Controller Partial Basic support
GKE Gateway Controller GA GCP native
Step 2: Install Gateway API CRDs
Gateway API resources are CRDs, not built into Kubernetes:
# Install standard channel (stable resources)
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.0/standard-install.yaml
# Or include experimental resources (TLSRoute, TCPRoute, UDPRoute)
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.0/experimental-install.yaml
Step 3: Deploy a GatewayClass
Your controller provides this, or you create one:
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: nginx
spec:
controllerName: gateway.nginx.org/nginx-gateway-controller
Step 4: Create a Gateway
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: main-gateway
namespace: infra
spec:
gatewayClassName: nginx
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: All
- name: https
protocol: HTTPS
port: 443
tls:
mode: Terminate
certificateRefs:
- name: tls-secret
allowedRoutes:
namespaces:
from: All
Step 5: Convert Ingress to HTTPRoute
Before (Ingress):
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: api.example.com
http:
paths:
- path: /v1
pathType: Prefix
backend:
service:
name: api-v1
port:
number: 80
After (HTTPRoute):
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: api-route
spec:
parentRefs:
- name: main-gateway
namespace: infra
hostnames:
- "api.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /v1
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /
backendRefs:
- name: api-v1
port: 80
Step 6: Run Both, Then Cut Over
During migration, both Ingress and Gateway API can route traffic. Test the new Gateway API routes with a subset of traffic or internal DNS, then switch production DNS once validated.
ReferenceGrant - Cross-Namespace Backend Access
By default, HTTPRoutes can only reference backends in the same namespace. To reference backends in other namespaces, the target namespace must grant permission:
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-infra-routes
namespace: backend-services
spec:
from:
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: infra
to:
- group: ""
kind: Service
This allows HTTPRoutes in the infra namespace to reference Services in backend-services.
Terraform Example
Deploy Gateway API infrastructure with Terraform:
# Install Gateway API CRDs
resource "kubernetes_manifest" "gateway_api_crds" {
manifest = yamldecode(file("${path.module}/gateway-api-crds.yaml"))
}
# GatewayClass
resource "kubernetes_manifest" "gateway_class" {
manifest = {
apiVersion = "gateway.networking.k8s.io/v1"
kind = "GatewayClass"
metadata = {
name = "nginx"
}
spec = {
controllerName = "gateway.nginx.org/nginx-gateway-controller"
}
}
}
# Gateway
resource "kubernetes_manifest" "main_gateway" {
manifest = {
apiVersion = "gateway.networking.k8s.io/v1"
kind = "Gateway"
metadata = {
name = "main-gateway"
namespace = "infra"
}
spec = {
gatewayClassName = "nginx"
listeners = [
{
name = "https"
protocol = "HTTPS"
port = 443
hostname = "*.example.com"
tls = {
mode = "Terminate"
certificateRefs = [{
name = "wildcard-cert"
}]
}
allowedRoutes = {
namespaces = {
from = "Selector"
selector = {
matchLabels = {
"gateway-access" = "true"
}
}
}
}
}
]
}
}
}
Troubleshooting
Route Not Attaching to Gateway
Check the HTTPRoute status:
kubectl describe httproute api-route
Look for conditions:
Status:
Parents:
- ControllerName: gateway.nginx.org/nginx-gateway-controller
ParentRef:
Name: main-gateway
Namespace: infra
Conditions:
- Type: Accepted
Status: "False"
Reason: NotAllowedByListeners
Common causes:
- Namespace not allowed by Gateway’s
allowedRoutes - Hostname doesn’t match Gateway’s listener hostname
- Missing ReferenceGrant for cross-namespace backends
Gateway Not Ready
kubectl describe gateway main-gateway -n infra
Check for:
- Missing TLS secrets
- Port conflicts
- Controller not running
Verify Routes Are Programmed
# Check Gateway status
kubectl get gateway main-gateway -n infra -o yaml
# Check HTTPRoute status
kubectl get httproute -A -o wide
What’s Next for Gateway API
Gateway API is actively evolving:
- GRPCRoute - GA since v1.1.0 for native gRPC routing
- Service Mesh (GAMMA) - East-west traffic management, GA since v1.1.0
- BackendTLSPolicy - Configure TLS to backends
- Timeouts - Native request timeout configuration
- Gateway infrastructure - Cloud-specific infrastructure attachment
The API is designed for extension. Implementations can add custom policies while maintaining portability for core features.
Conclusion
Gateway API isn’t just “Ingress v2” - it’s a fundamental redesign that acknowledges how organisations actually operate Kubernetes networking. The role-oriented model, native traffic management, and cross-namespace support solve real problems that Ingress can only address through controller-specific hacks.
If your current Ingress setup works and you don’t need advanced features, there’s no rush to migrate. But for new deployments or when you hit Ingress limitations, Gateway API is the clear path forward.
Start with one service, validate the workflow, then expand. Both APIs coexist peacefully.