Moving Off ingress-nginx? Migrate to Envoy Gateway Safely

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.1
Gateway 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
Short on time?Beta Get the gist of this 20-minute read in seconds, then ask follow-up questions

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 Service

During 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 + HTTPRoute

After 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 Service

What 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.

bash
kubectl get ingress -A
output
NAMESPACE   NAME   CLASS   HOSTS               ADDRESS         PORTS     AGE
shop        shop   nginx   shop.example.test   10.96.127.133   80, 443   10m

The 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.

bash
kubectl get ingressclass
output
NAME    CONTROLLER             PARAMETERS   AGE
nginx   k8s.io/ingress-nginx   <none>       11m

k8s.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:

bash
kubectl get ingress -A -o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,CLASS:.spec.ingressClassName,HOSTS:.spec.rules[*].host,TLS:.spec.tls[*].secretName'
output
NAMESPACE   NAME   CLASS   HOSTS               TLS
shop        shop   nginx   shop.example.test   shop-tls

This Ingress is the one I migrated:

  • Host: shop.example.test
  • /api backend: Service api on port 8080
  • / backend: Service web on port 8080
  • TLS Secret: shop-tls in the same namespace
yaml
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: 8080

If 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.

bash
kubectl -n shop describe ingress shop

The annotation block on this object is empty:

output
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.

bash
helm install eg oci://docker.io/envoyproxy/gateway-helm --version v1.9.1 -n envoy-gateway-system --create-namespace --wait --timeout 5m
output
NAME: eg
LAST DEPLOYED: Sat Oct  3 20:01:56 2026
NAMESPACE: envoy-gateway-system
STATUS: deployed
REVISION: 1

Wait until the controller Deployment is available:

bash
kubectl -n envoy-gateway-system get deploy envoy-gateway
output
NAME            READY   UP-TO-DATE   AVAILABLE   AGE
envoy-gateway   1/1     1            1           7s

READY 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:

bash
kubectl -n envoy-gateway-system get deploy envoy-gateway -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
output
docker.io/envoyproxy/gateway:v1.9.1

By default, the Envoy Gateway chart includes Gateway API CRDs from the experimental channel. Check the bundle on the CRD:

bash
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"}'
output
version=v1.6.1 channel=experimental

This 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:

text
helm install eg oci://docker.io/envoyproxy/gateway-helm --version v1.9.1 -n envoy-gateway-system --create-namespace --set crds.enabled=false

The 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.

yaml
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: eg
spec:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller

Save 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.

bash
kubectl apply -f gatewayclass.yaml
output
gatewayclass.gateway.networking.k8s.io/eg created

Accepted must be True before you apply routes. Accepted=True confirms that Envoy Gateway recognizes and manages this GatewayClass.

bash
kubectl get gatewayclass eg
output
NAME   CONTROLLER                                      ACCEPTED   AGE
eg     gateway.envoyproxy.io/gatewayclass-controller   True       1s

Convert 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.
bash
ingress2gateway print --namespace shop --providers ingress-nginx --emitter standard --no-color

The converter creates three resources for this example:

  1. A Gateway that exposes HTTP and HTTPS listeners for shop.example.test
  2. An HTTPS HTTPRoute that sends / and /api to their Services
  3. 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 /api and / 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.

output
┌─ 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.

IMPORTANT
ingress2gateway may keep the source IngressClass name on the Gateway. Check 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:

yaml
gatewayClassName: nginx

Change that one field:

yaml
gatewayClassName: eg

Leave 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: Secret shop-tls, same namespace as the Gateway
  • HTTPS backendRefs: api:8080 for /api, web:8080 for /
  • HTTP filters: RequestRedirect to scheme https with status 308
  • 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.

bash
kubectl apply -f shop-gateway.yaml
output
gateway.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 created

Verify that Envoy accepted the migrated routes

