Moving from Ingress NGINX to Traefik¶
Caution
This page is being continuously updated as more edge cases of the NGINX Ingress Controller to Traefik migration are being discovered.
Due to the March 2026 retirement of Ingress NGINX, Welkin is adopting Traefik instead as the standard Ingress Controller. We are making the transition as smooth as possible for both Application Developers and Platform Administrators.
Welkin Clusters will run both Ingress NGINX and Traefik during a transitional period to make sure environments have the opportunity to safely migrate. As an application developer, you can start testing on Traefik today by stepwise adapting your services with just a few changes at a time.
Prerequisites¶
Confirm both controllers are operational.
kubectl get pods -n ingress-nginx
kubectl get pods -n traefik
You should be able to resolve the *.traefik.$DOMAIN to the Traefik LoadBalancer IP using for example dig.
export TRAEFIK_EXTERNAL_LB=$(dig "*.traefik.$DOMAIN" +short)
The Migration Strategy¶
To guarantee service continuity, we utilize a side-by-side validation strategy, where Traefik is configured to shadow your existing NGINX Ingress resources. By running both controllers simultaneously, you are able to verify your services in isolation before redirecting any production traffic.
Clone and Patch¶
For all Ingress resources, follow these steps to migrate from NGINX to Traefik:
- Create a copy of the existing Ingress resource.
- Update the
ingressClassNametotraefik. - Replace NGINX-specific annotations or configuration snippets with the corresponding Traefik Middlewares.
Note
Traefik natively supports all standard Kubernetes Ingress fields.
If your Ingress uses only standard fields like paths and hosts without custom annotations, simply changing the ingressClassName is often sufficient.
Please read "Converting Annotations to Traefik Middlewares" for information about annotation support and instructions on how to create appropriate Traefik Middlewares.
Validation¶
You can verify that your services are running correctly on Traefik by sending a request directly to the Traefik LoadBalancer IP. This allows you to test the migration in isolation while your production DNS continues to point safely to NGINX.
Option 1: Client-Side Resolution¶
You can test routing without changing DNS by running:
curl --resolve example.base-domain.com:443:$TRAEFIK_EXTERNAL_LB https://example.base-domain.com
Alternatively, by updating your local /etc/hosts file to point your domain to the $TRAEFIK_EXTERNAL_LB IP.
This allows you to test the application in a browser on standard ports.
Option 2: Separate DNS¶
You can create separate Ingress objects and DNS pointing to the $TRAEFIK_EXTERNAL_LB IP.
The *.traefik.$DOMAIN DNS record pointing to the Traefik LoadBalancer can also be used directly.
The Permanent Switch¶
When validation is successful update your public DNS records to point to the Traefik LoadBalancer IP. Once propagation is complete, delete the old NGINX Ingress.
Converting Annotations to Traefik Middlewares¶
Traefik provides support for translating many common Ingress-NGINX annotations when using the nginx Ingress class name, though some may behave slightly differently.
However, not all annotations are supported. Some require additional steps and need to be converted to Traefik Middlewares. For more details on which annotations are supported or unsupported, see Traefik & Ingresses with NGINX annotations.
Caution
Due to a limitation in how Traefik's translation between Ingress NGINX and its own configuration format works: if you continue to use Ingress-NGINX annotations, you cannot use Traefik Middlewares on the same resource. We therefore recommend a full conversion to Traefik Middlewares to avoid configuration issues.
In the following sections below are step-by-step guides to help you convert various types of annotations into their Traefik Middleware equivalents.
Applying Middleware¶
To apply any Middleware to a Kubernetes Ingress resource, you add an annotation in the format traefik.ingress.kubernetes.io/router.middlewares: <namespace>-<middleware-name>@kubernetescrd.
In the annotation, you specifify the namespace and the name of the Middleware.
# Create a Middleware resource
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: request-headers
namespace: prod
spec:
headers:
customRequestHeaders:
X-Custom-Request-Added-Header: "test-request"
---
# Specify the namespace and the name of the Middleware in the annotation
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo-app
namespace: prod
annotations:
traefik.ingress.kubernetes.io/router.middlewares: prod-request-headers@kubernetescrd
spec:
ingressClassName: traefik
Rate-limiting and allowlisting¶
For rate-limiting and allowlisting -- which need a conversion from NGINX annotation to Traefik Middleware -- please find an example in our documentation on Network Model.
Here the allowlist annotation nginx.ingress.kubernetes.io/whitelist-source-range: 98.128.193.2/32 is currently not a supported annotation.
To keep the allow-list feature a Middleware resource is created:
# Blocklisted IP's will get HTTP 403.
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: demo-allowlist
namespace: <namespace>
spec:
ipAllowList:
sourceRange:
- 98.128.193.2/32
And the Traefik-specific Middleware annotation is added to the Ingress resource:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo-app
annotations:
traefik.ingress.kubernetes.io/router.middlewares: <namespace>-demo-allowlist@kubernetescrd
spec:
ingressClassName: traefik
...
Header manipulation¶
Traefik's Headers Middleware can handle several types of header manipulation. Depending on your use-case, it can replace or complement the following annotations (non-exhaustive list):
nginx.ingress.kubernetes.io/custom-headersnginx.ingress.kubernetes.io/enable-corsnginx.ingress.kubernetes.io/cors-allow-originnginx.ingress.kubernetes.io/cors-allow-headers
The Headers Middleware can also do things that in Ingress-NGINX is normally handled with configuration-snippet, such as adding custom response headers, or removing headers.
For example, here's how a custom request header can be added by the controller:
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: headers-request-added
spec:
headers:
customRequestHeaders:
X-Custom-Request-Added-Header: "test-request"
Then, annotate it on the Ingress resource:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo-app
annotations:
traefik.ingress.kubernetes.io/router.middlewares: <namespace>-headers-request-added@kubernetescrd
spec:
ingressClassName: traefik
...
If we instead want to configure it to manage CORS, it can be configured in the following fashion:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: cors-config
namespace:
spec:
headers:
accessControlAllowMethods:
- GET
- OPTIONS
- POST
- PUT
- DELETE
accessControlAllowOriginList:
- "https://app.example.com"
- "https://admin.example.com"
accessControlAllowHeaders:
- "*"
accessControlAllowCredentials: true
accessControlMaxAge: 100
addVaryHeader: true
Then apply it to your Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo-app
annotations:
traefik.ingress.kubernetes.io/router.middlewares: -cors-config@kubernetescrd
spec:
ingressClassName: traefik
...
Response Compression¶
Traefik's Compress Middleware is used to compress responses (using Zstandard, Brotli or Gzip) before sending them to the client. This replaces the common Ingress-NGINX pattern of using a configuration-snippet to inject manual compression directives:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-compression-example
annotations:
nginx.ingress.kubernetes.io/configuration-snippet: |
gzip on;
gzip_min_length 2048;
In Traefik, this can be handled by adding compress to a Middleware resource.
Adding compress to a Middleware by default adds support for Gzip, Brotli and Zstandard compression with Gzip having highest priority.
The following example demonstrates a basic Middleware configuration with a minimum byte threshold:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: traefik-compression-example
spec:
compress:
minResponseBodyBytes: 2048 # Equivalent to gzip_min_length
and then adding the annotation to the Ingress resource:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo-app
annotations:
traefik.ingress.kubernetes.io/router.middlewares: <namespace>-traefik-compression-example@kubernetescrd
spec:
ingressClassName: traefik
...
Path manipulation¶
Traefik uses specific Path Middlewares to handle URL modifications that are typically managed by rewrite annotations in NGINX. These Middlewares offer more granular control over how paths are modified before being sent to the backend service.
Common NGINX annotations that map to these Middlewares include:
nginx.ingress.kubernetes.io/rewrite-targetnginx.ingress.kubernetes.io/app-rootnginx.ingress.kubernetes.io/configuration-snippet(when used for path rewrites)
Add Prefix¶
If you need to prepend a path to the request (similar to NGINX path modification), use the AddPrefix Middleware:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: addprefix-foo
spec:
addPrefix:
prefix: /foo
Replace Path¶
To completely replace the path of a request, use the ReplacePath Middleware:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: replacepath-foo
spec:
replacePath:
path: /foo
For complex rewrites equivalent to nginx.ingress.kubernetes.io/rewrite-target, use regex-based replacement:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: replacepathregex-foo
spec:
replacePathRegex:
regex: "^/foo/(.*)"
replacement: "/bar/$1"
Strip Prefix¶
To remove a specific prefix from the path before it reaches the backend—a common requirement when the application is not "path-aware"—use StripPrefix:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: stripprefix-foo
spec:
stripPrefix:
prefixes:
- /foo
Alternatively, for dynamic path stripping based on patterns, use StripPrefixRegex:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: stripprefixregex-foo
spec:
stripPrefixRegex:
regex:
- "/foo/[a-z0-9]+/[0-9]+/"
Error handling and redirects¶
The Errors, RedirectScheme and RedirectRegex Middlewares are used to handle error responses and redirects.
It can replace or complement the following annotations (non-exhaustive list):
nginx.ingress.kubernetes.io/custom-http-errors:nginx.ingress.kubernetes.io/default-backend:nginx.ingress.kubernetes.io/ssl-redirect:nginx.ingress.kubernetes.io/force-ssl-redirect:nginx.ingress.kubernetes.io/permanent-redirect:nginx.ingress.kubernetes.io/permanent-redirect-code:nginx.ingress.kubernetes.io/temporal-redirect:nginx.ingress.kubernetes.io/temporal-redirect-code:
In some cases, these Middlewares can also replace functionality implemented via configuration-snippet annotations.
Errors¶
If you need to present a custom page when an error occurs, use the Errors Middleware:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: errors
spec:
errors:
status:
- "502"
query: /
service:
name: nginx
port: 80
Redirect Regex¶
If you need to create a permanent or temporary redirect, use the RedirectRegex Middleware:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: redirect-regex
spec:
redirectRegex:
regex: ^https?://domain\.com/(.*)
replacement: https://example.com/${1}
permanent: true
Redirect Scheme¶
If you need to redirect scheme (HTTP -> HTTPS), then use RedirectScheme Middleware:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: redirect-scheme
spec:
redirectScheme:
scheme: https
permanent: true
Sticky sessions (Session Affinity)¶
Sticky sessions, or session affinity, is supported by Traefik. More details about how it works can be found here.
Sticky sessions can replace the following Ingress-NGINX annotations:
nginx.ingress.kubernetes.io/session-cookie-max-age:nginx.ingress.kubernetes.io/session-cookie-secure:
To ensure that the sticky session cookie has a certain Max-Age and is only sent over TLS, the following annotations need to be set on the Service object.
Note
For sticky sessions, the annotations need to be set on the backend Service object, not the Ingress!
apiVersion: v1
kind: Service
metadata:
name: example-service
annotations:
traefik.ingress.kubernetes.io/service.sticky.cookie.secure: "true"
traefik.ingress.kubernetes.io/service.sticky.cookie.maxage: "600"
spec:
...
TLS Options¶
TLSOption objects can be used to customize TLS settings for ingresses.
Caution
Normally, there's no need to modify the TLS options for Traefik, since the defaults provided by Welkin is secure by default. You can read more about the defaults here. If you manually modify the settings, you may reduce the security of your HTTPS traffic!
If you need to modify the settings, similar to the Ingress-NGINX annotation nginx.ingress.kubernetes.io/ssl-ciphers:, you need to create a TLSOption.
As an example, if you want to enforce only TLSv1.3, you can use the following:
apiVersion: traefik.io/v1alpha1
kind: TLSOption
metadata:
name: very-modern-tls
spec:
minVersion: VersionTLS13
Then, annotate it on the Ingress resource:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo-app
annotations:
traefik.ingress.kubernetes.io/router.tls.options=<namespace>-very-modern-tls@kubernetescrd
spec:
ingressClassName: traefik
...
External Authentication¶
Traefik's ForwardAuth Middleware replaces the nginx.ingress.kubernetes.io/auth-url annotation. This Middleware forwards authentication to an external service before allowing requests to reach your application.
Here's an example ForwardAuth Middleware configuration:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: auth-service
namespace:
spec:
forwardAuth:
address: https://auth.example.com/verify
trustForwardHeader: true
authResponseHeaders:
- X-Auth-User
- X-Auth-Email
Then apply it to your Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo-app
annotations:
traefik.ingress.kubernetes.io/router.middlewares: -auth-service@kubernetescrd
spec:
ingressClassName: traefik
...
Metrics¶
| Ingress-NGINX Metric | Traefik equivalent | Notes |
|---|---|---|
nginx_ingress_controller_requests |
traefik_service_requests_total |
Filter by code instead of status. |
nginx_ingress_controller_request_duration_seconds |
traefik_service_request_duration_seconds |
Warning: Histogram buckets differ by default. (See longer explanation below) |
nginx_ingress_controller_response_duration_seconds |
traefik_service_request_duration_seconds |
Partial: Traefik bundles "wait + processing" into a single metric. It does not separate "upstream time" from "total time" in metrics. (See longer explanation below) |
nginx_ingress_controller_header_duration_seconds |
MISSING | |
nginx_ingress_controller_connect_duration_seconds |
MISSING | |
nginx_ingress_controller_response_size |
MISSING (Histogram) | Traefik provides a Counter (traefik_service_responses_bytes_total) but no Histogram. You can track bandwidth, but not "size distribution." |
nginx_ingress_controller_request_size |
MISSING (Histogram) | Traefik provides a Counter (traefik_service_requests_bytes_total) but no Histogram. |
nginx_ingress_controller_bytes_sent |
traefik_service_responses_bytes_total |
|
nginx_ingress_controller_nginx_process_connections |
traefik_open_connections |
|
nginx_ingress_controller_nginx_process_connections_total |
traefik_entrypoint_requests_total |
|
nginx_ingress_controller_nginx_process_cpu_seconds_total |
process_cpu_seconds_total |
|
nginx_ingress_controller_nginx_process_num_procs |
go_goroutines |
NGINX is multi-process (workers), Traefik is single-process (Goroutines). |
nginx_ingress_controller_nginx_process_oldest_start_time_seconds |
process_start_time_seconds |
|
nginx_ingress_controller_nginx_process_read_bytes_total |
traefik_entrypoint_requests_bytes_total |
|
nginx_ingress_controller_nginx_process_requests_total |
traefik_entrypoint_requests_total |
|
nginx_ingress_controller_nginx_process_resident_memory_bytes |
process_resident_memory_bytes |
|
nginx_ingress_controller_nginx_process_virtual_memory_bytes |
process_virtual_memory_bytes |
|
nginx_ingress_controller_nginx_process_write_bytes_total |
traefik_entrypoint_responses_bytes_total |
|
nginx_ingress_controller_build_info |
MISSING | Traefik does not output a specific metric for this. Could maybe use kube_pod_container_info instead. |
nginx_ingress_controller_check_success |
MISSING | Traefik lacks the this metric because it updates routing rules dynamically in memory, whereas NGINX must validate a static configuration file before every reload. |
nginx_ingress_controller_config_hash |
MISSING | Traefik does not expose a config hash. Use traefik_config_last_reload_success to verify sync timestamp. |
nginx_ingress_controller_config_last_reload_successful |
MISSING | |
nginx_ingress_controller_config_last_reload_successful_timestamp_seconds |
traefik_config_last_reload_success |
Timestamp of last successful config update. |
nginx_ingress_controller_ssl_certificate_info |
traefik_tls_certs_not_after |
Like NGINX, Traefik puts the cert details (CN, Issuer) in the labels. However, the value Differs, Traefik is the expiry timestamp and NGINX is 1. |
nginx_ingress_controller_success |
MISSING | traefik_config_reloads_total could possibly be used to prove the controller is active but they are not counting the same thing. |
nginx_ingress_controller_orphan_ingress |
MISSING | |
nginx_ingress_controller_admission_config_size |
MISSING | |
nginx_ingress_controller_admission_render_duration |
MISSING | |
nginx_ingress_controller_admission_render_ingresses |
MISSING | |
nginx_ingress_controller_admission_roundtrip_duration |
MISSING | |
nginx_ingress_controller_admission_tested_duration |
MISSING | |
nginx_ingress_controller_admission_tested_ingresses |
MISSING |
Troubleshooting¶
See Traefik's official migration docs for more troubleshooting advice.