- Published on
Migrating Between Ingress Controllers — A Zero-Downtime Strategy
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- Introduction
- Clarifying the Migration Motivation
- Building an Inventory
- Annotation Mapping Table
- Coexistence Strategy: IngressClass Separation
- Converting Annotations to CRDs/Middlewares
- Weighted DNS Cutover
- Staged Rollout and Rollback
- Verification: Traffic Comparison and Synthetic Monitors
- Pitfalls: Behavioral Differences
- Migration Checklist
- Conclusion
- References
Introduction
An Ingress Controller is infrastructure you rarely change once chosen. Yet in 2026 the number of organizations evaluating a replacement has jumped. The biggest reason is that ingress-nginx, the most widely used option, has moved into maintenance mode, and operational risk grew as several security issues (CVEs) were reported in its annotation-based configuration injection (snippet). On top of that comes the large trend of the Ingress API being frozen and the flow moving toward the Gateway API.
The problem is that the Ingress Controller is the entry point for external traffic. Replace it wrongly and your entire service goes down at once. Therefore a replacement should be approached not as a "swap it all at once" but as a zero-downtime strategy of "keeping two controllers coexisting and moving traffic over bit by bit."
In this article we walk step by step through the practical procedure: clarifying the migration motivation, building an inventory, mapping annotations, coexisting via IngressClass separation, weighted DNS cutover, staged rollout and rollback, verification, common pitfalls, and a final checklist.
Clarifying the Migration Motivation
You must first clarify "why are we changing?" so that your cutover goals and verification criteria fall into place. Common motivations include:
- Maintenance/security: ingress-nginx moving to maintenance mode, responding to snippet-related CVEs
- Gateway API transition: moving to a Gateway API native implementation in line with the Ingress-frozen trend
- Feature requirements: needing API gateway features (auth, rate limiting, transformation) or a multitenancy delegation model
- Performance/observability: dynamic reload, better metrics/tracing
- Cost: load balancer consolidation, operational simplification
The target controller differs by motivation. If the motivation is security/maintenance, a relatively simple swap to an Ingress-compatible controller like HAProxy or Traefik; if it is a Gateway API transition, a model change to something like Envoy Gateway becomes the goal.
Building an Inventory
The first practical step of the cutover is to fully understand what is currently deployed. Collect all Ingress resources with the following commands.
# List Ingresses in all namespaces
kubectl get ingress -A -o wide
# Back up the full definitions including annotations
kubectl get ingress -A -o yaml > ingress-backup.yaml
# Check the IngressClasses in use
kubectl get ingressclass
# Tally which annotations are used
kubectl get ingress -A -o json \
| jq -r '.items[].metadata.annotations | keys[]' \
| sort | uniq -c | sort -rn
The last command tallies which annotations are used and how often across the whole cluster, revealing which features you must reproduce in the new controller. Also catalog TLS Secrets, cert-manager-issued certificates, and external DNS records.
Annotation Mapping Table
The most labor-intensive task is moving annotations into the new controller's expression. Here is the correspondence for representative ingress-nginx annotations.
| Feature | ingress-nginx | Traefik | Contour |
|---|---|---|---|
| Path rewrite | rewrite-target | Middleware (ReplacePathRegex) | pathRewritePolicy |
| SSL redirect | ssl-redirect | Middleware (RedirectScheme) | virtualhost.tls auto |
| Body size limit | proxy-body-size | Middleware (Buffering) | global config |
| Rate limit | limit-rps | Middleware (RateLimit) | global/external |
| Backend protocol | backend-protocol | serversTransport | service protocol |
| Allowlist | whitelist-source-range | Middleware (IPWhiteList) | authorization/external |
As the table shows, a single ingress-nginx annotation scatters into a separate Middleware CRD in Traefik and into HTTPProxy fields or global config in Contour. In other words, recognize that this is not a one-to-one conversion but a model conversion.
Coexistence Strategy: IngressClass Separation
The core of a zero-downtime cutover is to run both controllers at the same time and clearly distinguish, via IngressClass, which one handles which Ingress.
External DNS
│
┌────────────┴────────────┐
│ (distribute by weight/record)
▼ ▼
┌──────────────┐ ┌──────────────┐
│ LB (old) │ │ LB (new) │
│ nginx ctrl │ │ traefik ctrl │
└──────┬───────┘ └──────┬───────┘
│ class: nginx │ class: traefik
▼ ▼
┌───────────────────────────────────────────┐
│ Same backend Service/Pod │
└───────────────────────────────────────────┘
Install the new controller with a separate IngressClass.
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: traefik
spec:
controller: traefik.io/ingress-controller
With this, existing Ingresses (ingressClassName: nginx) continue to be handled by the nginx controller, and only newly created resources (ingressClassName: traefik) are handled by the new controller. The two do not interfere with each other, so you can run them in parallel safely.
Converting Annotations to CRDs/Middlewares
If you are moving to Traefik, convert the ingress-nginx rewrite annotation into a Middleware.
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: strip-api-prefix
spec:
replacePathRegex:
regex: ^/api/(.*)
replacement: /$1
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: web
spec:
entryPoints:
- websecure
routes:
- match: Host(`app.example.com`) && PathPrefix(`/api`)
kind: Rule
services:
- name: api
port: 80
middlewares:
- name: strip-api-prefix
If you are moving to Contour, convert it into an HTTPProxy.
apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
name: web
spec:
virtualhost:
fqdn: app.example.com
tls:
secretName: app-tls
routes:
- conditions:
- prefix: /api
pathRewritePolicy:
replacePrefix:
- replacement: /
services:
- name: api
port: 80
The key is to create the converted resources under the new IngressClass and verify them first, isolated from existing traffic.
Weighted DNS Cutover
Once you have replicated resources to the new controller and verified them, it is time to move actual traffic. Because the two controllers each have their own LoadBalancer, you shift traffic gradually by adjusting weights in DNS.
Stage 1: new 0% - synthetic traffic only to new controller (internal verify)
Stage 2: new 5% - canary. Compare error rate / latency
Stage 3: new 25% - confirm metrics are stable
Stage 4: new 50% - even split, compare load
Stage 5: new 100% - full cutover
Stage 6: remove old - clean up old controller after a stabilization period
You can use weighted DNS (such as a routing policy) or place a shared entry point in front of the two LBs. Allow a sufficient observation period between each stage, and immediately revert to the previous weight if something is off.
Staged Rollout and Rollback
The cutover procedure at the command level looks like this.
# 1) Install the new controller (separate IngressClass)
helm install traefik traefik/traefik \
--namespace traefik --create-namespace \
--set ingressClass.name=traefik
# 2) Apply the converted resources (new class)
kubectl apply -f converted-routes/
# 3) Verify directly against the new LB endpoint
curl -H "Host: app.example.com" http://<NEW_LB_IP>/healthz
# 4) Raise the DNS weight in stages (5 -> 25 -> 50 -> 100)
# Observe metrics at each stage
# 5) If rollback is needed, set the DNS weight to 0 immediately
# Resources stay in place, so only traffic returns to the old controller
The core of rollback design is "never delete the old controller and its Ingress until a stabilization period after the cutover is complete." It must be built so that reverting only the DNS weight instantly restores the previous state.
Verification: Traffic Comparison and Synthetic Monitors
At each cutover stage, verify the following by comparison.
- Status code distribution: Is the 2xx/4xx/5xx ratio the same as the old controller?
- Latency: Have the p50/p95/p99 percentile latencies not degraded?
- TLS behavior: Are the certificate chain, SNI, and redirects identical?
- Path matching: Does the rewrite-result URL match the backend's expectation?
With a synthetic check, throwing key paths at both controllers simultaneously and diffing the responses lets you catch behavioral differences early.
# Send the same request to both LBs and compare responses
for path in / /api/users /login /static/app.js; do
old=$(curl -s -o /dev/null -w "%{http_code}" -H "Host: app.example.com" http://<OLD_LB_IP>$path)
new=$(curl -s -o /dev/null -w "%{http_code}" -H "Host: app.example.com" http://<NEW_LB_IP>$path)
echo "$path old=$old new=$new"
done
Pitfalls: Behavioral Differences
Even with seemingly identical configuration, subtle behavioral differences between controllers cause incidents. The representative ones:
- Rewrite regex differences: ingress-nginx's rewrite-target and capture-group behavior differ from Traefik/Contour. Always check slash handling and trailing-slash presence.
- Path-matching priority: Prefix vs Exact and longest-match-wins rules may differ per implementation.
- Default timeouts: Backend response timeout and idle timeout defaults differ. A long request may be cut off only on the new controller.
- Header handling: Differences in policy for adding/overwriting X-Forwarded-* headers.
- Body size limit: Differing defaults can block uploads only on the new controller.
- Regex host matching: Differences in how wildcard hosts are handled.
These differences are hard to catch by spec comparison alone; they surface only when you run real traffic through the synthetic-monitor diff above.
Migration Checklist
The items to check before and after the cutover.
[ ] Back up all Ingress inventory (kubectl get ingress -A -o yaml)
[ ] Fully tally annotations in use and build a mapping table
[ ] Catalog TLS Secrets / cert-manager-issued certificates
[ ] Install new controller with a separate IngressClass (coexist)
[ ] Author annotation -> CRD/Middleware converted resources
[ ] Verify the new LB endpoint directly (with Host header)
[ ] Diff both responses with a synthetic monitor
[ ] Stage DNS weight cutover (5->25->50->100), observe each stage
[ ] Pass status-code/latency/TLS/path-matching comparison
[ ] Rehearse rollback scenario (revert weight to 0)
[ ] Clean up old controller/old Ingress after stabilization period
[ ] Plan a Gateway API transition path for the long term
Conclusion
Replacing an Ingress Controller is a risky operation that changes your external entry point, but by coexisting two controllers via IngressClass separation and migrating gradually with DNS weights, you can do it safely with zero downtime. The core points are three. First, an accurate inventory and annotation mapping. Second, the recognition that this is a model conversion rather than a one-to-one conversion, plus synthetic-monitor verification of behavioral differences. Third, a rollback design where reverting only the DNS weight instantly restores the previous state.
Finally, since you are replacing the controller anyway, planning the transition path to the Gateway API for the long term — in line with the Ingress-frozen trend — means you won't have to go through one more big cutover next time.
References
- Kubernetes Ingress official docs: https://kubernetes.io/docs/concepts/services-networking/ingress/
- Kubernetes IngressClass docs: https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-class
- ingress-nginx annotations docs: https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/
- Traefik Kubernetes Ingress docs: https://doc.traefik.io/traefik/providers/kubernetes-ingress/
- Traefik Middleware docs: https://doc.traefik.io/traefik/middlewares/overview/
- Project Contour HTTPProxy docs: https://projectcontour.io/docs/main/config/fundamentals/
- HAProxy Kubernetes Ingress docs: https://www.haproxy.com/documentation/kubernetes-ingress/
- Gateway API migration (ingress2gateway): https://gateway-api.sigs.k8s.io/guides/migrating-from-ingress/
- cert-manager official docs: https://cert-manager.io/docs/