Give the controller a few seconds, then read status before you send a request.

  • Gateway Accepted means Envoy Gateway recognized the Gateway
  • HTTPRoute Accepted means the route is attached to that parent
  • ResolvedRefs=True means 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.

bash
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'
output
NAME    CLASS   ACCEPTED   PROGRAMMED
nginx   eg      Accepted   AddressNotAssigned

CLASS 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.

bash
kubectl -n shop get httproute -o custom-columns='NAME:.metadata.name,PARENT:.status.parents[*].parentRef.name,REASON:.status.parents[*].conditions[*].reason'
output
NAME                          PARENT   REASON
shop-shop-example-test        nginx    Accepted,ResolvedRefs
shop-shop-example-test-http   nginx    Accepted,ResolvedRefs

Both 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 Gateway

Envoy 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.

bash
kubectl get svc -A -l gateway.envoyproxy.io/owning-gateway-namespace=shop,gateway.envoyproxy.io/owning-gateway-name=nginx
output
NAMESPACE              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   5s

Use the external address on ports 80 and 443 when the platform fills EXTERNAL-IP in. curl --resolve should name that address.

NOTE
No external IP? If the Service stays <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.

bash
kubectl -n ingress-nginx port-forward svc/ingress-nginx-controller 18080:80 18443:443
output
Forwarding from 127.0.0.1:18080 -> 80
Forwarding from 127.0.0.1:18443 -> 443

Forward the Envoy Service the label selector printed:

bash
kubectl -n envoy-gateway-system port-forward svc/envoy-shop-nginx-ddf171dd 19080:80 19443:443
output
Forwarding from 127.0.0.1:19080 -> 10080
Forwarding from 127.0.0.1:19443 -> 10443

10080 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.

bash
curl -skD - --resolve shop.example.test:18443:127.0.0.1 https://shop.example.test:18443/
output
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

web

The 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:

bash
curl -sk --resolve shop.example.test:18443:127.0.0.1 https://shop.example.test:18443/api
output
api

Plain HTTP on this Ingress returns only the redirect:

bash
curl -sD - -o /dev/null --resolve shop.example.test:18080:127.0.0.1 http://shop.example.test:18080/
output
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.test

Send the same HTTPS paths through Envoy:

bash
curl -skD - --resolve shop.example.test:19443:127.0.0.1 https://shop.example.test:19443/
output
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:

bash
curl -sk --resolve shop.example.test:19443:127.0.0.1 https://shop.example.test:19443/api
output
api

The 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:

bash
curl -sD - -o /dev/null --resolve shop.example.test:80:10.96.26.40 http://shop.example.test/
output
HTTP/1.1 308 Permanent Redirect
location: https://shop.example.test/
date: Sat, 03 Oct 2026 14:44:01 GMT
content-length: 0

Status 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 /api return 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.

IMPORTANT
If DNS or the load balancer already sends clients to Envoy, point that traffic back at ingress-nginx and confirm the app answers before you delete the Gateway. Deleting the Gateway while clients still target Envoy takes the site down.

After cutover, use this order:

  1. Point shop.example.test back at the ingress-nginx Service.
  2. Request the hostname and confirm the application still answers.
  3. Delete only the HTTPRoutes this conversion created.
  4. 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.

bash
kubectl -n shop delete httproute shop-shop-example-test shop-shop-example-test-http
output
httproute.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 namespace

Delete the Gateway after the routes. That removes the Envoy proxy Service for this host.

bash
kubectl -n shop delete gateway nginx
output
gateway.gateway.networking.k8s.io "nginx" deleted from shop namespace

The ingress-nginx forward still returned the same bodies after that delete:

bash
curl -sk --resolve shop.example.test:18443:127.0.0.1 https://shop.example.test:18443/
output
web

/ still returns web through ingress-nginx. Check /api on the same forward:

bash
curl -sk --resolve shop.example.test:18443:127.0.0.1 https://shop.example.test:18443/api
output
api

You 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


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.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)