Nginx Ingress Migration: Your Complete 2025 Guide
Introduction: Why You Need to Migrate Now
If you’re running Kubernetes workloads with NGINX Ingress Controller, you need to act fast. The official NGINX Ingress Controller retirement is scheduled for March 2026, marking the end of security updates, bug fixes, and community support. With approximately four months remaining, organizations worldwide are rushing to implement migration strategies.
This comprehensive guide will walk you through migrating from NGINX Ingress to the Kubernetes Gateway API using the ingress2gateway tool, providing you with practical examples, common pitfalls, and production-ready strategies.
Understanding the NGINX Ingress Sunset {#understanding-the-sunset}
Why is NGINX Ingress Being Retired?
The NGINX Ingress Controller was originally created as a reference implementation to demonstrate the Ingress API concept. Nobody anticipated it would become the de facto standard for Kubernetes traffic routing. However, several factors led to its retirement:
- Maintainer Burnout: The project relied heavily on 1-2 maintainers working in their spare time
- Security Concerns: The flexibility to inject arbitrary NGINX configuration through annotations became a security liability
- Scalability Issues: Community contribution didn’t scale with massive adoption
- Maintenance Nightmare: What was once a feature became increasingly difficult to maintain
Timeline and Impact
- Current Status: Updates are released on a best-effort basis, limited to critical security fixes
- March 2026: Complete end of support, no security patches, no bug fixes
- Impact: Approximately 40-50% of Kubernetes clusters use NGINX Ingress Controller
This affects millions of production workloads worldwide.
What is Kubernetes Gateway API? {#what-is-gateway-api}
The Kubernetes Gateway API is the next-generation standard for service networking in Kubernetes, officially supported by SIG Network. It’s not just an Ingress replacement – it’s a complete reimagining of how traffic routing should work.
Key Components
Gateway API introduces three primary resources:
# GatewayClass - Infrastructure template (managed by cluster admins)
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: nginx-gateway-class
spec:
controllerName: nginx-gateway.io/controller
---
# Gateway - Actual load balancer instance
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: production-gateway
namespace: default
spec:
gatewayClassName: nginx-gateway-class
listeners:
- name: http
protocol: HTTP
port: 80
- name: https
protocol: HTTPS
port: 443
tls:
mode: Terminate
certificateRefs:
- name: example-tls-secret
---
# HTTPRoute - Traffic routing rules (managed by developers)
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: example-route
namespace: default
spec:
parentRefs:
- name: production-gateway
hostnames:
- "example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /api
backendRefs:
- name: api-service
port: 8080
Why Gateway API Over Traditional Ingress? {#why-gateway-api}
Comparison: Ingress vs Gateway API
| Feature | Ingress | Gateway API |
|---|---|---|
| Role Separation | Single resource for everything | Separate resources for infra/dev concerns |
| Protocol Support | HTTP/HTTPS only | HTTP, HTTPS, TCP, UDP, gRPC |
| Routing Capabilities | Basic path/host routing | Advanced: traffic splitting, header matching, mirroring |
| Extensibility | Vendor-specific annotations | Standardized extension points |
| Cross-Namespace Routing | Not supported | Native support with explicit grants |
| Traffic Weighting | Via annotations (inconsistent) | Native built-in feature |
| Conformance Testing | No standard | Comprehensive conformance reports |
Key Advantages of Gateway API
- Role-Oriented Design
- Infrastructure providers manage GatewayClass
- Cluster operators manage Gateway
- Application developers manage Routes
- Clear separation of concerns and responsibilities
- Enhanced Security
- Developers can’t accidentally inject gateway-level configuration
- Explicit cross-namespace grants prevent unauthorized access
- No arbitrary annotation-based configuration injection
- Portability
- Switching between implementations (NGINX → Envoy → Traefik) is simpler
- Common core API across all providers
- Reduced vendor lock-in
- Advanced Features Built-In
- Traffic splitting for canary deployments
- Request/response header modification
- Request mirroring for testing
- URL rewriting and redirection
- Timeout and retry policies
- Future-Proof
- Active development by SIG Network
- Regular updates and new features
- Growing ecosystem support
Prerequisites for Migration {#prerequisites}
Before starting your migration, ensure you have:
Required Components
- Kubernetes Cluster (v1.23+)
kubectl version --short - Gateway API CRDs installed
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/standard-install.yaml - Gateway Controller (choose one):
- Envoy Gateway
- NGINX Gateway Fabric
- Traefik
- Cilium Gateway API
- Istio Gateway
- Access to existing Ingress resources
kubectl get ingress --all-namespaces
Recommended Tools
kubectl(latest version)Go(1.20+) for installing ingress2gatewayjqfor JSON processingyqfor YAML manipulation
Environment Check
# Check current Ingress resources
kubectl get ingress --all-namespaces -o wide
# Check Ingress controller version
kubectl get pods -n ingress-nginx -o yaml | grep image:
# Verify Gateway API CRDs
kubectl get crd | grep gateway
# Expected output:
# gatewayclasses.gateway.networking.k8s.io
# gateways.gateway.networking.k8s.io
# httproutes.gateway.networking.k8s.io
# referencegrants.gateway.networking.k8s.io
Installing ingress2gateway Tool {#installing-ingress2gateway}
The ingress2gateway tool is an official Kubernetes SIG Network project that automates conversion from Ingress to Gateway API resources.
Installation Methods
Method 1: Using Go (Recommended)
# Install latest version
go install github.com/kubernetes-sigs/ingress2gateway@latest
# Verify installation
ingress2gateway version
# The binary will be at:
# $(go env GOPATH)/bin/ingress2gateway
Method 2: Download Binary
# For Linux AMD64
wget https://github.com/kubernetes-sigs/ingress2gateway/releases/download/v0.4.0/ingress2gateway_linux_amd64.tar.gz
tar -xzf ingress2gateway_linux_amd64.tar.gz
sudo mv ingress2gateway /usr/local/bin/
chmod +x /usr/local/bin/ingress2gateway
# For macOS
wget https://github.com/kubernetes-sigs/ingress2gateway/releases/download/v0.4.0/ingress2gateway_darwin_amd64.tar.gz
tar -xzf ingress2gateway_darwin_amd64.tar.gz
sudo mv ingress2gateway /usr/local/bin/
Method 3: Homebrew (macOS)
# Add tap
brew tap kubernetes-sigs/ingress2gateway
# Install
brew install ingress2gateway
Verify Installation
# Check version
ingress2gateway version
# View help
ingress2gateway --help
# List supported providers
ingress2gateway print --help | grep providers
Step-by-Step Migration Process {#migration-process}
Phase 1: Assessment and Planning
Step 1: Inventory Your Ingress Resources
# List all Ingress resources
kubectl get ingress --all-namespaces -o yaml > current-ingress-backup.yaml
# Count Ingress resources per namespace
kubectl get ingress --all-namespaces --no-headers | awk '{print $1}' | sort | uniq -c
# Identify Ingress classes
kubectl get ingress --all-namespaces -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.spec.ingressClassName}{"\n"}{end}' | sort -u
# Check annotations (these may need special handling)
kubectl get ingress --all-namespaces -o json | jq -r '.items[].metadata.annotations | keys[]' | sort -u
Step 2: Analyze Complex Configurations
# Find Ingress resources with TLS
kubectl get ingress --all-namespaces -o json | jq '.items[] | select(.spec.tls != null) | {namespace: .metadata.namespace, name: .metadata.name}'
# Find Ingress with multiple rules
kubectl get ingress --all-namespaces -o json | jq '.items[] | select((.spec.rules | length) > 1) | {namespace: .metadata.namespace, name: .metadata.name}'
# Identify custom annotations
kubectl get ingress --all-namespaces -o json | jq -r '.items[].metadata.annotations | to_entries[] | select(.key | startswith("nginx.ingress.kubernetes.io")) | .key' | sort -u
Phase 2: Test Conversion
Step 3: Convert Single Ingress (Dry Run)
# Convert specific Ingress to stdout
ingress2gateway print \
--namespace=production \
--ingress-name=my-app-ingress \
--providers=ingress-nginx
# Save to file for review
ingress2gateway print \
--namespace=production \
--providers=ingress-nginx \
> converted-gateway-resources.yaml
Step 4: Convert All Ingress in Namespace
# Convert all Ingress resources in a namespace
ingress2gateway print \
--namespace=production \
--providers=ingress-nginx \
--output=yaml > production-gateways.yaml
# Review the output
cat production-gateways.yaml
# Check for any warnings or errors in conversion
grep -i "unsupported\|warning\|error" production-gateways.yaml
Step 5: Validate Generated Resources
# Validate YAML syntax
kubectl apply --dry-run=client -f production-gateways.yaml
# Check for resource conflicts
kubectl apply --dry-run=server -f production-gateways.yaml
# Validate Gateway API resources
kubectl apply --dry-run=server -f production-gateways.yaml 2>&1 | grep -i "error\|invalid"
Phase 3: Staging Deployment
Step 6: Create Test Environment
# Create migration testing namespace
kubectl create namespace gateway-migration-test
# Copy secrets and configmaps
kubectl get secrets -n production -o yaml | \
sed 's/namespace: production/namespace: gateway-migration-test/' | \
kubectl apply -f -
# Deploy sample application
kubectl apply -n gateway-migration-test -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-app
spec:
replicas: 2
selector:
matchLabels:
app: test-app
template:
metadata:
labels:
app: test-app
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: test-app-service
spec:
selector:
app: test-app
ports:
- port: 80
targetPort: 80
EOF
Step 7: Deploy Gateway Resources
# Apply converted Gateway API resources to test namespace
ingress2gateway print \
--namespace=gateway-migration-test \
--providers=ingress-nginx | \
kubectl apply -f -
# Verify Gateway creation
kubectl get gateways -n gateway-migration-test
kubectl get httproutes -n gateway-migration-test
# Check Gateway status
kubectl describe gateway production-gateway -n gateway-migration-test
Step 8: Test Connectivity
# Get Gateway external IP
GATEWAY_IP=$(kubectl get svc -n gateway-migration-test \
-l gateway.networking.k8s.io/gateway-name=production-gateway \
-o jsonpath='{.items[0].status.loadBalancer.ingress[0].ip}')
echo "Gateway IP: $GATEWAY_IP"
# Test HTTP endpoint
curl -H "Host: test-app.example.com" http://$GATEWAY_IP/
# Test with verbose output
curl -v -H "Host: test-app.example.com" http://$GATEWAY_IP/
# Load testing (optional)
ab -n 1000 -c 10 -H "Host: test-app.example.com" http://$GATEWAY_IP/
Phase 4: Production Migration
Step 9: Parallel Deployment Strategy
The safest approach is running both Ingress and Gateway API simultaneously:
# Keep existing Ingress running
kubectl get ingress -n production
# Deploy Gateway API resources alongside
ingress2gateway print \
--namespace=production \
--providers=ingress-nginx | \
kubectl apply -f -
# Both systems are now running in parallel
# Ingress: Old traffic
# Gateway: New traffic (if DNS/LB updated)
Step 10: Gradual Traffic Migration
# Option A: DNS-based migration
# Update DNS A record gradually:
# - 10% traffic to Gateway IP
# - Monitor for 24 hours
# - Increase to 50%
# - Monitor for 24 hours
# - 100% to Gateway
# Option B: LoadBalancer IP preservation
# If you need to keep the same IP:
# Get current Ingress LoadBalancer IP
CURRENT_IP=$(kubectl get svc -n ingress-nginx ingress-nginx-controller \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}')
# Update Gateway service to use same IP
kubectl patch svc -n production gateway-production-gateway \
-p "{\"spec\":{\"loadBalancerIP\":\"$CURRENT_IP\"}}"
# Delete old Ingress LoadBalancer
kubectl delete svc -n ingress-nginx ingress-nginx-controller
Step 11: Monitor and Validate
# Monitor Gateway logs
kubectl logs -n production -l gateway.networking.k8s.io/gateway-name=production-gateway -f
# Check HTTPRoute status
kubectl get httproutes -n production -o wide
# Verify backend connectivity
kubectl get httproutes -n production -o json | \
jq '.items[] | {name: .metadata.name, status: .status.parents[0].conditions}'
# Monitor application logs
kubectl logs -n production -l app=your-app --tail=100 -f
# Check for errors
kubectl get events -n production --sort-by='.lastTimestamp' | grep -i error
Phase 5: Cleanup
Step 12: Remove Old Ingress Resources
⚠️ Warning: Only do this after confirming Gateway is working correctly!
# Backup before deletion
kubectl get ingress --all-namespaces -o yaml > ingress-final-backup.yaml
# Delete specific Ingress
kubectl delete ingress my-app-ingress -n production
# Or delete all Ingress in namespace (use with caution)
kubectl delete ingress --all -n production
# Uninstall NGINX Ingress Controller
helm uninstall ingress-nginx -n ingress-nginx
# Remove namespace
kubectl delete namespace ingress-nginx
Real-World Migration Examples {#real-world-examples}
Example 1: Simple HTTP Ingress Migration
Original Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: simple-app-ingress
namespace: production
annotations:
kubernetes.io/ingress.class: nginx
spec:
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app-service
port:
number: 80
Convert with ingress2gateway:
ingress2gateway print \
--namespace=production \
--ingress-name=simple-app-ingress \
--providers=ingress-nginx
Resulting Gateway API Resources:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: simple-app-ingress-gateway
namespace: production
spec:
gatewayClassName: nginx
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: Same
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: simple-app-ingress-app-example-com
namespace: production
spec:
parentRefs:
- name: simple-app-ingress-gateway
hostnames:
- "app.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: app-service
port: 80
Example 2: HTTPS with TLS Termination
Original Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: secure-app-ingress
namespace: production
spec:
ingressClassName: nginx
tls:
- hosts:
- secure.example.com
secretName: tls-secret
rules:
- host: secure.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-service
port:
number: 8080
- path: /web
pathType: Prefix
backend:
service:
name: web-service
port:
number: 3000
Converted Gateway API:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: secure-app-gateway
namespace: production
spec:
gatewayClassName: nginx
listeners:
- name: https
protocol: HTTPS
port: 443
hostname: "secure.example.com"
tls:
mode: Terminate
certificateRefs:
- name: tls-secret
kind: Secret
allowedRoutes:
namespaces:
from: Same
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: secure-app-route
namespace: production
spec:
parentRefs:
- name: secure-app-gateway
sectionName: https
hostnames:
- "secure.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /api
backendRefs:
- name: api-service
port: 8080
- matches:
- path:
type: PathPrefix
value: /web
backendRefs:
- name: web-service
port: 3000
Example 3: Advanced Routing with Header Matching
Original Ingress (with annotations):
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: advanced-routing
namespace: production
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-by-header: "X-Canary"
nginx.ingress.kubernetes.io/canary-by-header-value: "true"
spec:
ingressClassName: nginx
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-v2-service
port:
number: 8080
Equivalent Gateway API (manual enhancement needed):
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: canary-routing
namespace: production
spec:
parentRefs:
- name: api-gateway
hostnames:
- "api.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /
headers:
- name: X-Canary
value: "true"
backendRefs:
- name: api-v2-service
port: 8080
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: api-v1-service
port: 8080
Example 4: Multi-Namespace Routing
Gateway in infra namespace:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: shared-gateway
namespace: infrastructure
spec:
gatewayClassName: nginx
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: All # Allow routes from any namespace
---
# ReferenceGrant to allow cross-namespace access
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-apps-to-gateway
namespace: infrastructure
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: gateway.networking.k8s.io
kind: Gateway
name: shared-gateway
HTTPRoute in application namespace:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: team-a-route
namespace: app-team-a
spec:
parentRefs:
- name: shared-gateway
namespace: infrastructure # Cross-namespace reference
hostnames:
- "team-a.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: team-a-service
port: 80
Common Migration Pitfalls and Solutions {#common-pitfalls}
Pitfall 1: IP Address Changes
Problem: New Gateway LoadBalancer gets a different IP than Ingress.
Solution:
# Option 1: Preserve existing IP
OLD_IP=$(kubectl get svc ingress-nginx-controller -n ingress-nginx \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}')
kubectl patch svc gateway-svc -n production \
-p "{\"spec\":{\"loadBalancerIP\":\"$OLD_IP\"}}"
# Option 2: Update DNS records
# Update your DNS A records to point to new Gateway IP
# Option 3: Use MetalLB IP pool
kubectl annotate svc gateway-svc -n production \
metallb.universe.tf/address-pool=production-pool
Pitfall 2: Namespace Scoping Issues
Problem: HTTPRoute not attaching to Gateway due to incorrect allowedRoutes.
Error Message:
HTTPRoute "my-route" not accepted by Gateway "my-gateway":
cross-namespace route not allowed
Solution:
# Fix 1: Update Gateway allowedRoutes
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: my-gateway
namespace: infrastructure
spec:
gatewayClassName: nginx
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: All # Or use Selector for specific namespaces
---
# Fix 2: Create ReferenceGrant
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: cross-namespace-grant
namespace: infrastructure
spec:
from:
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: application
to:
- group: gateway.networking.k8s.io
kind: Gateway
Pitfall 3: TLS Secret Not Found
Problem: Gateway can’t find TLS certificate secret.
Error Message:
Gateway "my-gateway" listener "https" condition "ResolvedRefs"
is False: secret "tls-cert" not found
Solution:
# Fix 1: Copy secret to Gateway namespace
kubectl get secret tls-cert -n app-namespace -o yaml | \
sed 's/namespace: app-namespace/namespace: gateway-namespace/' | \
kubectl apply -f -
# Fix 2: Create ReferenceGrant for cross-namespace secret access
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-gateway-to-secret
namespace: app-namespace
spec:
from:
- group: gateway.networking.k8s.io
kind: Gateway
namespace: gateway-namespace
to:
- group: ""
kind: Secret
name: tls-cert
EOF
Pitfall 4: Unsupported Annotations
Problem: NGINX Ingress annotations not translating to Gateway API.
Common Unsupported Annotations:
nginx.ingress.kubernetes.io/rewrite-targetnginx.ingress.kubernetes.io/auth-urlnginx.ingress.kubernetes.io/rate-limitnginx.ingress.kubernetes.io/cors-*
Solution:
# Use Gateway API-native features or implementation-specific policies
# Example: URL Rewrite (depends on implementation)
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: rewrite-example
spec:
parentRefs:
- name: my-gateway
rules:
- matches:
- path:
type: PathPrefix
value: /old-path
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /new-path
backendRefs:
- name: my-service
port: 80
# Example: Rate Limiting (implementation-specific)
# For Envoy Gateway:
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
name: rate-limit-policy
spec:
targetRef:
group: gateway.networking.k8s.io
kind: HTTPRoute
name: my-route
rateLimit:
type: Global
global:
rules:
- limit:
requests: 100
unit: Minute
Pitfall 5: Default Backend Conversion
Problem: Ingress defaultBackend not converting properly.
Original Ingress:
spec:
defaultBackend:
service:
name: default-404-service
port:
number: 80
Solution:
# Create catch-all HTTPRoute
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: default-backend-route
spec:
parentRefs:
- name: my-gateway
# No hostnames = matches all
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: default-404-service
port: 80
Pitfall 6: MetalLB Controller Restart Required
Problem: Using MetalLB and IP addresses not assigning correctly.
Solution:
# Restart MetalLB controller after Gateway creation
kubectl rollout restart deployment controller -n metallb-system
# Verify IP assignment
kubectl get svc -n production -o wide | grep gateway
# Check MetalLB logs
kubectl logs -n metallb-system -l app=metallb,component=controller
Pitfall 7: Gateway Not Ready
Problem: Gateway status shows “Not Ready” or “No addresses”.
Diagnosis:
# Check Gateway status
kubectl describe gateway my-gateway -n production
# Check GatewayClass
kubectl get gatewayclass
# Verify controller is running
kubectl get pods -n gateway-system
# Check controller logs
kubectl logs -n gateway-system deployment/gateway-controller
Common Fixes:
# Fix 1: Ensure GatewayClass exists and references correct controller
kubectl get gatewayclass -o yaml
# Fix 2: Check if controller has proper RBAC permissions
kubectl get clusterrole gateway-controller -o yaml
# Fix 3: Verify cloud provider load balancer support
# For on-prem, ensure MetalLB or similar is installed
Testing and Validation Strategies {#testing-strategies}
Pre-Migration Testing Checklist
#!/bin/bash
# migration-test.sh - Comprehensive migration testing script
NAMESPACE="production"
GATEWAY_NAME="production-gateway"
echo "=== Gateway Migration Testing ==="
# Test 1: Verify Gateway is ready
echo "Test 1: Gateway Status"
kubectl get gateway $GATEWAY_NAME -n $NAMESPACE -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}'
# Test 2: Check HTTPRoutes attachment
echo "Test 2: HTTPRoute Attachment"
kubectl get httproutes -n $NAMESPACE -o json | \
jq -r '.items[] | select(.spec.parentRefs[0].name=="'$GATEWAY_NAME'") | .metadata.name'
# Test 3: Verify backend services are healthy
echo "Test 3: Backend Services"
kubectl get httproutes -n $NAMESPACE -o json | \
jq -r '.items[].spec.rules[].backendRefs[].name' | \
while read svc; do
kubectl get svc $svc -n $NAMESPACE &> /dev/null && echo "$svc: OK" || echo "$svc: MISSING"
done
# Test 4: DNS resolution
echo "Test 4: DNS Resolution"
kubectl get httproutes -n $NAMESPACE -o json | \
jq -r '.items[].spec.hostnames[]' | \
while read host; do
dig +short $host | grep -q "." && echo "$host: Resolves" || echo "$host: No DNS"
done
# Test 5: HTTP connectivity
echo "Test 5: HTTP Connectivity"
GATEWAY_IP=$(kubectl get svc -n $NAMESPACE \
-l gateway.networking.k8s.io/gateway-name=$GATEWAY_NAME \
-o jsonpath='{.items[0].status.loadBalancer.ingress[0].ip}')
kubectl get httproutes -n $NAMESPACE -o json | \
jq -r '.items[].spec.hostnames[]' | \
while read host; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Host: $host" http://$GATEWAY_IP/)
echo "$host: HTTP $STATUS"
done
echo "=== Testing Complete ==="
Load Testing
# Install Apache Bench
sudo apt-get install apache2-utils -y
# Basic load test
GATEWAY_IP=$(kubectl get svc -n production gateway-svc \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}')
ab -n 10000 -c 100 -H "Host: app.example.com" http://$GATEWAY_IP/
# Advanced load test with different endpoints
for path in / /api /health; do
echo "Testing $path"
ab -n 1000 -c 50 -H "Host: app.example.com" http://$GATEWAY_IP$path
done
Performance Comparison
#!/bin/bash
# compare-performance.sh
echo "Testing Ingress Performance..."
ab -n 10000 -c 100 http://ingress-endpoint/ > ingress-results.txt
echo "Testing Gateway Performance..."
ab -n 10000 -c 100 http://gateway-endpoint/ > gateway-results.txt
echo "=== Comparison ==="
echo "Ingress:"
grep "Requests per second" ingress-results.txt
echo "Gateway:"
grep "Requests per second" gateway-results.txt
Monitoring During Migration
# prometheus-servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: gateway-metrics
namespace: monitoring
spec:
selector:
matchLabels:
gateway.networking.k8s.io/gateway-name: production-gateway
endpoints:
- port: metrics
interval: 30s
---
# grafana-dashboard-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: gateway-dashboard
namespace: monitoring
data:
dashboard.json: |
{
"dashboard": {
"title": "Gateway Migration Dashboard",
"panels": [
{
"title": "Request Rate",
"targets": [
{
"expr": "rate(gateway_api_http_requests_total[5m])"
}
]
},
{
"title": "Response Times",
"targets": [
{
"expr": "histogram_quantile(0.95, rate(gateway_api_http_request_duration_seconds_bucket[5m]))"
}
]
},
{
"title": "Error Rate",
"targets": [
{
"expr": "rate(gateway_api_http_requests_total{code=~\"5..\"}[5m])"
}
]
}
]
}
}
Alternative Ingress Controllers {#alternatives}
If you’re not ready for Gateway API, consider these NGINX Ingress alternatives:
Option 1: F5 NGINX Ingress Controller
Pros:
- Similar annotations to community NGINX Ingress
- Defined migration path from community version
- Commercial support available with NGINX Plus
Cons:
- Doesn’t support Gateway API (need NGINX Gateway Fabric separately)
- Commercial features require licensing
Installation:
# Add F5 NGINX Helm repo
helm repo add nginx-stable https://helm.nginx.com/stable
helm repo update
# Install
helm install nginx-ingress nginx-stable/nginx-ingress \
--namespace nginx-ingress \
--create-namespace
Option 2: Traefik Proxy
Pros:
- Supports both Ingress and Gateway API
- NGINX Ingress annotation compatibility layer
- Active development and community
Cons:
- Different configuration paradigm
- Some features require Traefik-specific CRDs
Installation:
# Add Traefik Helm repo
helm repo add traefik https://helm.traefik.io/traefik
helm repo update
# Install with NGINX annotation support
helm install traefik traefik/traefik \
--namespace traefik \
--create-namespace \
--set providers.kubernetesIngress.enabled=true \
--set providers.kubernetesGateway.enabled=true
Option 3: Envoy Gateway
Pros:
- Built natively for Gateway API
- High performance (CNCF project)
- Modern architecture
- Excellent for cloud-native applications
Cons:
- No native Ingress support
- Requires full Gateway API migration
- Newer project (less mature than others)
Installation:
# Install Envoy Gateway
helm install envoy-gateway oci://docker.io/envoyproxy/gateway-helm \
--namespace envoy-gateway-system \
--create-namespace
Option 4: Cilium Gateway API
Pros:
- Integrated with Cilium CNI
- eBPF-based (excellent performance)
- Native Gateway API support
- Advanced network policies
Cons:
- Requires Cilium as CNI
- Complex if not already using Cilium
Installation:
# If Cilium is already installed
cilium install --set gatewayAPI.enabled=true
# Create GatewayClass
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: cilium
spec:
controllerName: io.cilium/gateway-controller
EOF
Comparison Matrix
| Controller | Gateway API | Ingress | Performance | Maturity | License |
|---|---|---|---|---|---|
| NGINX Gateway Fabric | ✅ Yes | ❌ No | High | Medium | Apache 2.0 |
| F5 NGINX Ingress | ❌ No | ✅ Yes | High | High | Apache 2.0 |
| Traefik | ✅ Yes | ✅ Yes | Medium-High | High | MIT |
| Envoy Gateway | ✅ Yes | ❌ No | Very High | Medium | Apache 2.0 |
| Cilium | ✅ Yes | ✅ Yes | Very High | High | Apache 2.0 |
| Istio | ✅ Yes | ✅ Yes | High | Very High | Apache 2.0 |
| Kong | ✅ Yes | ✅ Yes | High | High | Apache 2.0 |
Migration Best Practices {#best-practices}
1. Start Early, Migrate Gradually
# Create migration phases
PHASES=(
"development"
"staging"
"production-canary"
"production-full"
)
for phase in "${PHASES[@]}"; do
echo "Migrating $phase environment..."
# Your migration commands here
# Wait and validate before proceeding
done
2. Document Everything
# migration-log.md
## Migration Timeline
- 2025-01-10: Assessment complete, 47 Ingress resources identified
- 2025-01-15: Test conversion successful in dev environment
- 2025-01-20: Staging migration complete, monitoring for 1 week
- 2025-01-27: Production migration Phase 1 (20% traffic)
## Known Issues
1. Custom annotation `nginx.ingress.kubernetes.io/custom-header`
- Not supported in Gateway API
- Manual HTTPRoute filter required
- Issue #123 opened with vendor
## Rollback Procedures
1. Restore Ingress from backup: `kubectl apply -f ingress-backup.yaml`
2. Update DNS to old IPs
3. Delete Gateway resources
3. Automate with GitOps
# argocd-application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: gateway-migration
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/your-org/gateway-configs
targetRevision: main
path: gateway-api
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: false # Don't auto-delete during migration
selfHeal: false
syncOptions:
- CreateNamespace=true
4. Test Rollback Procedures
#!/bin/bash
# test-rollback.sh
# Backup current state
kubectl get gateways,httproutes -n production -o yaml > gateway-backup.yaml
# Simulate rollback
kubectl delete gateways,httproutes -n production
kubectl apply -f ingress-backup.yaml
# Verify rollback
curl -v http://your-app.example.com
# Re-apply Gateway if test successful
kubectl apply -f gateway-backup.yaml
5. Implement Circuit Breakers
# health-check-policy.yaml (implementation-specific)
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
name: health-check-policy
spec:
targetRef:
group: gateway.networking.k8s.io
kind: HTTPRoute
name: my-route
healthCheck:
active:
type: HTTP
http:
path: /health
timeout: 5s
interval: 10s
unhealthyThreshold: 3
healthyThreshold: 1
6. Set Up Comprehensive Monitoring
# prometheus-alerts.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: gateway-migration-alerts
spec:
groups:
- name: gateway-migration
interval: 30s
rules:
- alert: GatewayHighErrorRate
expr: |
rate(gateway_api_http_requests_total{code=~"5.."}[5m]) > 0.05
for: 5m
annotations:
summary: "Gateway error rate above 5%"
description: "Check Gateway and HTTPRoute configurations"
- alert: GatewayNotReady
expr: |
gateway_api_gateway_status{condition="Ready",status="False"} == 1
for: 2m
annotations:
summary: "Gateway not in ready state"
- alert: HTTPRouteNotAttached
expr: |
gateway_api_httproute_status{condition="Accepted",status="False"} == 1
for: 5m
annotations:
summary: "HTTPRoute not attached to Gateway"
7. Maintain Feature Parity
# feature-parity-checklist.yaml
features:
- name: "Basic HTTP routing"
ingress: "✅ Supported"
gateway: "✅ Supported"
status: "Complete"
- name: "TLS termination"
ingress: "✅ Supported"
gateway: "✅ Supported"
status: "Complete"
- name: "Custom headers"
ingress: "✅ Via annotation"
gateway: "✅ Via HTTPRoute filters"
status: "Requires manual conversion"
- name: "Rate limiting"
ingress: "✅ Via annotation"
gateway: "⚠️ Implementation-specific"
status: "Vendor policy required"
- name: "Authentication"
ingress: "✅ oauth2-proxy annotation"
gateway: "⚠️ External AuthN service"
status: "Architecture change needed"
8. Create Reusable Templates
# gateway-template.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: ${APP_NAME}-gateway
namespace: ${NAMESPACE}
labels:
app: ${APP_NAME}
team: ${TEAM_NAME}
spec:
gatewayClassName: ${GATEWAY_CLASS}
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: Same
- name: https
protocol: HTTPS
port: 443
hostname: "${APP_NAME}.${DOMAIN}"
tls:
mode: Terminate
certificateRefs:
- name: ${APP_NAME}-tls
kind: Secret
allowedRoutes:
namespaces:
from: Same
9. Educate Your Team
# Create training materials
cat > team-training.md <<EOF
# Gateway API Migration Training
## Key Differences
1. Gateway vs Ingress
2. HTTPRoute vs Ingress rules
3. ReferenceGrant for cross-namespace
## Common Tasks
### Creating a new route
\`\`\`bash
kubectl apply -f - <<YAML
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: my-app
spec:
parentRefs:
- name: shared-gateway
hostnames:
- "my-app.example.com"
rules:
- backendRefs:
- name: my-service
port: 80
YAML
\`\`\`
### Troubleshooting
1. Check Gateway status: \`kubectl describe gateway <name>\`
2. Check HTTPRoute status: \`kubectl describe httproute <name>\`
3. View logs: \`kubectl logs -l gateway.networking.k8s.io/gateway-name=<name>\`
## Resources
- Gateway API docs: https://gateway-api.sigs.k8s.io
- Internal wiki: https://wiki.company.com/gateway-api
- Slack channel: #gateway-migration
EOF
10. Plan for the Unexpected
# contingency-plan.yaml
scenarios:
- scenario: "Gateway LoadBalancer fails to provision"
detection: "Gateway status 'Ready' = False for >5 minutes"
action: |
1. Check cloud provider quotas
2. Review service annotations
3. Rollback to Ingress if critical
4. Escalate to cloud provider support
- scenario: "HTTPRoute not routing traffic"
detection: "HTTPRoute status 'Accepted' = False"
action: |
1. Check parentRefs match Gateway name
2. Verify namespace scoping
3. Check ReferenceGrant if cross-namespace
4. Review Gateway allowedRoutes
- scenario: "Performance degradation"
detection: "P95 latency >2x baseline"
action: |
1. Check Gateway controller resources
2. Review Gateway configuration
3. Scale Gateway pods if needed
4. Consider reverting to Ingress temporarily
- scenario: "TLS certificate issues"
detection: "HTTPS endpoints returning SSL errors"
action: |
1. Verify secret exists in correct namespace
2. Check ReferenceGrant for cross-namespace secrets
3. Validate certificate expiry
4. Review Gateway TLS configuration
Conclusion {#conclusion}
Migrating from NGINX Ingress to Kubernetes Gateway API is no longer optional – with the March 2026 retirement deadline fast approaching, organizations must act now. While the migration requires careful planning and execution, the benefits of Gateway API make it worthwhile:
Key Takeaways
- Start Planning Immediately – You have limited time before NGINX Ingress loses support
- Use ingress2gateway – Automate conversion where possible, but always review output
- Migrate Gradually – Don’t try to migrate everything at once
- Test Extensively – Use staging environments and parallel deployments
- Monitor Everything – Watch metrics closely during and after migration
- Document Your Journey – Help your team and the community learn from your experience
Next Steps
- Today: Inventory your Ingress resources and identify complex configurations
- This Week: Set up a test environment and experiment with ingress2gateway
- This Month: Migrate your development and staging environments
- Next Quarter: Begin gradual production migration
Additional Resources
- Official Gateway API Documentation: https://gateway-api.sigs.k8s.io
- ingress2gateway GitHub: https://github.com/kubernetes-sigs/ingress2gateway
- Gateway API Conformance Reports: https://gateway-api.sigs.k8s.io/implementations/
- Kubernetes SIG Network: https://github.com/kubernetes/community/tree/master/sig-network
Get Help
- Gateway API Slack: #sig-network-gateway-api on Kubernetes Slack
- GitHub Issues: Report problems or request features
- Community Meetings: Join SIG Network meetings for discussions
The migration from NGINX Ingress to Gateway API represents a significant evolution in Kubernetes networking. While it requires effort upfront, the improved architecture, standardization, and future-proof design make it a worthwhile investment for any organization running production Kubernetes workloads.
Don’t wait until the last minute – start your migration journey today!