Why I Migrated from Traefik to Gateway API — And What I'd Do Differently
After I shipped the Ingress NGINX → Traefik migration across our three Kubernetes clusters, a lot of you asked: “Okay, but what’s next? Is Traefik the end state?”
It’s not. And I’ll say something that might surprise you given my last article: Traefik was the right move for March 2026, but Gateway API is the right move for everything after.
I’ve now migrated all 47 Ingress resources across our production clusters to Gateway API, running on Envoy Gateway. This article covers the full migration path, the tools that made it bearable, the things that broke in production, and why I think every team that migrated to Traefik should be planning their next move right now.
Why Gateway API After Traefik?
Let me be clear: I’m not saying Traefik was wrong. When Ingress NGINX went read-only in March 2026 with no security patches coming, Traefik’s NGINX compatibility layer was the fastest, lowest-risk escape route. I wrote about that migration here.
But Traefik’s compatibility layer is a bridge, not a destination. Here’s why I moved past it:
1. Annotation debt didn’t disappear — it just changed owners. With Ingress NGINX, I was managing 20+ nginx.ingress.kubernetes.io/* annotations. With Traefik, I replaced them with Traefik-specific annotations and middleware CRDs. Same problem, different vendor lock-in.
2. Gateway API is a Kubernetes standard, not a vendor feature. Envoy Gateway, Cilium, Istio, Kong, and yes — even Traefik itself all implement Gateway API. Write once, run on any controller. That’s the portability Ingress never gave us.
3. Role separation actually matters in teams larger than 5 people. Gateway API splits infrastructure (GatewayClass), cluster ops (Gateway), and application routing (HTTPRoute) into distinct resources. App developers don’t need cluster-admin to add a route. Platform teams control TLS and listeners. It’s the kind of separation of concerns we’ve been faking with namespace annotations for years.
4. Native canary deployments without hacky workarounds. Traffic splitting is a first-class citizen in HTTPRoute with weight fields. No more traefik.ingress.kubernetes.io/service.weights annotations that half the team doesn’t understand.
The Migration: What Actually Happened
Step 1: Install Gateway API CRDs
This is the only step that requires cluster-admin. You’re installing Custom Resource Definitions — no controllers yet, just the API surface.
# Standard CRDs (GatewayClass, Gateway, HTTPRoute)
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.0/standard-install.yaml
# Verify
kubectl get crd | grep gateway
# gatewayclasses.gateway.networking.k8s.io
# gateways.gateway.networking.k8s.io
# httproutes.gateway.networking.k8s.io
Gateway API v1.2 requires Kubernetes 1.26 or later. If you’re on an older cluster, that’s your first blocker.
Step 2: Choose Your Gateway Controller
This is where my Traefik experience paid off. I evaluated four options:
| Controller | Gateway API Support | Migration Effort | Notes |
|---|---|---|---|
| Envoy Gateway | v1.4 full | Medium | CNCF-backed, cleanest Gateway API implementation |
| Cilium Gateway | v1.4 full | Low (if already on Cilium CNI) | eBPF networking, fastest data plane |
| Istio | v1.4 full | High | Full service mesh — overkill if you just need ingress |
| Traefik | v1.4 via IngressRoute | Low | You already have Traefik — but then why migrate? |
I went with Envoy Gateway for two reasons: it’s the reference implementation (CNCF SIG Network builds against it), and it has no vendor lock-in beyond Envoy itself — which is already the data plane for Istio, Cilium, and half the ecosystem.
# Install Envoy Gateway
helm install envoy-gateway oci://docker.io/envoyproxy/gateway-helm \
--version v1.2.0 \
--namespace envoy-gateway-system \
--create-namespace
Step 3: Convert Ingress Resources with ingress2gateway 1.0
This tool is a game-changer. The Kubernetes SIG Network team released ingress2gateway 1.0 in March 2026 specifically to solve the migration problem. It converts Ingress resources (including annotations) to Gateway API equivalents and warns you about things it can’t translate.
# Install
go install github.com/kubernetes-sigs/[email protected]
# Convert all namespaces to Gateway API YAML
ingress2gateway print --all-namespaces --providers=ingress-nginx > gwapi.yaml
# Or convert from existing files
ingress2gateway print --input-file traefik-ingress.yaml > gwapi.yaml
The tool supports 30+ Ingress-NGINX annotations including CORS, backend TLS, regex matching, path rewrites, and timeouts. But here’s what tripped me up:
┌─ WARN ────────────────────────────────────────
│ Unsupported annotation nginx.ingress.kubernetes.io/configuration-snippet
│ source: INGRESS-NGINX
│ object: Ingress: production/api-gateway
└─
┌─ WARN ────────────────────────────────────────
│ ingress-nginx only supports TCP-level timeouts;
│ i2gw has made a best-effort translation to
│ Gateway API timeouts.request. Please verify.
│ source: INGRESS-NGINX
│ object: HTTPRoute: production/api-gateway
└─
These warnings are not noise — they’re the things that will break if you don’t handle them. The configuration-snippet annotation? That was my custom NGINX config for request body size limits. In Gateway API, you need an ExtensionRef filter or controller-specific policy. There’s no standard equivalent.
Step 4: Create the GatewayClass and Gateway
This is where the role separation becomes real. The platform team (in my case, me wearing the platform hat) creates the infrastructure:
# gateway-class.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: envoy
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
---
# gateway.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: production-gateway
namespace: infra
spec:
gatewayClassName: envoy
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: All
- name: https
protocol: HTTPS
port: 443
tls:
mode: Terminate
certificateRefs:
- name: wildcard-tls-cert
kind: Secret
namespace: infra
allowedRoutes:
namespaces:
from: All
Key thing: the allowedRoutes.from: All setting lets any namespace create routes pointing to this Gateway. That’s the permission boundary — app teams can define routes, but they can’t change the Gateway itself.
Step 5: Application Teams Define HTTPRoutes
Now each team creates their own routes. No more begging platform to update the monolithic Ingress resource:
# api-route.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: api-routes
namespace: production
spec:
parentRefs:
- name: production-gateway
namespace: infra
hostnames:
- "api.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /v2
backendRefs:
- name: api-v2
port: 8080
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: api-v1
port: 8080
The Real Win: Canary Deployments Without the Pain
This is the feature that made me stop and rethink how we’ve been doing deployments. With Ingress (and even Traefik), canary deployments required either:
- Weighted DNS records (slow propagation)
- Separate Ingress resources with manual traffic management
- Third-party tools like Flagger or Argo Rollouts
With Gateway API, it’s native:
# canary-deployment.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: api-canary
namespace: production
spec:
parentRefs:
- name: production-gateway
namespace: infra
hostnames:
- "api.example.com"
rules:
- backendRefs:
- name: api-stable
port: 8080
weight: 90
- name: api-canary
port: 8080
weight: 10
Change weight: 10 to weight: 50 to weight: 100. No DNS changes, no separate controllers, no Flagger CRDs. Just edit the YAML and apply. The Gateway controller handles the rest.
I used this for our last three deployments. The canary traffic split is handled at the proxy level — no client-side logic, no service mesh required. It just works.
What Went Wrong
I won’t pretend this was smooth. Here’s what broke:
1. Regex Path Matching Is Different
The ingress2gateway tool converted our regex paths like (/api/v[0-9]+)/users into Gateway API’s RegularExpression type. But Gateway API uses RE2 syntax, not PCRE. Our old paths with \d+ and (?i) flags silently changed behavior.
# What ingress2gateway generated:
- path:
type: RegularExpression
value: (?i)/api/v[0-9]+/users
The (?i) flag for case-insensitive matching is not supported in RE2. We had to manually convert to lowercase paths and update client configs. Cost me an afternoon of debugging 404s on mobile clients that sent /API/V1/users.
2. Cross-Namespace References Need ReferenceGrants
When I tried to route from the production namespace to the Gateway in infra, the route stayed in Pending status. The fix:
# reference-grant.yaml (in the infra namespace)
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-production-routes
namespace: infra
spec:
from:
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: production
to:
- group: ""
kind: Secret
This is a security feature, not a bug — but it’s the kind of thing that catches you off guard during migration. The error message (HTTPRoute rejected: cross-namespace reference not permitted) is at least clear.
3. Timeout Translation Is Lossy
The ingress2gateway tool translates nginx.ingress.kubernetes.io/proxy-read-timeout: "60" to timeouts.request: 60s. But NGINX had three timeout settings (read, send, connect) while Gateway API’s standard timeout model only covers request and backendRequest. If your service has long-running streaming endpoints, you need to set these manually.
# Manual timeout override needed for streaming endpoints
timeouts:
request: "5m"
backendRequest: "5m"
Common Objections
| Concern | Reality |
|---|---|
| ”We just migrated to Traefik — why move again?” | You don’t have to move controllers. Traefik supports Gateway API natively (v3.6+). You migrate the API, not the controller. Keep Traefik as your Gateway implementation if you want. |
| ”Gateway API is too complex for our team” | It’s three resource types (GatewayClass, Gateway, HTTPRoute) vs. one (Ingress) with 50 annotation variants. The learning curve is about the same, but the long-term maintainability is significantly better. |
| ”We’re too small to care about role separation” | Fair. If you’re a solo dev managing one cluster, Ingress annotations are fine. This article is for teams where app developers and platform engineers are different people. |
| ”Envoy Gateway is too new for production” | It’s CNCF-backed, used by major companies, and implements Gateway API v1.4 — the most mature standard available. If you want something older, Cilium Gateway API is equally production-ready. |
| ”What about our Traefik middleware investments?” | Gateway API’s HTTPRouteFilter replaces most Traefik middleware (header modifications, redirects, rate limiting). Custom middleware needs controller-specific ExtensionRef. Plan for a partial rewrite. |
The Decision: When to Migrate
I wouldn’t recommend dropping everything and migrating tomorrow. Here’s my framework:
Migrate to Gateway API now if:
- You have 3+ Kubernetes clusters (portability matters)
- App developers need self-service routing (role separation)
- You’re doing canary/blue-green deployments regularly
- You’re on Kubernetes 1.26+
Stick with Traefik IngressRoute for now if:
- You have fewer than 20 Ingress resources
- Your Traefik setup is stable and well-understood
- You have custom Traefik middleware that has no Gateway API equivalent
- Your team is already stretched thin from the Ingress NGINX migration
The smart play: Use Traefik as your Gateway API implementation. Traefik v3.6 supports Gateway API v1.4, so you get the standardized API without changing your controller. It’s the gradual migration path I wish I’d taken from the start.
What I’d Do Differently
If I were starting this migration today, here’s what I’d change:
-
Start with
--dry-run=serveron every converted manifest. I applied theingress2gatewayoutput directly and found issues in production. Always validate first. -
Run Gateway API alongside Traefik for at least two weeks. Don’t flip the switch. Deploy both, split traffic at the DNS level, monitor error rates, then decommission.
-
Audit regex paths manually. The automated conversion is good but not perfect. Every regex path needs a human review against RE2 syntax.
-
Set up
gwctlfrom day one. Thegateway-apiCLI tool (gwctl) gives you visibility into Gateway and route status thatkubectl getdoesn’t. Install it before you start migrating:
go install github.com/kubernetes-sigs/gateway-api/gwctl@latest
gwctl get gateways -A
gwctl get httproutes -A
The Bottom Line
Gateway API isn’t just “Ingress but with more YAML.” It’s a fundamentally different way of thinking about cluster networking — one where infrastructure, operations, and application concerns are properly separated, where canary deployments are native, and where your routing configuration isn’t locked to a specific vendor’s annotation schema.
I migrated to Traefik because I had to. I migrated to Gateway API because I should have from the start.
If you’re reading this after following my Traefik migration guide: don’t panic. Your Traefik setup isn’t going anywhere. But start planning your Gateway API migration now, use ingress2gateway to do the heavy lifting, and consider keeping Traefik as your Gateway implementation. You get the best of both worlds — standardized API, familiar controller.
What’s your next networking move? Gateway API, or sticking with your current ingress setup? Let me know — I’m tracking which approaches survive contact with production.
📚 Related Articles
Continue building your Kubernetes networking expertise:
Enjoying the content? Here are tools I personally use and recommend:
- 🌐 Hosting: Bluehost — what this blog runs on
- 🛒 Tech Gear: My Amazon Store — keyboards, monitors, dev tools I use
Purchases through my links help keep this blog ad-free 💙
Enjoyed this post?
Subscribe to the newsletter or follow on YouTube for more dev content.
🎬 Watch Shorts