| Tested on | RHEL 10.2 (Coughlan) workstation |
|---|---|
| Package | kubectl 1.37.0 (server 1.33.1)ingress-nginx controller v1.15.1 (chart 4.15.1)Envoy Gateway v1.9.1Gateway API CRDs v1.6.1 (experimental channel)ingress2gateway 1.0.0 |
| Applies to | Any host with kubectl configured; any Kubernetes cluster that already runs ingress-nginx |
| Lab environment | A Kubernetes cluster with the ingress-nginx controller already installed |
| Privilege | Normal user |
| Scope | How an existing Ingress stays on ingress-nginx while you add a Gateway and HTTPRoutes for the same host, prefix paths, and TLS Secret, compare both controllers, and change DNS only after that check. Rewrites, regex, canary, CORS, authentication, and rate limits can appear in the converter output and need their own review. They are not the worked example here. |
| Related guides | Kubernetes Ingress rules Kubernetes Gateway API Kubernetes Services Kubernetes Secrets Helm charts |
ingress-nginx and Envoy Gateway do not configure traffic the same way. Your existing Kubernetes Ingress stays active under ingress-nginx while you create equivalent Gateway and HTTPRoute resources for Envoy Gateway. Both controllers can run side by side, and clients stay on the old address until you have checked the new route.
The worked example is one hostname, shop.example.test, with / and /api on two Services that already exist, plus the TLS Secret that Ingress already uses. Community ingress-nginx reached retirement in March 2026. This guide focuses only on migrating that basic host, path, and TLS configuration.
Clients use the Ingress until you change DNS or the load balancer:
BEFORE
Client
↓
Ingress
↓
ingress-nginx
↓
┌─────┴─────┐
↓ ↓
web Service api ServiceDuring the check, both controllers reach those same Services. You send test requests to Envoy directly. Public DNS still points at ingress-nginx.
DURING MIGRATION
┌──► ingress-nginx ──► Services
Client / DNS ─────────┤
└──► Envoy Gateway ──► Services
▲
│
Gateway + HTTPRouteAfter cutover, clients use Envoy. Leave the Ingress in place until that path is the one you trust.
AFTER CUTOVER
Client
↓
Envoy Gateway
↓
Gateway + HTTPRoute
↓
┌─────┴─────┐
↓ ↓
web Service api ServiceWhat changes when you move from ingress-nginx to Envoy Gateway
The application does not move. You add a second way to reach the Services it already has, then you change where clients connect.
| Component | During migration |
|---|---|
| Application Deployment | No change |
| Kubernetes Service | No change |
| Existing Ingress | Keep it until rollback is no longer needed |
| ingress-nginx | Keep it running while you validate Envoy |
| GatewayClass | New |
| Gateway | New |
| HTTPRoute | New |
| TLS Secret | Reuse the Secret the Ingress already references |
| DNS or load balancer target | Change it only at cutover |
For this example, you create the GatewayClass once for Envoy Gateway. ingress2gateway then converts this Ingress into three resources: one Gateway and two HTTPRoutes.
| Today | After conversion | What it selects |
|---|---|---|
IngressClass nginx |
GatewayClass eg |
Which controller programs the listeners |
Ingress shop |
Gateway nginx plus two HTTPRoutes |
Listeners on ports 80 and 443, then the path rules |
spec.tls[].secretName: shop-tls |
Gateway listener certificateRefs |
The same TLS Secret |
Service web / api port 8080 |
HTTPRoute backendRefs |
The same Services |
The Gateway object name nginx is only a name. It does not select the ingress-nginx controller. The generated Gateway initially keeps nginx as its gatewayClassName. Before applying it, change that field to eg so Envoy Gateway knows that it should manage this Gateway.
Audit your existing ingress-nginx configuration
List every Ingress before you install anything new. You need the class, the hosts, the paths, and the TLS Secret names so the converted Gateway still matches what clients request.
kubectl get ingress -ANAMESPACE NAME CLASS HOSTS ADDRESS PORTS AGE
shop shop nginx shop.example.test 10.96.127.133 80, 443 10mThe ADDRESS column is the ingress-nginx controller Service, not an application pod. On a cloud cluster this is often a load balancer hostname. Here it is the controller ClusterIP 10.96.127.133. That address must keep answering until clients move.
Confirm which controller owns the class named on the Ingress. Envoy does not take over an IngressClass.
kubectl get ingressclassNAME CONTROLLER PARAMETERS AGE
nginx k8s.io/ingress-nginx <none> 11mk8s.io/ingress-nginx is the community ingress-nginx controller. A class whose controller is nginx.org/ingress-controller is the F5 NGINX Ingress Controller. This guide only covers the community controller named above.
Pull the hosts and Secret names out of the objects so you can match them after conversion:
kubectl get ingress -A -o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,CLASS:.spec.ingressClassName,HOSTS:.spec.rules[*].host,TLS:.spec.tls[*].secretName'NAMESPACE NAME CLASS HOSTS TLS
shop shop nginx shop.example.test shop-tlsThis Ingress is the one I migrated:
- Host:
shop.example.test /apibackend: Serviceapion port 8080/backend: Servicewebon port 8080- TLS Secret:
shop-tlsin the same namespace
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop
namespace: shop
spec:
ingressClassName: nginx
tls:
- hosts:
- shop.example.test
secretName: shop-tls
rules:
- host: shop.example.test
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api
port:
number: 8080
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 8080If the same host appears on more than one Ingress, convert those objects in one pass and review the generated HTTPRoutes together. ingress-nginx merges rules for a host across Ingress objects. Gateway API does not merge them for you.
Audit ingress-nginx annotations before conversion
Do not assume an annotation is preserved because ingress2gateway prints valid YAML. Version 1.0.0 understands a much larger set of nginx.ingress.kubernetes.io/* annotations, including:
- Rewrites and redirects
- Regex path matching and CORS
- Canary routing
- Timeouts, headers, backend TLS, and other traffic settings
The generated resources may still behave differently from the original Ingress, so review the converter warnings before you move traffic.
Inventory every nginx.ingress.kubernetes.io/* key first. Then read which ones the converter translated, reported as unsupported, or left unrecognized.
kubectl -n shop describe ingress shopThe annotation block on this object is empty:
Annotations: <none>kubectl describe hides kubectl.kubernetes.io/last-applied-configuration. Look for keys that start with nginx.ingress.kubernetes.io/.
| You have | What to do |
|---|---|
| Host, prefix paths, and a TLS Secret | Convert it with this page. A TLS block turns on HTTP 308 to HTTPS unless ssl-redirect is "false". |
rewrite-target, use-regex, canary-*, CORS, timeouts, header changes, backend TLS |
1.0.0 may translate these. Compare the generated HTTPRoutes and any policy objects with the live Ingress before you cut traffic over. |
Authentication, rate limits, configuration-snippet, server-snippet |
These often need an Envoy Gateway policy or another design, not a single HTTPRoute field. There is no generic one-to-one core Gateway API equivalent for arbitrary snippet text. |
Rewrites and regex paths need a closer look than the host and prefix example on this page. Migrate rewrite-target and regex paths shows the upstream path the converter produces, including a capture group copied as the literal path /$2.
This Ingress had none of those keys, so the file below is only the host, the two prefixes, and the TLS Secret.
Install Envoy Gateway beside ingress-nginx
Install Envoy Gateway into its own namespace. Do not delete the ingress-nginx Helm release, and do not change ingressClassName on the live Ingress.
The chart installs both Gateway API CRDs and Envoy Gateway CRDs by default. If your Kubernetes platform already manages Gateway API CRDs, first confirm that its version and channel are compatible with this Envoy Gateway release. When they are compatible, keep the platform-managed Gateway API CRDs and install Envoy Gateway without installing another copy of them.
In my test cluster, Gateway API CRDs were not installed beforehand, so the Envoy Gateway chart installed them.
helm install eg oci://docker.io/envoyproxy/gateway-helm --version v1.9.1 -n envoy-gateway-system --create-namespace --wait --timeout 5mNAME: eg
LAST DEPLOYED: Sat Oct 3 20:01:56 2026
NAMESPACE: envoy-gateway-system
STATUS: deployed
REVISION: 1Wait until the controller Deployment is available:
kubectl -n envoy-gateway-system get deploy envoy-gatewayNAME READY UP-TO-DATE AVAILABLE AGE
envoy-gateway 1/1 1 1 7sREADY 1/1 means the controller process is up. You can also confirm the controller image to verify the Envoy Gateway version running in the cluster:
kubectl -n envoy-gateway-system get deploy envoy-gateway -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'docker.io/envoyproxy/gateway:v1.9.1By default, the Envoy Gateway chart includes Gateway API CRDs from the experimental channel. Check the bundle on the CRD:
kubectl get crd gateways.gateway.networking.k8s.io -o jsonpath='version={.metadata.annotations.gateway\.networking\.k8s\.io/bundle-version} channel={.metadata.annotations.gateway\.networking\.k8s\.io/channel}{"\n"}'version=v1.6.1 channel=experimentalThis output is the default experimental bundle, version v1.6.1. A platform that already owns compatible Gateway API CRDs should keep them. Install the Envoy Gateway CRDs separately first, then install the controller with crds.enabled=false. That flag alone does not install the Envoy-specific CRDs. The Envoy Gateway Helm install guide shows the separate CRD chart. The controller install that follows it looks like this:
helm install eg oci://docker.io/envoyproxy/gateway-helm --version v1.9.1 -n envoy-gateway-system --create-namespace --set crds.enabled=falseThe Helm install earlier in this section did not use that flag, because my test cluster had no Gateway API CRDs yet. ingress-nginx is still the only implementation of this Ingress. Nothing in this install reads those Ingress objects.
Create the Envoy GatewayClass
A GatewayClass identifies the controller that will manage Gateways, similar to how an IngressClass identifies an Ingress controller. A Gateway is the actual traffic entry point created from that class. It contains listeners such as HTTP port 80 and HTTPS port 443. Multiple Gateways can use the same GatewayClass.
Envoy Gateway does not create a GatewayClass for you. Without one, the controller log says no accepted gatewayclass and no proxy pods start.
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: eg
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controllerSave that manifest as gatewayclass.yaml. controllerName tells Kubernetes which controller manages this GatewayClass. Envoy Gateway watches for gateway.envoyproxy.io/gatewayclass-controller. The object name eg is what you will put in gatewayClassName later.
kubectl apply -f gatewayclass.yamlgatewayclass.gateway.networking.k8s.io/eg createdAccepted must be True before you apply routes. Accepted=True confirms that Envoy Gateway recognizes and manages this GatewayClass.
kubectl get gatewayclass egNAME CONTROLLER ACCEPTED AGE
eg gateway.envoyproxy.io/gatewayclass-controller True 1sConvert the Ingress with ingress2gateway
This guide uses ingress2gateway 1.0.0 from the Kubernetes SIG-Network release archive. Envoy Gateway's migration page also mentions ingress2eg, an unofficial tool that emits extra Envoy policy objects. This example uses ingress2gateway because the migration only needs standard host, path, redirect, and TLS resources.
Point the binary at the namespace that holds the Ingress, and ask the standard emitter for core Gateway API objects.
For this example:
- The standard emitter produced the Gateway and HTTPRoutes shown below.
- I also ran
--emitter envoy-gateway. - For this annotation-free Ingress, both emitters generated the same Gateway and HTTPRoutes.
- The Envoy-specific emitter had no additional policy objects to create.
ingress2gateway print --namespace shop --providers ingress-nginx --emitter standard --no-colorThe converter creates three resources for this example:
- A Gateway that exposes HTTP and HTTPS listeners for
shop.example.test - An HTTPS HTTPRoute that sends
/and/apito their Services - An HTTP HTTPRoute that redirects port 80 to HTTPS with status
308
The warning at the start of the tool output is expected. Review it with these points in mind:
- Gateway API does not expose nginx-style URL-normalization controls.
- Plain prefixes
/apiand/were unchanged in this example. - Paths that rely on collapsed slashes or
.segments need direct tests on Envoy. - When
nginx.ingress.kubernetes.io/*keys exist, the command reports translated, unsupported, and unrecognized annotations.
This Ingress had no annotations, so that annotation report is absent.
┌─ WARN ────────────────────────────────────────
│ Gateway API does not support configuring URL normalization (RFC 3986, Section 6). Please check if this matters for your use case and consult implementation-specific details.
│ source: STANDARD_EMITTER
└─
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
annotations:
gateway.networking.k8s.io/generator: ingress2gateway-dev
name: nginx
namespace: shop
spec:
gatewayClassName: nginx
listeners:
- hostname: shop.example.test
name: shop-example-test-http
port: 80
protocol: HTTP
- hostname: shop.example.test
name: shop-example-test-https
port: 443
protocol: HTTPS
tls:
certificateRefs:
- group: ""
kind: Secret
name: shop-tls
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
annotations:
gateway.networking.k8s.io/generator: ingress2gateway-dev
name: shop-shop-example-test
namespace: shop
spec:
hostnames:
- shop.example.test
parentRefs:
- name: nginx
port: 443
rules:
- backendRefs:
- name: api
port: 8080
matches:
- path:
type: PathPrefix
value: /api
name: rule-0
- backendRefs:
- name: web
port: 8080
matches:
- path:
type: PathPrefix
value: /
name: rule-1
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
annotations:
gateway.networking.k8s.io/generator: ingress2gateway-dev
name: shop-shop-example-test-http
namespace: shop
spec:
hostnames:
- shop.example.test
parentRefs:
- name: nginx
port: 80
rules:
- filters:
- requestRedirect:
scheme: https
statusCode: 308
type: RequestRedirect
matches:
- path:
type: PathPrefix
value: /api
- filters:
- requestRedirect:
scheme: https
statusCode: 308
type: RequestRedirect
matches:
- path:
type: PathPrefix
value: /The annotation ingress2gateway-dev is the generator mark written by the 1.0.0 release binary. ingress2gateway --version on that binary also prints dev.
Review the generated Gateway and HTTPRoutes
Save the YAML. Before applying the file, compare these fields with the original Ingress to make sure Envoy will serve the same hostname, paths, TLS certificate, and backends.
spec.gatewayClassName before you apply. It must name the GatewayClass Envoy Gateway owns (eg here), not the IngressClass nginx. A Gateway left on nginx never gets an Envoy proxy.
The tool printed this:
gatewayClassName: nginxChange that one field:
gatewayClassName: egLeave the Gateway name as nginx. That name is only an object name. The HTTPRoutes refer to it in parentRefs.
Also check these before apply:
- Listeners: port 80 HTTP and port 443 HTTPS, both hostname
shop.example.test certificateRefs: Secretshop-tls, same namespace as the Gateway- HTTPS
backendRefs:api:8080for/api,web:8080for/ - HTTP filters:
RequestRedirectto schemehttpswith status308 - Any annotation the tool marked unsupported or unrecognized
Delete every status: block if the tool printed one. Apply the spec only.
The HTTPS HTTPRoute keeps both backends. The HTTP HTTPRoute is only the redirect ingress-nginx was already sending, because a TLS block turns SSL redirect on unless ssl-redirect is "false".
Apply the migrated Gateway API resources
Apply the edited file. The original Ingress is not in that file, so ingress-nginx keeps serving it.
kubectl apply -f shop-gateway.yamlgateway.gateway.networking.k8s.io/nginx created
httproute.gateway.networking.k8s.io/shop-shop-example-test created
httproute.gateway.networking.k8s.io/shop-shop-example-test-http createdVerify that Envoy accepted the migrated routes
Give the controller a few seconds, then read status before you send a request.
- Gateway
Acceptedmeans Envoy Gateway recognized the Gateway - HTTPRoute
Acceptedmeans the route is attached to that parent ResolvedRefs=Truemeans the references used by the route, such as its backend Services, could be resolved successfully
AddressNotAssigned on Programmed means no load balancer has published an external address yet. The listeners can still be programmed.
kubectl -n shop get gateway nginx -o custom-columns='NAME:.metadata.name,CLASS:.spec.gatewayClassName,ACCEPTED:.status.conditions[?(@.type=="Accepted")].reason,PROGRAMMED:.status.conditions[?(@.type=="Programmed")].reason'NAME CLASS ACCEPTED PROGRAMMED
nginx eg Accepted AddressNotAssignedCLASS is eg, which is the edit from the previous section. Listener status in kubectl describe gateway said the HTTP and HTTPS listeners were programmed and their Secret refs resolved. The routes were ready, but the LoadBalancer had not received an external address yet.
kubectl -n shop get httproute -o custom-columns='NAME:.metadata.name,PARENT:.status.parents[*].parentRef.name,REASON:.status.parents[*].conditions[*].reason'NAME PARENT REASON
shop-shop-example-test nginx Accepted,ResolvedRefs
shop-shop-example-test-http nginx Accepted,ResolvedRefsBoth routes attach to the Gateway named nginx. ResolvedRefs here confirms that the web and api Service references are valid.
Test Envoy before changing traffic
Compare the same hostname, the same URLs, and the same backend responses on both controllers. Change only the address curl connects to. Keep the Host header and TLS SNI as shop.example.test. A request to the raw IP often fails TLS on Envoy, because the HTTPS listener is bound to that hostname.
┌── ingress-nginx
curl --resolve ┤
└── Envoy GatewayEnvoy Gateway creates a proxy Service for this Gateway. It is not the ingress-nginx Service. Select it by the Gateway that owns it, because the Service name ends in a hash.
kubectl get svc -A -l gateway.envoyproxy.io/owning-gateway-namespace=shop,gateway.envoyproxy.io/owning-gateway-name=nginxNAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
envoy-gateway-system envoy-shop-nginx-ddf171dd LoadBalancer 10.96.26.40 <pending> 80:31468/TCP,443:32458/TCP 5sUse the external address on ports 80 and 443 when the platform fills EXTERNAL-IP in. curl --resolve should name that address.
<pending>, compare through the ClusterIP, a NodePort, or a temporary kubectl port-forward. Do not update public DNS until an address outside the cluster exists.
This Service stayed <pending>, which matches AddressNotAssigned. I forwarded local ports so the checks could use 127.0.0.1 and still send the real hostname. Use any free local ports. Leave each forward running in its own terminal.
kubectl -n ingress-nginx port-forward svc/ingress-nginx-controller 18080:80 18443:443Forwarding from 127.0.0.1:18080 -> 80
Forwarding from 127.0.0.1:18443 -> 443Forward the Envoy Service the label selector printed:
kubectl -n envoy-gateway-system port-forward svc/envoy-shop-nginx-ddf171dd 19080:80 19443:443Forwarding from 127.0.0.1:19080 -> 10080
Forwarding from 127.0.0.1:19443 -> 1044310080 and 10443 are the unprivileged ports inside the Envoy container. Clients, and this port-forward, still use Service ports 80 and 443. Do not put the container ports in DNS.
Ask ingress-nginx for / over HTTPS. The body is the backend, not the controller.
curl -skD - --resolve shop.example.test:18443:127.0.0.1 https://shop.example.test:18443/HTTP/2 200
date: Sat, 03 Oct 2026 14:46:35 GMT
content-type: text/plain; charset=utf-8
content-length: 4
x-app-name: http-echo
x-app-version: 1.0.0
strict-transport-security: max-age=31536000; includeSubDomains
webThe body web is the / Service. strict-transport-security is added by ingress-nginx. A matching status code does not mean the headers match.
/api should hit the other Service:
curl -sk --resolve shop.example.test:18443:127.0.0.1 https://shop.example.test:18443/apiapiPlain HTTP on this Ingress returns only the redirect:
curl -sD - -o /dev/null --resolve shop.example.test:18080:127.0.0.1 http://shop.example.test:18080/HTTP/1.1 308 Permanent Redirect
Date: Sat, 03 Oct 2026 14:46:35 GMT
Content-Type: text/html
Content-Length: 164
Connection: keep-alive
Location: https://shop.example.testSend the same HTTPS paths through Envoy:
curl -skD - --resolve shop.example.test:19443:127.0.0.1 https://shop.example.test:19443/HTTP/2 200
x-app-name: http-echo
x-app-version: 1.0.0
date: Sat, 03 Oct 2026 14:44:16 GMT
content-length: 4
content-type: text/plain; charset=utf-8
web/ returned web. The /api prefix should hit the other Service:
curl -sk --resolve shop.example.test:19443:127.0.0.1 https://shop.example.test:19443/apiapiThe bodies match. Envoy did not add the HSTS header ingress-nginx added. Browsers that already cached HSTS for this host will still upgrade HTTP. A new client will not see that header until you set it on Envoy. Plan that header before you remove ingress-nginx.
Do not compare only status codes and response bodies. ingress-nginx may have added behavior by default or through annotations. Check these on both controllers before cutover:
- HTTP status
- Response body
- Redirect target
- TLS certificate name and dates
- Response headers you rely on, including HSTS
- Each path, not only
/
Check the HTTP redirect against Service port 80, not against the high local port. A request that arrives on port 19080 can produce Location: https://shop.example.test:19080/, which is the port you forwarded, not the port clients will use. Against the ClusterIP on port 80 the location is the hostname only:
curl -sD - -o /dev/null --resolve shop.example.test:80:10.96.26.40 http://shop.example.test/HTTP/1.1 308 Permanent Redirect
location: https://shop.example.test/
date: Sat, 03 Oct 2026 14:44:01 GMT
content-length: 0Status 308 and scheme https match ingress-nginx. The HTML body differs. Clients follow the Location header. Both certificates are the Secret shop-tls. The subject and dates matched on the two HTTPS ports.
Cut over traffic to Envoy Gateway
Now change the entry point clients actually use, normally the DNS record or external load balancer for shop.example.test, so that it points to the Envoy Gateway address on ports 80 and 443. Leave the Ingress and the ingress-nginx Service in place.
Before you cut over, confirm:
/and/apireturn the expected bodies through Envoy.- Port 80 redirects to
https://shop.example.test/without a temporary local port. - The Envoy Service has an externally reachable address.
- The TLS certificate and required response headers match your application policy.
If EXTERNAL-IP is still <pending>, fix the Service type or the load balancer controller first. The application pods keep running through the switch because both controllers select the existing Services.
After DNS changes, request the public name with no --resolve and expect web on / and api on /api. Keep a way to reach ingress-nginx directly until that public check works, so rollback does not depend on the new path.
Roll back to ingress-nginx
Because the original Ingress is still running, rollback does not require rebuilding ingress-nginx. If traffic has already moved to Envoy, first point it back to ingress-nginx, then remove the Gateway API resources created for this migration.
After cutover, use this order:
- Point
shop.example.testback at the ingress-nginx Service. - Request the hostname and confirm the application still answers.
- Delete only the HTTPRoutes this conversion created.
- Delete the Gateway.
If you have not changed DNS, clients are already on ingress-nginx. Start at step 3.
Delete these two routes by name. Other applications in the namespace may already have HTTPRoutes. Do not delete every HTTPRoute in the namespace.
kubectl -n shop delete httproute shop-shop-example-test shop-shop-example-test-httphttproute.gateway.networking.k8s.io "shop-shop-example-test" deleted from shop namespace
httproute.gateway.networking.k8s.io "shop-shop-example-test-http" deleted from shop namespaceDelete the Gateway after the routes. That removes the Envoy proxy Service for this host.
kubectl -n shop delete gateway nginxgateway.gateway.networking.k8s.io "nginx" deleted from shop namespaceThe ingress-nginx forward still returned the same bodies after that delete:
curl -sk --resolve shop.example.test:18443:127.0.0.1 https://shop.example.test:18443/web/ still returns web through ingress-nginx. Check /api on the same forward:
curl -sk --resolve shop.example.test:18443:127.0.0.1 https://shop.example.test:18443/apiapiYou can leave the Envoy Gateway controller and the eg GatewayClass installed. They do nothing to Ingress objects. Remove them later with helm uninstall eg -n envoy-gateway-system only when no other Gateway uses that release. Helm does not remove the CRDs.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Gateway never gets an Envoy Service | gatewayClassName is still nginx, or the GatewayClass is not Accepted |
Set gatewayClassName: eg and confirm kubectl get gatewayclass eg shows Accepted True |
Programmed is AddressNotAssigned |
No external load balancer address | Compare on the ClusterIP, a NodePort, or a port-forward. Do not publish DNS until EXTERNAL-IP is set |
| TLS reset when you curl the Envoy IP | The client did not send SNI for shop.example.test |
curl --resolve with the hostname, or use the real DNS name |
Redirect Location contains a high port |
The request hit a port-forward or NodePort | Repeat the check against Service port 80. The location should be https://shop.example.test/ |
| HTTPS body matches but browsers still warn | The Secret is not the certificate clients trust, or HSTS existed only on ingress-nginx | Compare the certificate subject and dates on both ports. Plan the HSTS header before you remove ingress-nginx |
| Paths that worked on ingress-nginx now return 404 | The route may depend on regex, a rewrite, URL normalization, or another annotation whose generated behavior differs | Compare the generated HTTPRoute and the converter warnings with the original Ingress before you cut traffic over |
Summary
The Ingress stayed on ingress-nginx the whole time. Envoy Gateway went in beside it, with its own GatewayClass, and ingress2gateway turned that Ingress into a Gateway plus two HTTPRoutes. The edit that made Envoy claim the Gateway was gatewayClassName: eg. The generated object name nginx is not the controller.
HTTPS / returned web and /api returned api on both controllers. HTTP returned 308 to https://shop.example.test/ once the request hit port 80. Envoy did not send the HSTS header ingress-nginx adds by default, so a matching body is not the whole check. AddressNotAssigned only meant the load balancer had no public address yet.
Point DNS at Envoy only after that comparison. This is a parallel migration with the old controller left in place. It is not a weighted, request-by-request traffic shift. Roll back by pointing clients at ingress-nginx first, then deleting the two HTTPRoutes and the Gateway. The Ingress object and the application Services stay where they were. If the converter reports translated annotations, compare those generated resources with the live Ingress before you cut that host over.
References
- Envoy Gateway install with Helm
- Migrate from Ingress to Envoy Gateway
- Gateway API
- ingress2gateway 1.0.0
- Ingress2Gateway 1.0 announcement
- Kubernetes Ingress
- Ingress NGINX retirement
Frequently Asked Questions
Can I delete ingress-nginx as soon as Envoy returns 200?
No. Leave the Ingress and the ingress-nginx Service until clients use the Envoy address and you have compared redirects, paths, and headers. A 200 only shows that Envoy can serve that URL. Deleting the old controller while DNS still points at it takes the site down.
Does ingress2gateway 1.0.0 keep nginx annotations?
It translates many of them, including rewrites, redirects, regex paths, CORS, canary, and several traffic settings. Valid YAML is not proof the behavior matches. Read the report of translated, unsupported, and unrecognized annotations, and compare the generated routes before cutover. Arbitrary configuration-snippet and server-snippet text has no one-to-one core Gateway API field.
Why is the generated Gateway named nginx?
ingress2gateway copies the IngressClass name into the Gateway name and into gatewayClassName. The name is only an object name. Change gatewayClassName to the Envoy GatewayClass, eg in this guide, before you apply the file.
Programmed is False and the external address is pending. Did the route fail?
Not by itself. AddressNotAssigned means no load balancer published an IP. Check that the HTTPRoutes are Accepted and ResolvedRefs, then curl the ClusterIP or a port-forward. Publish DNS only after EXTERNAL-IP is filled in.
Do the application pods restart during this cutover?
No. Both controllers send traffic to the existing Services. Deleting the Gateway removes the Envoy proxy for that host. It does not delete the Deployments.

