A Docker-only smoke test of ingress2gateway against a synthetic Ingress with TLS, HTTP-to-HTTPS redirect, a regex route, and a capture-group rewrite.
ingress-nginx reached retirement in March 2026. The obvious response is to convert manifests to Gateway API and move on.
I do not think that is enough.
An ingress migration changes the way a public request reaches an application. The YAML is different, but the thing worth preserving is a routing contract: which hostnames exist, which paths match, which backend receives them, whether HTTP redirects, where TLS terminates, and which exceptions were carried by annotations.
The Kubernetes SIG Network project ingress2gateway is the right kind of starting point. It converts Ingress and supported provider resources into Gateway API resources, and it reports fields it cannot translate. Its current v1.2.0 release was published on July 7, 2026.
I wanted to see what that warning surface looks like in a small, bounded migration—not to test a live controller.
The fixture was deliberately awkward
I made one synthetic Ingress for shop.example.test:
- TLS with a named Secret;
- a
/prefix route to a storefront service; - an
/api(/|$)(.*)regular-expression route to an API service; - an HTTP-to-HTTPS redirect; and
- the common ingress-nginx rewrite annotation
nginx.ingress.kubernetes.io/rewrite-target: /$2.
That last field is the important one. It changes the request URI forwarded to the application. Losing it can turn a migration that looks correct in a manifest review into an application-level break.
I built ingress2gateway from the signed v1.2.0 source tag in a disposable Docker build stage. The final run used a non-root UID, no network, a read-only filesystem, a small temporary filesystem, dropped Linux capabilities, no-new-privileges, one CPU, 256 MiB memory, and a PID limit of 64. The fixture was baked into the image, so the run did not mount a host directory.
Gateway/nginx
HTTPRoute/store-shop-example-test
HTTPRoute/store-shop-example-test-http
The generated Gateway preserved the hostname and produced separate HTTP and HTTPS listeners:
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:
- kind: Secret
name: shop-tls
It also generated a port-80 HTTPRoute that returns a 308 redirect to HTTPS. That is a concrete routing behavior worth checking, not just a generated object worth counting.
The converter gave me the review list
The conversion did not silently declare success. It emitted these warnings:
INFO Using case-insensitive regex path matches. You may want to change this.
WARN Path rewrites with capture group references are not supported
source: STANDARD_EMITTER
object: HTTPRoute: demo/store-shop-example-test
WARN Gateway API does not support configuring URL normalization
Please check if this matters for your use case.
That is the most useful result of the experiment.
The generated HTTPS route still matched the API pattern:
matches:
- path:
type: RegularExpression
value: (?i)/api(/|$)(.*).*
backendRefs:
- name: api
port: 8080
But the $2 rewrite was absent because the standard conversion cannot represent capture-group references. The route exists; the request URI semantics may not. A simple resource-by-resource diff would make that easy to miss.
Conversion is not equivalence
ingress2gateway documents an important boundary: it translates supported Ingress fields and provider-specific resources into Gateway API resources; it is not meant to copy annotations blindly. That is sensible. Many annotations are controller-specific behavior, not portable API.
The consequence is that every untranslatable annotation is a migration-review item. It should have an owner and a decision:
warning -> affected route -> expected behavior -> replacement or explicit acceptance
For this fixture, the follow-up would be to decide how the API should receive /api/... requests under the target Gateway implementation. That might require a supported rewrite mechanism, a backend change, or a decision to preserve controller-specific behavior outside the standard conversion. The converter cannot safely make that product decision.
The check I would put beside a migration PR
I would review an Ingress-to-Gateway change as a table of observable behavior, not as an object count:
| Contract | Before | After | Evidence |
|---|---|---|---|
| Hostname | shop.example.test |
listener and HTTPRoute hostname |
rendered manifests |
| TLS | Secret-backed HTTPS | certificateRefs on port 443 |
rendered manifests + target controller status |
| HTTP redirect | enabled | explicit 308 route filter |
rendered manifests + request test |
| API path match | regex | case-insensitive regex | converter warning + request test |
| URI rewrite | /$2 |
absent | converter warning; remediation required |
| Backend | api:8080 |
api:8080 |
rendered manifests |
Only the final column can establish that a controller accepts and serves the intended configuration. My Docker run did not install Gateway API CRDs, start a controller, contact a cluster, resolve DNS, or send an external request. It proved that the converter can surface an important portability gap in a repeatable synthetic input. It did not prove a production migration safe.
That distinction matters. A migration has passed the conversion stage when its generated resources parse and its warnings are understood. It has passed the routing stage only when the target controller reports accepted resources and the expected requests behave correctly.
What I would build next
The gap is not another converter. ingress2gateway already has a focused job, supports several providers and emitters, and reports unsupported behavior.
The useful companion would be a small, offline routing-contract diff: read the source Ingress plus the generated Gateway and Routes, then list hosts, path matching, backends, redirects, TLS references, and converter warnings side by side. It should flag a missing rewrite or a changed match mode as review required, never claim behavioral equivalence.
That would make the handoff from conversion output to migration review explicit—the same way a good GitOps diff turns a subtle configuration change into a question someone can answer deliberately.
Test notes: This was a smoke test in a disposable Docker image built from the ingress2gateway v1.2.0 source tag. The final container ran as UID 65532 with --network none, a read-only filesystem, /tmp as a limited tmpfs, all capabilities dropped, no-new-privileges, --pids-limit 64, --memory 256m, and --cpus 1. It used one sanitized synthetic Ingress. No Kubernetes cluster, kubeconfig, DNS provider, Secret value, or live hostname was accessed. The output YAML SHA-256 was 11aa5ed72087027043e299a6b78e9f1efd944f26b3883127cdcc375e469dbaa5.
Sources: Kubernetes on the retirement of ingress-nginx · Ingress2Gateway 1.0 announcement · ingress2gateway documentation
Related work: I built a security-impact diff for GitOps · A rollout needs a return address