Ingress NGINX works, but the Ingress API is a dead end. Gateway API (now GA) is the Kubernetes-native replacement: better traffic splitting, header-based routing, retries, and timeouts — all without annotations. Envoy Gateway implements it cleanly and is what upstream projects are converging on.

The problem: the migration path is barely documented. Most guides show a fresh install. This is about moving a live cluster with existing Ingress resources.

Ingress NGINXGateway API (Envoy Gateway)
IngressClassGatewayClass
IngressHTTPRoute
nginx.ingress.kubernetes.io/… annotationsNative fields in HTTPRoute / filters
TLS in spec.tls[]TLS in Gateway listener
nginx.conf snippetsEnvoyProxy custom resource

Read all existing Ingresses

kubectl get ingress -A -o yaml > ingresses-dump.yaml # List unique annotations in use kubectl get ingress -A -o jsonpath='{range .items[*]}{.metadata.annotations}{"\n"}{end}' \ | grep -o '"nginx\.ingress[^"]*"' | sort -u

Every annotation maps to a Gateway API equivalent (or a workaround). You need to know which ones you're actually using before you can plan the migration.

Common annotations and their replacements: nginx.ingress.kubernetes.io/rewrite-targetHTTPRoute.filters[].urlRewrite · proxy-connect-timeoutHTTPRoute.timeouts.request · ssl-redirectGateway listener with HTTPS and a redirect route.
helm install eg oci://docker.io/envoyproxy/gateway-helm \ --version v1.1.0 \ --namespace envoy-gateway-system \ --create-namespace

Then create the GatewayClass and a Gateway per environment (or one shared Gateway). The Gateway is where TLS certs and listeners live.

apiVersion: gateway.networking.k8s.io/v1 kind: GatewayClass metadata: name: envoy spec: controllerName: gateway.envoyproxy.io/gatewayclass-controller --- apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: name: prod namespace: envoy-gateway-system spec: gatewayClassName: envoy listeners: - name: https port: 443 protocol: HTTPS tls: mode: Terminate certificateRefs: - name: wildcard-tls namespace: envoy-gateway-system - name: http port: 80 protocol: HTTP

Before (Ingress NGINX)

apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: api 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-service port: number: 8080

After (HTTPRoute)

apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: api spec: parentRefs: - name: prod namespace: envoy-gateway-system hostnames: - api.example.com rules: - matches: - path: type: PathPrefix value: /v1 filters: - type: URLRewrite urlRewrite: path: type: ReplacePrefixMatch replacePrefixMatch: / backendRefs: - name: api-service port: 8080
Run both in parallel first. Deploy Envoy Gateway and HTTPRoutes while keeping Ingress NGINX running. Use a lower-weight DNS record to send 10% of traffic to the new gateway.
Watch error rates, not just latency. Annotation-based rewrites and header manipulations are easy to miss. Check 5xx rates per service in the new path for at least 24 hours.
Shift by service, not all at once. Migrate one HTTPRoute at a time. If something breaks, roll back that route — not the entire gateway.
Delete Ingresses last. Keep the Ingress objects around until every service is confirmed working via HTTPRoute. Deleting an Ingress is easy; recovering from a wrong cutover is not.
The thing nobody mentions: Envoy Gateway creates a separate LoadBalancer per Gateway. If you have one Gateway for all services (recommended), you get one LB. If you create one Gateway per namespace, you get one LB per namespace — and the cost adds up fast. Plan your Gateway topology before you start.