Repository: weaveworks/flagger
Pull request #736 (2020-11-25 03:12:46)

Traefik support
fix: #499 

Traefik has a [TraefikService](https://doc.traefik.io/traefik/routing/services/) crd resource, which enables us to do weighted load balancing between multiple kubernetes services.

The only nuance that I've stumbled upon is that `traefik` stops to recognize `TraefikService` if one of the services in it have 0 endpoints, which is the case for promoted canary. So I have to remove canary service from `TraefikService` when canary weight is set to 0.

#### What has been done:
- Added `traefik` implementation and tests for router interface
- Added `traefik` implementation and tests for observer interface. "request-success-rate" and "request-duration" queries are implemented via prometheus metrics provider, by querying metrics exposed by `traefik`
- Added e2e tests
- CircleCI
- Documentation

## Proof
### Traffic distribution when canary is successful
![successful canary](https://user-images.githubusercontent.com/41836660/100178472-bdbbd180-2ef5-11eb-8d34-b7bb7f5dab68.png)

### Traffic distribution when canary failed
![failed canary](https://user-images.githubusercontent.com/41836660/100178518-d4622880-2ef5-11eb-80db-8075c1497cb5.png)




Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 0e2ab403..39f30353 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -160,6 +160,18 @@ jobs:
       - run: test/e2e-skipper-tests.sh
       - run: test/e2e-skipper-cleanup.sh
 
+  e2e-traefik-testing:
+    machine: true
+    steps:
+      - checkout
+      - attach_workspace:
+          at: /tmp/bin
+      - run: test/container-build.sh
+      - run: test/e2e-kind.sh
+      - run: test/e2e-traefik.sh
+      - run: test/e2e-traefik-tests.sh
+      - run: test/e2e-skipper-cleanup.sh
+
   push-helm-charts:
     docker:
       - image: circleci/golang:1.14
@@ -232,6 +244,9 @@ workflows:
       - e2e-skipper-testing:
           requires:
             - build-binary
+      - e2e-traefik-testing:
+          requires:
+            - build-binary
       - push-container:
           requires:
             - build-binary
@@ -241,6 +256,7 @@ workflows:
             - e2e-nginx-testing
             - e2e-linkerd-testing
             - e2e-skipper-testing
+            - e2e-traefik-testing
           filters:
             branches:
               only:
diff --git a/.gitbook.yaml b/.gitbook.yaml
index 1d974eca..d13970bf 100644
--- a/.gitbook.yaml
+++ b/.gitbook.yaml
@@ -12,3 +12,4 @@ redirects:
   usage/nginx-progressive-delivery: tutorials/nginx-progressive-delivery.md
   usage/skipper-progressive-delivery: tutorials/skipper-progressive-delivery.md
   usage/crossover-progressive-delivery: tutorials/crossover-progressive-delivery.md
+  usage/traefik-progressive-delivery: tutorials/traefik-progressive-delivery.md
diff --git a/README.md b/README.md
index 100d943a..8aeafee7 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ by gradually shifting traffic to the new version while measuring metrics and run
 ![flagger-overview](https://raw.githubusercontent.com/weaveworks/flagger/master/docs/diagrams/flagger-canary-overview.png)
 
 Flagger implements several deployment strategies (Canary releases, A/B testing, Blue/Green mirroring)
-using a service mesh (App Mesh, Istio, Linkerd) or an ingress controller (Contour, Gloo, NGINX, Skipper) for traffic routing.
+using a service mesh (App Mesh, Istio, Linkerd) or an ingress controller (Contour, Gloo, NGINX, Skipper, Traefik) for traffic routing.
 For release analysis, Flagger can query Prometheus, Datadog or CloudWatch
 and for alerting it uses Slack, MS Teams, Discord and Rocket.
 
@@ -38,6 +38,7 @@ Flagger documentation can be found at [docs.flagger.app](https://docs.flagger.ap
   * [Gloo](https://docs.flagger.app/tutorials/gloo-progressive-delivery)
   * [NGINX Ingress](https://docs.flagger.app/tutorials/nginx-progressive-delivery)
   * [Skipper](https://docs.flagger.app/tutorials/skipper-progressive-delivery)
+  * [Traefik](https://docs.flagger.app/tutorials/traefik-progressive-delivery)
   * [Kubernetes Blue/Green](https://docs.flagger.app/tutorials/kubernetes-blue-green)
 
 ### Who is using Flagger
@@ -73,7 +74,7 @@ metadata:
   namespace: test
 spec:
   # service mesh provider (optional)
-  # can be: kubernetes, istio, linkerd, appmesh, nginx, skipper, contour, gloo, supergloo
+  # can be: kubernetes, istio, linkerd, appmesh, nginx, skipper, contour, gloo, supergloo, traefik
   provider: istio
   # deployment reference
   targetRef:
@@ -198,16 +199,16 @@ For more details on how the canary analysis and promotion works please [read the
 
 **Ingress**
 
-| Feature                                    | Contour            | Gloo               | NGINX              | Skipper            |
-| ------------------------------------------ | ------------------ | ------------------ | ------------------ | ------------------ |
-| Canary deployments (weighted traffic)      | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
-| A/B testing (headers and cookies routing)  | :heavy_check_mark: | :heavy_minus_sign: | :heavy_check_mark: | :heavy_minus_sign: |
-| Blue/Green deployments (traffic switch)    | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
-| Webhooks (acceptance/load testing)         | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
-| Manual gating (approve/pause/resume)       | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
-| Request success rate check (L7 metric)     | :heavy_check_mark: | :heavy_check_mark: | :heavy_minus_sign: | :heavy_check_mark: |
-| Request duration check (L7 metric)         | :heavy_check_mark: | :heavy_check_mark: | :heavy_minus_sign: | :heavy_check_mark: |
-| Custom metric checks                       | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
+| Feature                                    | Contour            | Gloo               | NGINX              | Skipper            | Traefik            |
+| ------------------------------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ |
+| Canary deployments (weighted traffic)      | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
+| A/B testing (headers and cookies routing)  | :heavy_check_mark: | :heavy_minus_sign: | :heavy_check_mark: | :heavy_minus_sign: | :heavy_minus_sign: |
+| Blue/Green deployments (traffic switch)    | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
+| Webhooks (acceptance/load testing)         | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
+| Manual gating (approve/pause/resume)       | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
+| Request success rate check (L7 metric)     | :heavy_check_mark: | :heavy_check_mark: | :heavy_minus_sign: | :heavy_check_mark: | :heavy_check_mark: |
+| Request duration check (L7 metric)         | :heavy_check_mark: | :heavy_check_mark: | :heavy_minus_sign: | :heavy_check_mark: | :heavy_check_mark: |
+| Custom metric checks                       | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
 
 ### Roadmap
 
diff --git a/charts/flagger/README.md b/charts/flagger/README.md
index 0c684acc..d7d51631 100644
--- a/charts/flagger/README.md
+++ b/charts/flagger/README.md
@@ -7,7 +7,7 @@ Flagger can run automated application analysis, testing, promotion and rollback
 * A/B Testing (HTTP headers and cookies traffic routing)
 * Blue/Green (traffic switching and mirroring)
 
-Flagger works with service mesh solutions (Istio, Linkerd, AWS App Mesh) and with Kubernetes ingress controllers (NGINX, Skipper, Gloo, Contour).
+Flagger works with service mesh solutions (Istio, Linkerd, AWS App Mesh) and with Kubernetes ingress controllers (NGINX, Skipper, Gloo, Contour, Traefik).
 Flagger can be configured to send alerts to various chat platforms such as Slack, Microsoft Teams, Discord and Rocket.
 
 ## Prerequisites
diff --git a/charts/flagger/templates/rbac.yaml b/charts/flagger/templates/rbac.yaml
index 506c1783..16a9151f 100644
--- a/charts/flagger/templates/rbac.yaml
+++ b/charts/flagger/templates/rbac.yaml
@@ -172,6 +172,18 @@ rules:
       - update
       - patch
       - delete
+  - apiGroups:
+    - traefik.containo.us
+    resources:
+    - traefikservices
+    verbs:
+    - get
+    - list
+    - watch
+    - create
+    - update
+    - patch
+    - delete
   - nonResourceURLs:
       - /version
     verbs:
diff --git a/cmd/flagger/main.go b/cmd/flagger/main.go
index d0c54d77..933d73a0 100644
--- a/cmd/flagger/main.go
+++ b/cmd/flagger/main.go
@@ -84,7 +84,7 @@ func init() {
 	flag.BoolVar(&zapReplaceGlobals, "zap-replace-globals", false, "Whether to change the logging level of the global zap logger.")
 	flag.StringVar(&zapEncoding, "zap-encoding", "json", "Zap logger encoding.")
 	flag.StringVar(&namespace, "namespace", "", "Namespace that flagger would watch canary object.")
-	flag.StringVar(&meshProvider, "mesh-provider", "istio", "Service mesh provider, can be istio, linkerd, appmesh, contour, gloo, nginx or skipper.")
+	flag.StringVar(&meshProvider, "mesh-provider", "istio", "Service mesh provider, can be istio, linkerd, appmesh, contour, gloo, nginx, skipper or traefik.")
 	flag.StringVar(&selectorLabels, "selector-labels", "app,name,app.kubernetes.io/name", "List of pod labels that Flagger uses to create pod selectors.")
 	flag.StringVar(&ingressAnnotationsPrefix, "ingress-annotations-prefix", "nginx.ingress.kubernetes.io", "Annotations prefix for NGINX ingresses.")
 	flag.StringVar(&ingressClass, "ingress-class", "", "Ingress class used for annotating HTTPProxy objects.")
diff --git a/docs/gitbook/README.md b/docs/gitbook/README.md
index fae6f42f..b8c37f5c 100644
--- a/docs/gitbook/README.md
+++ b/docs/gitbook/README.md
@@ -5,7 +5,7 @@ description: Flagger is a progressive delivery Kubernetes operator
 # Introduction
 
 [Flagger](https://github.com/weaveworks/flagger) is a **Kubernetes** operator that automates the promotion of
-canary deployments using **Istio**, **Linkerd**, **App Mesh**, **NGINX**, **Skipper**, **Contour** or **Gloo** routing for
+canary deployments using **Istio**, **Linkerd**, **App Mesh**, **NGINX**, **Skipper**, **Contour**, **Gloo** or **Traefik** routing for
 traffic shifting and **Prometheus** metrics for canary analysis. The canary analysis can be extended with webhooks for
 running system integration/acceptance tests, load tests, or any other custom validation.
 
@@ -40,6 +40,7 @@ After install Flagger, you can follow one of the tutorials:
 * [Gloo](tutorials/gloo-progressive-delivery.md)
 * [NGINX Ingress](tutorials/nginx-progressive-delivery.md)
 * [Skipper Ingress](tutorials/skipper-progressive-delivery.md)
+* [Traefik](tutorials/traefik-progressive-delivery.md)
 
 **Hands-on GitOps workshops**
 
diff --git a/docs/gitbook/SUMMARY.md b/docs/gitbook/SUMMARY.md
index 84301984..0fcd3038 100644
--- a/docs/gitbook/SUMMARY.md
+++ b/docs/gitbook/SUMMARY.md
@@ -28,6 +28,7 @@
 * [Gloo Canary Deployments](tutorials/gloo-progressive-delivery.md)
 * [NGINX Canary Deployments](tutorials/nginx-progressive-delivery.md)
 * [Skipper Canary Deployments](tutorials/skipper-progressive-delivery.md)
+* [Traefik Canary Deployments](tutorials/traefik-progressive-delivery.md)
 * [Blue/Green Deployments](tutorials/kubernetes-blue-green.md)
 * [Crossover Canary Deployments](tutorials/crossover-progressive-delivery.md)
 * [Canary analysis with Prometheus Operator](tutorials/prometheus-operator.md)
diff --git a/docs/gitbook/install/flagger-install-on-kubernetes.md b/docs/gitbook/install/flagger-install-on-kubernetes.md
index 07b040d1..40045bcc 100644
--- a/docs/gitbook/install/flagger-install-on-kubernetes.md
+++ b/docs/gitbook/install/flagger-install-on-kubernetes.md
@@ -78,6 +78,7 @@ For ingress controllers, the install instructions are:
 * [Gloo](https://docs.flagger.app/tutorials/gloo-progressive-delivery)
 * [NGINX](https://docs.flagger.app/tutorials/nginx-progressive-delivery)
 * [Skipper](https://docs.flagger.app/tutorials/skipper-progressive-delivery)
+* [Traefik](https://docs.flagger.app/tutorials/traefik-progressive-delivery)
 
 Enable **Slack** notifications:
 
@@ -199,7 +200,7 @@ kustomize build https://github.com/weaveworks/flagger/kustomize/linkerd?ref=v1.0
 
 **Generic installer**
 
-Install Flagger and Prometheus for Contour, Gloo, NGINX or Skipper ingress:
+Install Flagger and Prometheus for Contour, Gloo, NGINX, Skipper, or Traefik ingress:
 
 ```bash
 kustomize build https://github.com/weaveworks/flagger/kustomize/kubernetes | kubectl apply -f -
@@ -220,7 +221,7 @@ metadata:
   name: app
   namespace: test
 spec:
-  # can be: kubernetes, istio, linkerd, appmesh, nginx, skipper, gloo
+  # can be: kubernetes, istio, linkerd, appmesh, nginx, skipper, gloo, traefik
   # use the kubernetes provider for Blue/Green style deployments
   provider: nginx
 ```
diff --git a/docs/gitbook/tutorials/traefik-progressive-delivery.md b/docs/gitbook/tutorials/traefik-progressive-delivery.md
new file mode 100644
index 00000000..77d95669
--- /dev/null
+++ b/docs/gitbook/tutorials/traefik-progressive-delivery.md
@@ -0,0 +1,367 @@
+# Traefik Canary Deployments
+
+This guide shows you how to use the [Traefik](https://doc.traefik.io/traefik/) and Flagger to automate canary deployments.
+
+## Prerequisites
+
+Flagger requires a Kubernetes cluster **v1.14** or newer and Traefik **v2.3** or newer.
+
+Install Traefik with Helm v3:
+
+```bash
+helm repo add traefik https://helm.traefik.io/traefik
+kubectl create ns traefik
+helm upgrade -i traefik traefik/traefik \
+--namespace traefik \
+--set additionalArguments="--metrics.prometheus=true"
+```
+
+Install Flagger and the Prometheus add-on in the same namespace as Traefik:
+
+```bash
+helm repo add flagger https://flagger.app
+
+helm upgrade -i flagger flagger/flagger \
+--namespace traefik \
+--set prometheus.install=true \
+--set meshProvider=traefik
+```
+
+## Bootstrap
+
+Flagger takes a Kubernetes deployment and optionally a horizontal pod autoscaler (HPA),
+then creates a series of objects (Kubernetes deployments, ClusterIP services and TraefikService).
+These objects expose the application outside the cluster and drive the canary analysis and promotion.
+
+Create a test namespace:
+
+```bash
+kubectl create ns test
+```
+
+Create a deployment and a horizontal pod autoscaler:
+
+```bash
+kubectl apply -k github.com/weaveworks/flagger//kustomize/podinfo
+```
+
+Deploy the load testing service to generate traffic during the canary analysis:
+
+```bash
+helm upgrade -i flagger-loadtester flagger/loadtester \
+--namespace=test
+```
+
+Create Traefik IngressRoute that references TraefikService generated by Flagger 
+(replace `app.example.com` with your own domain):
+
+```yaml
+apiVersion: traefik.containo.us/v1alpha1
+kind: IngressRoute
+metadata:
+  name: podinfo
+  namespace: test
+spec:
+  entryPoints:
+    - web
+  routes:
+    - match: Host(`app.example.com`)
+      kind: Rule
+      services:
+        - name: podinfo
+          kind: TraefikService
+          port: 80
+```
+
+Save the above resource as podinfo-ingressroute.yaml and then apply it:
+
+```bash
+kubectl apply -f ./podinfo-ingressroute.yaml
+```
+
+Create a canary custom resource (replace `app.example.com` with your own domain):
+
+```yaml
+apiVersion: flagger.app/v1beta1
+kind: Canary
+metadata:
+  name: podinfo
+  namespace: test
+spec:
+  provider: traefik
+  # deployment reference
+  targetRef:
+    apiVersion: apps/v1
+    kind: Deployment
+    name: podinfo
+  # HPA reference (optional)
+  autoscalerRef:
+    apiVersion: autoscaling/v2beta1
+    kind: HorizontalPodAutoscaler
+    name: podinfo
+  # the maximum time in seconds for the canary deployment
+  # to make progress before it is rollback (default 600s)
+  progressDeadlineSeconds: 60
+  service:
+    # ClusterIP port number
+    port: 80
+    # container port number or name
+    targetPort: 9898
+  analysis:
+    # schedule interval (default 60s)
+    interval: 10s
+    # max number of failed metric checks before rollback
+    threshold: 10
+    # max traffic percentage routed to canary
+    # percentage (0-100)
+    maxWeight: 50
+    # canary increment step
+    # percentage (0-100)
+    stepWeight: 5
+    # Traefik Prometheus checks
+    metrics:
+    - name: request-success-rate
+      interval: 1m
+      # minimum req success rate (non 5xx responses)
+      # percentage (0-100)
+      thresholdRange:
+        min: 99
+    - name: request-duration
+      interval: 1m
+      # maximum req duration P99
+      # milliseconds
+      thresholdRange:
+        max: 500
+    webhooks:
+      - name: acceptance-test
+        type: pre-rollout
+        url: http://flagger-loadtester.test/
+        timeout: 10s
+        metadata:
+          type: bash
+          cmd: "curl -sd 'test' http://podinfo-canary.test/token | grep token"
+      - name: load-test
+        type: rollout
+        url: http://flagger-loadtester.test/
+        timeout: 5s
+        metadata:
+          type: cmd
+          cmd: "hey -z 10m -q 10 -c 2 -host app.example.com http://traefik.traefik"
+          logCmdOutput: "true"
+```
+
+Save the above resource as podinfo-canary.yaml and then apply it:
+
+```bash
+kubectl apply -f ./podinfo-canary.yaml
+```
+
+After a couple of seconds Flagger will create the canary objects:
+
+```bash
+# applied 
+deployment.apps/podinfo
+horizontalpodautoscaler.autoscaling/podinfo
+canary.flagger.app/podinfo
+
+# generated 
+deployment.apps/podinfo-primary
+horizontalpodautoscaler.autoscaling/podinfo-primary
+service/podinfo
+service/podinfo-canary
+service/podinfo-primary
+traefikservice.traefik.containo.us/podinfo
+```
+
+## Automated canary promotion
+
+Flagger implements a control loop that gradually shifts traffic to the canary while measuring
+key performance indicators like HTTP requests success rate, requests average duration and pod health.
+Based on analysis of the KPIs a canary is promoted or aborted, and the analysis result is published to Slack or MS Teams.
+
+![Flagger Canary Stages](https://raw.githubusercontent.com/weaveworks/flagger/master/docs/diagrams/flagger-canary-steps.png)
+
+Trigger a canary deployment by updating the container image:
+
+```bash
+kubectl -n test set image deployment/podinfo \
+podinfod=stefanprodan/podinfo:4.0.6
+```
+
+Flagger detects that the deployment revision changed and starts a new rollout:
+
+```text
+kubectl -n test describe canary/podinfo
+
+Status:
+  Canary Weight:         0
+  Failed Checks:         0
+  Phase:                 Succeeded
+Events:
+ New revision detected! Scaling up podinfo.test
+ Waiting for podinfo.test rollout to finish: 0 of 1 updated replicas are available
+ Pre-rollout check acceptance-test passed
+ Advance podinfo.test canary weight 5
+ Advance podinfo.test canary weight 10
+ Advance podinfo.test canary weight 15
+ Advance podinfo.test canary weight 20
+ Advance podinfo.test canary weight 25
+ Advance podinfo.test canary weight 30
+ Advance podinfo.test canary weight 35
+ Advance podinfo.test canary weight 40
+ Advance podinfo.test canary weight 45
+ Advance podinfo.test canary weight 50
+ Copying podinfo.test template spec to podinfo-primary.test
+ Waiting for podinfo-primary.test rollout to finish: 1 of 2 updated replicas are available
+ Routing all traffic to primary
+ Promotion completed! Scaling down podinfo.test
+```
+
+**Note** that if you apply new changes to the deployment during the canary analysis, Flagger will restart the analysis.
+
+You can monitor all canaries with:
+
+```bash
+watch kubectl get canaries --all-namespaces
+
+NAMESPACE   NAME        STATUS        WEIGHT   LASTTRANSITIONTIME
+test        podinfo-2   Progressing   30       2020-08-14T12:32:12Z
+test        podinfo     Succeeded     0        2020-08-14T11:23:88Z
+```
+
+## Automated rollback
+
+During the canary analysis you can generate HTTP 500 errors to test if Flagger pauses and rolls back the faulted version.
+
+Trigger another canary deployment:
+
+```bash
+kubectl -n test set image deployment/podinfo \
+podinfod=stefanprodan/podinfo:4.0.6
+```
+
+Exec into the load tester pod with:
+
+```bash
+kubectl -n test exec -it deploy/flagger-loadtester bash
+```
+
+Generate HTTP 500 errors:
+
+```bash
+hey -z 1m -c 5 -q 5 http://app.example.com/status/500
+```
+
+Generate latency:
+
+```bash
+watch -n 1 curl http://app.example.com/delay/1
+```
+
+When the number of failed checks reaches the canary analysis threshold, the traffic is routed back to the primary,
+the canary is scaled to zero and the rollout is marked as failed.
+
+```text
+kubectl -n traefik logs deploy/flagger -f | jq .msg
+
+New revision detected! Scaling up podinfo.test
+Canary deployment podinfo.test not ready: waiting for rollout to finish: 0 of 1 updated replicas are available
+Starting canary analysis for podinfo.test
+Pre-rollout check acceptance-test passed
+Advance podinfo.test canary weight 5
+Advance podinfo.test canary weight 10
+Advance podinfo.test canary weight 15
+Advance podinfo.test canary weight 20
+Halt podinfo.test advancement success rate 53.42% < 99%
+Halt podinfo.test advancement success rate 53.19% < 99%
+Halt podinfo.test advancement success rate 48.05% < 99%
+Rolling back podinfo.test failed checks threshold reached 3
+Canary failed! Scaling down podinfo.test
+```
+
+## Custom metrics
+
+The canary analysis can be extended with Prometheus queries.
+
+Create a metric template and apply it on the cluster:
+
+```yaml
+apiVersion: flagger.app/v1beta1
+kind: MetricTemplate
+metadata:
+  name: not-found-percentage
+  namespace: test
+spec:
+  provider:
+    type: prometheus
+    address: http://flagger-prometheus.traefik:9090
+  query: |
+    sum(
+      rate(
+        traefik_service_request_duration_seconds_bucket{
+          service=~"{{ namespace }}-{{ target }}-canary-[0-9a-zA-Z-]+@kubernetescrd",
+          code!="404",
+        }[{{ interval }}]
+      )
+    )
+    /
+    sum(
+      rate(
+        traefik_service_request_duration_seconds_bucket{
+          service=~"{{ namespace }}-{{ target }}-canary-[0-9a-zA-Z-]+@kubernetescrd",
+        }[{{ interval }}]
+      )
+    ) * 100
+```
+
+Edit the canary analysis and add the not found error rate check:
+
+```yaml
+  analysis:
+    metrics:
+      - name: "404s percentage"
+        templateRef:
+          name: not-found-percentage
+        thresholdRange:
+          max: 5
+        interval: 1m
+```
+
+The above configuration validates the canary by checking if the HTTP 404 req/sec percentage is
+below 5 percent of the total traffic. If the 404s rate reaches the 5% threshold, then the canary fails.
+
+Trigger a canary deployment by updating the container image:
+
+```bash
+kubectl -n test set image deployment/podinfo \
+podinfod=stefanprodan/podinfo:4.0.6
+```
+
+Generate 404s:
+
+```bash
+watch curl http://app.example.com/status/400
+```
+
+Watch Flagger logs:
+
+```text
+kubectl -n traefik logs deployment/flagger -f | jq .msg
+
+Starting canary deployment for podinfo.test
+Advance podinfo.test canary weight 5
+Advance podinfo.test canary weight 10
+Advance podinfo.test canary weight 15
+Halt podinfo.test advancement 404s percentage 6.20 > 5
+Halt podinfo.test advancement 404s percentage 6.45 > 5
+Halt podinfo.test advancement 404s percentage 7.60 > 5
+Halt podinfo.test advancement 404s percentage 8.69 > 5
+Halt podinfo.test advancement 404s percentage 9.70 > 5
+Rolling back podinfo.test failed checks threshold reached 5
+Canary failed! Scaling down podinfo.test
+```
+
+If you have [alerting](../usage/alerting.md) configured,
+Flagger will send a notification with the reason why the canary failed.
+
+For an in-depth look at the analysis process read the [usage docs](../usage/how-it-works.md).
diff --git a/docs/gitbook/usage/deployment-strategies.md b/docs/gitbook/usage/deployment-strategies.md
index fdbadf1d..e96a12e6 100644
--- a/docs/gitbook/usage/deployment-strategies.md
+++ b/docs/gitbook/usage/deployment-strategies.md
@@ -2,7 +2,7 @@
 
 Flagger can run automated application analysis, promotion and rollback for the following deployment strategies:
 * **Canary Release** (progressive traffic shifting)
-    * Istio, Linkerd, App Mesh, NGINX, Skipper, Contour, Gloo
+    * Istio, Linkerd, App Mesh, NGINX, Skipper, Contour, Gloo, Traefik
 * **A/B Testing** (HTTP headers and cookies traffic routing)
     * Istio, App Mesh, NGINX, Contour
 * **Blue/Green** (traffic switching)
diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh
index eaed5c60..5faf7977 100755
--- a/hack/update-codegen.sh
+++ b/hack/update-codegen.sh
@@ -30,7 +30,7 @@ chmod +x ${CODEGEN_PKG}/generate-groups.sh
 
 ${CODEGEN_PKG}/generate-groups.sh all \
     github.com/weaveworks/flagger/pkg/client github.com/weaveworks/flagger/pkg/apis \
-    "flagger:v1beta1 appmesh:v1beta2 appmesh:v1beta1 istio:v1alpha3 smi:v1alpha1 smi:v1alpha2 gloo:v1 projectcontour:v1" \
+    "flagger:v1beta1 appmesh:v1beta2 appmesh:v1beta1 istio:v1alpha3 smi:v1alpha1 smi:v1alpha2 gloo:v1 projectcontour:v1 traefik:v1alpha1" \
     --output-base "${TEMP_DIR}" \
     --go-header-file ${SCRIPT_ROOT}/hack/boilerplate.go.txt
 
diff --git a/pkg/apis/flagger/v1beta1/provider.go b/pkg/apis/flagger/v1beta1/provider.go
index a27bbc88..e81018ed 100644
--- a/pkg/apis/flagger/v1beta1/provider.go
+++ b/pkg/apis/flagger/v1beta1/provider.go
@@ -10,4 +10,5 @@ const (
 	NGINXProvider      string = "nginx"
 	KubernetesProvider string = "kubernetes"
 	SkipperProvider    string = "skipper"
+	TraefikProvider    string = "traefik"
 )
diff --git a/pkg/apis/traefik/register.go b/pkg/apis/traefik/register.go
new file mode 100644
index 00000000..d5c31e73
--- /dev/null
+++ b/pkg/apis/traefik/register.go
@@ -0,0 +1,5 @@
+package traefik
+
+const (
+	GroupName = "traefik.containo.us"
+)
diff --git a/pkg/apis/traefik/v1alpha1/doc.go b/pkg/apis/traefik/v1alpha1/doc.go
new file mode 100644
index 00000000..71b5219b
--- /dev/null
+++ b/pkg/apis/traefik/v1alpha1/doc.go
@@ -0,0 +1,5 @@
+// +k8s:deepcopy-gen=package
+
+// Package v1alpha1 is the v1alpha1 version of the API.
+// +groupName=traefik.containo.us
+package v1alpha1
diff --git a/pkg/apis/traefik/v1alpha1/register.go b/pkg/apis/traefik/v1alpha1/register.go
new file mode 100755
index 00000000..19604fec
--- /dev/null
+++ b/pkg/apis/traefik/v1alpha1/register.go
@@ -0,0 +1,36 @@
+package v1alpha1
+
+import (
+	"github.com/weaveworks/flagger/pkg/apis/traefik"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+)
+
+// SchemeGroupVersion is group version used to register these objects
+var SchemeGroupVersion = schema.GroupVersion{Group: traefik.GroupName, Version: "v1alpha1"}
+
+// Kind takes an unqualified kind and returns back a Group qualified GroupKind
+func Kind(kind string) schema.GroupKind {
+	return SchemeGroupVersion.WithKind(kind).GroupKind()
+}
+
+// Resource takes an unqualified resource and returns a Group qualified GroupResource
+func Resource(resource string) schema.GroupResource {
+	return SchemeGroupVersion.WithResource(resource).GroupResource()
+}
+
+var (
+	SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
+	AddToScheme   = SchemeBuilder.AddToScheme
+)
+
+// Adds the list of known types to Scheme.
+func addKnownTypes(scheme *runtime.Scheme) error {
+	scheme.AddKnownTypes(SchemeGroupVersion,
+		&TraefikService{},
+		&TraefikServiceList{},
+	)
+	metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
+	return nil
+}
diff --git a/pkg/apis/traefik/v1alpha1/types.go b/pkg/apis/traefik/v1alpha1/types.go
new file mode 100644
index 00000000..2794d7bc
--- /dev/null
+++ b/pkg/apis/traefik/v1alpha1/types.go
@@ -0,0 +1,47 @@
+package v1alpha1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// +genclient
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// TraefikService is the specification for a service (that an IngressRoute refers
+// to) that is usually not a terminal service (i.e. not a pod of servers), as
+// opposed to a Kubernetes Service. That is to say, it usually refers to other
+// (children) services, which themselves can be TraefikServices or Services.
+type TraefikService struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata"`
+
+	Spec ServiceSpec `json:"spec"`
+}
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// TraefikServiceList is a list of TraefikService resources.
+type TraefikServiceList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata"`
+
+	Items []TraefikService `json:"items"`
+}
+
+// ServiceSpec defines whether a TraefikService is a load-balancer of services or a
+// mirroring service.
+type ServiceSpec struct {
+	Weighted *WeightedRoundRobin `json:"weighted,omitempty"`
+}
+
+// WeightedRoundRobin defines a load-balancer of services.
+type WeightedRoundRobin struct {
+	Services []Service `json:"services,omitempty"`
+}
+
+type Service struct {
+	Name      string `json:"name"`
+	Namespace string `json:"namespace"`
+	Port      int32  `json:"port"`
+	Weight    uint   `json:"weight,omitempty"`
+}
diff --git a/pkg/apis/traefik/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/traefik/v1alpha1/zz_generated.deepcopy.go
new file mode 100644
index 00000000..7b07d795
--- /dev/null
+++ b/pkg/apis/traefik/v1alpha1/zz_generated.deepcopy.go
@@ -0,0 +1,143 @@
+// +build !ignore_autogenerated
+
+/*
+Copyright The Flagger Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by deepcopy-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	runtime "k8s.io/apimachinery/pkg/runtime"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Service) DeepCopyInto(out *Service) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Service.
+func (in *Service) DeepCopy() *Service {
+	if in == nil {
+		return nil
+	}
+	out := new(Service)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ServiceSpec) DeepCopyInto(out *ServiceSpec) {
+	*out = *in
+	if in.Weighted != nil {
+		in, out := &in.Weighted, &out.Weighted
+		*out = new(WeightedRoundRobin)
+		(*in).DeepCopyInto(*out)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceSpec.
+func (in *ServiceSpec) DeepCopy() *ServiceSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(ServiceSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TraefikService) DeepCopyInto(out *TraefikService) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TraefikService.
+func (in *TraefikService) DeepCopy() *TraefikService {
+	if in == nil {
+		return nil
+	}
+	out := new(TraefikService)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *TraefikService) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TraefikServiceList) DeepCopyInto(out *TraefikServiceList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]TraefikService, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TraefikServiceList.
+func (in *TraefikServiceList) DeepCopy() *TraefikServiceList {
+	if in == nil {
+		return nil
+	}
+	out := new(TraefikServiceList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *TraefikServiceList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *WeightedRoundRobin) DeepCopyInto(out *WeightedRoundRobin) {
+	*out = *in
+	if in.Services != nil {
+		in, out := &in.Services, &out.Services
+		*out = make([]Service, len(*in))
+		copy(*out, *in)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WeightedRoundRobin.
+func (in *WeightedRoundRobin) DeepCopy() *WeightedRoundRobin {
+	if in == nil {
+		return nil
+	}
+	out := new(WeightedRoundRobin)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/pkg/client/clientset/versioned/clientset.go b/pkg/client/clientset/versioned/clientset.go
index fc0b22c3..81853527 100644
--- a/pkg/client/clientset/versioned/clientset.go
+++ b/pkg/client/clientset/versioned/clientset.go
@@ -29,6 +29,7 @@ import (
 	projectcontourv1 "github.com/weaveworks/flagger/pkg/client/clientset/versioned/typed/projectcontour/v1"
 	splitv1alpha1 "github.com/weaveworks/flagger/pkg/client/clientset/versioned/typed/smi/v1alpha1"
 	splitv1alpha2 "github.com/weaveworks/flagger/pkg/client/clientset/versioned/typed/smi/v1alpha2"
+	traefikv1alpha1 "github.com/weaveworks/flagger/pkg/client/clientset/versioned/typed/traefik/v1alpha1"
 	discovery "k8s.io/client-go/discovery"
 	rest "k8s.io/client-go/rest"
 	flowcontrol "k8s.io/client-go/util/flowcontrol"
@@ -44,6 +45,7 @@ type Interface interface {
 	ProjectcontourV1() projectcontourv1.ProjectcontourV1Interface
 	SplitV1alpha1() splitv1alpha1.SplitV1alpha1Interface
 	SplitV1alpha2() splitv1alpha2.SplitV1alpha2Interface
+	TraefikV1alpha1() traefikv1alpha1.TraefikV1alpha1Interface
 }
 
 // Clientset contains the clients for groups. Each group has exactly one
@@ -58,6 +60,7 @@ type Clientset struct {
 	projectcontourV1   *projectcontourv1.ProjectcontourV1Client
 	splitV1alpha1      *splitv1alpha1.SplitV1alpha1Client
 	splitV1alpha2      *splitv1alpha2.SplitV1alpha2Client
+	traefikV1alpha1    *traefikv1alpha1.TraefikV1alpha1Client
 }
 
 // AppmeshV1beta2 retrieves the AppmeshV1beta2Client
@@ -100,6 +103,11 @@ func (c *Clientset) SplitV1alpha2() splitv1alpha2.SplitV1alpha2Interface {
 	return c.splitV1alpha2
 }
 
+// TraefikV1alpha1 retrieves the TraefikV1alpha1Client
+func (c *Clientset) TraefikV1alpha1() traefikv1alpha1.TraefikV1alpha1Interface {
+	return c.traefikV1alpha1
+}
+
 // Discovery retrieves the DiscoveryClient
 func (c *Clientset) Discovery() discovery.DiscoveryInterface {
 	if c == nil {
@@ -153,6 +161,10 @@ func NewForConfig(c *rest.Config) (*Clientset, error) {
 	if err != nil {
 		return nil, err
 	}
+	cs.traefikV1alpha1, err = traefikv1alpha1.NewForConfig(&configShallowCopy)
+	if err != nil {
+		return nil, err
+	}
 
 	cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(&configShallowCopy)
 	if err != nil {
@@ -173,6 +185,7 @@ func NewForConfigOrDie(c *rest.Config) *Clientset {
 	cs.projectcontourV1 = projectcontourv1.NewForConfigOrDie(c)
 	cs.splitV1alpha1 = splitv1alpha1.NewForConfigOrDie(c)
 	cs.splitV1alpha2 = splitv1alpha2.NewForConfigOrDie(c)
+	cs.traefikV1alpha1 = traefikv1alpha1.NewForConfigOrDie(c)
 
 	cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c)
 	return &cs
@@ -189,6 +202,7 @@ func New(c rest.Interface) *Clientset {
 	cs.projectcontourV1 = projectcontourv1.New(c)
 	cs.splitV1alpha1 = splitv1alpha1.New(c)
 	cs.splitV1alpha2 = splitv1alpha2.New(c)
+	cs.traefikV1alpha1 = traefikv1alpha1.New(c)
 
 	cs.DiscoveryClient = discovery.NewDiscoveryClient(c)
 	return &cs
diff --git a/pkg/client/clientset/versioned/fake/clientset_generated.go b/pkg/client/clientset/versioned/fake/clientset_generated.go
index d68d1142..dcb551b9 100644
--- a/pkg/client/clientset/versioned/fake/clientset_generated.go
+++ b/pkg/client/clientset/versioned/fake/clientset_generated.go
@@ -36,6 +36,8 @@ import (
 	fakesplitv1alpha1 "github.com/weaveworks/flagger/pkg/client/clientset/versioned/typed/smi/v1alpha1/fake"
 	splitv1alpha2 "github.com/weaveworks/flagger/pkg/client/clientset/versioned/typed/smi/v1alpha2"
 	fakesplitv1alpha2 "github.com/weaveworks/flagger/pkg/client/clientset/versioned/typed/smi/v1alpha2/fake"
+	traefikv1alpha1 "github.com/weaveworks/flagger/pkg/client/clientset/versioned/typed/traefik/v1alpha1"
+	faketraefikv1alpha1 "github.com/weaveworks/flagger/pkg/client/clientset/versioned/typed/traefik/v1alpha1/fake"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/watch"
 	"k8s.io/client-go/discovery"
@@ -129,3 +131,8 @@ func (c *Clientset) SplitV1alpha1() splitv1alpha1.SplitV1alpha1Interface {
 func (c *Clientset) SplitV1alpha2() splitv1alpha2.SplitV1alpha2Interface {
 	return &fakesplitv1alpha2.FakeSplitV1alpha2{Fake: &c.Fake}
 }
+
+// TraefikV1alpha1 retrieves the TraefikV1alpha1Client
+func (c *Clientset) TraefikV1alpha1() traefikv1alpha1.TraefikV1alpha1Interface {
+	return &faketraefikv1alpha1.FakeTraefikV1alpha1{Fake: &c.Fake}
+}
diff --git a/pkg/client/clientset/versioned/fake/register.go b/pkg/client/clientset/versioned/fake/register.go
index e5ab88c5..b7feb7ce 100644
--- a/pkg/client/clientset/versioned/fake/register.go
+++ b/pkg/client/clientset/versioned/fake/register.go
@@ -27,6 +27,7 @@ import (
 	projectcontourv1 "github.com/weaveworks/flagger/pkg/apis/projectcontour/v1"
 	splitv1alpha1 "github.com/weaveworks/flagger/pkg/apis/smi/v1alpha1"
 	splitv1alpha2 "github.com/weaveworks/flagger/pkg/apis/smi/v1alpha2"
+	traefikv1alpha1 "github.com/weaveworks/flagger/pkg/apis/traefik/v1alpha1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	runtime "k8s.io/apimachinery/pkg/runtime"
 	schema "k8s.io/apimachinery/pkg/runtime/schema"
@@ -46,6 +47,7 @@ var localSchemeBuilder = runtime.SchemeBuilder{
 	projectcontourv1.AddToScheme,
 	splitv1alpha1.AddToScheme,
 	splitv1alpha2.AddToScheme,
+	traefikv1alpha1.AddToScheme,
 }
 
 // AddToScheme adds all types of this clientset into the given scheme. This allows composition
diff --git a/pkg/client/clientset/versioned/scheme/register.go b/pkg/client/clientset/versioned/scheme/register.go
index 19b1677c..3623b977 100644
--- a/pkg/client/clientset/versioned/scheme/register.go
+++ b/pkg/client/clientset/versioned/scheme/register.go
@@ -27,6 +27,7 @@ import (
 	projectcontourv1 "github.com/weaveworks/flagger/pkg/apis/projectcontour/v1"
 	splitv1alpha1 "github.com/weaveworks/flagger/pkg/apis/smi/v1alpha1"
 	splitv1alpha2 "github.com/weaveworks/flagger/pkg/apis/smi/v1alpha2"
+	traefikv1alpha1 "github.com/weaveworks/flagger/pkg/apis/traefik/v1alpha1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	runtime "k8s.io/apimachinery/pkg/runtime"
 	schema "k8s.io/apimachinery/pkg/runtime/schema"
@@ -46,6 +47,7 @@ var localSchemeBuilder = runtime.SchemeBuilder{
 	projectcontourv1.AddToScheme,
 	splitv1alpha1.AddToScheme,
 	splitv1alpha2.AddToScheme,
+	traefikv1alpha1.AddToScheme,
 }
 
 // AddToScheme adds all types of this clientset into the given scheme. This allows composition
diff --git a/pkg/client/clientset/versioned/typed/traefik/v1alpha1/doc.go b/pkg/client/clientset/versioned/typed/traefik/v1alpha1/doc.go
new file mode 100644
index 00000000..20b3d7fd
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/traefik/v1alpha1/doc.go
@@ -0,0 +1,20 @@
+/*
+Copyright The Flagger Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// This package has the automatically generated typed clients.
+package v1alpha1
diff --git a/pkg/client/clientset/versioned/typed/traefik/v1alpha1/fake/doc.go b/pkg/client/clientset/versioned/typed/traefik/v1alpha1/fake/doc.go
new file mode 100644
index 00000000..7a3b19cb
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/traefik/v1alpha1/fake/doc.go
@@ -0,0 +1,20 @@
+/*
+Copyright The Flagger Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// Package fake has the automatically generated clients.
+package fake
diff --git a/pkg/client/clientset/versioned/typed/traefik/v1alpha1/fake/fake_traefik_client.go b/pkg/client/clientset/versioned/typed/traefik/v1alpha1/fake/fake_traefik_client.go
new file mode 100644
index 00000000..7b1f1295
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/traefik/v1alpha1/fake/fake_traefik_client.go
@@ -0,0 +1,40 @@
+/*
+Copyright The Flagger Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	v1alpha1 "github.com/weaveworks/flagger/pkg/client/clientset/versioned/typed/traefik/v1alpha1"
+	rest "k8s.io/client-go/rest"
+	testing "k8s.io/client-go/testing"
+)
+
+type FakeTraefikV1alpha1 struct {
+	*testing.Fake
+}
+
+func (c *FakeTraefikV1alpha1) TraefikServices(namespace string) v1alpha1.TraefikServiceInterface {
+	return &FakeTraefikServices{c, namespace}
+}
+
+// RESTClient returns a RESTClient that is used to communicate
+// with API server by this client implementation.
+func (c *FakeTraefikV1alpha1) RESTClient() rest.Interface {
+	var ret *rest.RESTClient
+	return ret
+}
diff --git a/pkg/client/clientset/versioned/typed/traefik/v1alpha1/fake/fake_traefikservice.go b/pkg/client/clientset/versioned/typed/traefik/v1alpha1/fake/fake_traefikservice.go
new file mode 100644
index 00000000..c12862bc
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/traefik/v1alpha1/fake/fake_traefikservice.go
@@ -0,0 +1,130 @@
+/*
+Copyright The Flagger Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	"context"
+
+	v1alpha1 "github.com/weaveworks/flagger/pkg/apis/traefik/v1alpha1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	labels "k8s.io/apimachinery/pkg/labels"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	testing "k8s.io/client-go/testing"
+)
+
+// FakeTraefikServices implements TraefikServiceInterface
+type FakeTraefikServices struct {
+	Fake *FakeTraefikV1alpha1
+	ns   string
+}
+
+var traefikservicesResource = schema.GroupVersionResource{Group: "traefik.containo.us", Version: "v1alpha1", Resource: "traefikservices"}
+
+var traefikservicesKind = schema.GroupVersionKind{Group: "traefik.containo.us", Version: "v1alpha1", Kind: "TraefikService"}
+
+// Get takes name of the traefikService, and returns the corresponding traefikService object, and an error if there is any.
+func (c *FakeTraefikServices) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.TraefikService, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewGetAction(traefikservicesResource, c.ns, name), &v1alpha1.TraefikService{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.TraefikService), err
+}
+
+// List takes label and field selectors, and returns the list of TraefikServices that match those selectors.
+func (c *FakeTraefikServices) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.TraefikServiceList, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewListAction(traefikservicesResource, traefikservicesKind, c.ns, opts), &v1alpha1.TraefikServiceList{})
+
+	if obj == nil {
+		return nil, err
+	}
+
+	label, _, _ := testing.ExtractFromListOptions(opts)
+	if label == nil {
+		label = labels.Everything()
+	}
+	list := &v1alpha1.TraefikServiceList{ListMeta: obj.(*v1alpha1.TraefikServiceList).ListMeta}
+	for _, item := range obj.(*v1alpha1.TraefikServiceList).Items {
+		if label.Matches(labels.Set(item.Labels)) {
+			list.Items = append(list.Items, item)
+		}
+	}
+	return list, err
+}
+
+// Watch returns a watch.Interface that watches the requested traefikServices.
+func (c *FakeTraefikServices) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	return c.Fake.
+		InvokesWatch(testing.NewWatchAction(traefikservicesResource, c.ns, opts))
+
+}
+
+// Create takes the representation of a traefikService and creates it.  Returns the server's representation of the traefikService, and an error, if there is any.
+func (c *FakeTraefikServices) Create(ctx context.Context, traefikService *v1alpha1.TraefikService, opts v1.CreateOptions) (result *v1alpha1.TraefikService, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewCreateAction(traefikservicesResource, c.ns, traefikService), &v1alpha1.TraefikService{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.TraefikService), err
+}
+
+// Update takes the representation of a traefikService and updates it. Returns the server's representation of the traefikService, and an error, if there is any.
+func (c *FakeTraefikServices) Update(ctx context.Context, traefikService *v1alpha1.TraefikService, opts v1.UpdateOptions) (result *v1alpha1.TraefikService, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateAction(traefikservicesResource, c.ns, traefikService), &v1alpha1.TraefikService{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.TraefikService), err
+}
+
+// Delete takes name of the traefikService and deletes it. Returns an error if one occurs.
+func (c *FakeTraefikServices) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	_, err := c.Fake.
+		Invokes(testing.NewDeleteAction(traefikservicesResource, c.ns, name), &v1alpha1.TraefikService{})
+
+	return err
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *FakeTraefikServices) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	action := testing.NewDeleteCollectionAction(traefikservicesResource, c.ns, listOpts)
+
+	_, err := c.Fake.Invokes(action, &v1alpha1.TraefikServiceList{})
+	return err
+}
+
+// Patch applies the patch and returns the patched traefikService.
+func (c *FakeTraefikServices) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.TraefikService, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewPatchSubresourceAction(traefikservicesResource, c.ns, name, pt, data, subresources...), &v1alpha1.TraefikService{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.TraefikService), err
+}
diff --git a/pkg/client/clientset/versioned/typed/traefik/v1alpha1/generated_expansion.go b/pkg/client/clientset/versioned/typed/traefik/v1alpha1/generated_expansion.go
new file mode 100644
index 00000000..9a4fb62a
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/traefik/v1alpha1/generated_expansion.go
@@ -0,0 +1,21 @@
+/*
+Copyright The Flagger Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+type TraefikServiceExpansion interface{}
diff --git a/pkg/client/clientset/versioned/typed/traefik/v1alpha1/traefik_client.go b/pkg/client/clientset/versioned/typed/traefik/v1alpha1/traefik_client.go
new file mode 100644
index 00000000..966bc6de
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/traefik/v1alpha1/traefik_client.go
@@ -0,0 +1,89 @@
+/*
+Copyright The Flagger Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	v1alpha1 "github.com/weaveworks/flagger/pkg/apis/traefik/v1alpha1"
+	"github.com/weaveworks/flagger/pkg/client/clientset/versioned/scheme"
+	rest "k8s.io/client-go/rest"
+)
+
+type TraefikV1alpha1Interface interface {
+	RESTClient() rest.Interface
+	TraefikServicesGetter
+}
+
+// TraefikV1alpha1Client is used to interact with features provided by the traefik.containo.us group.
+type TraefikV1alpha1Client struct {
+	restClient rest.Interface
+}
+
+func (c *TraefikV1alpha1Client) TraefikServices(namespace string) TraefikServiceInterface {
+	return newTraefikServices(c, namespace)
+}
+
+// NewForConfig creates a new TraefikV1alpha1Client for the given config.
+func NewForConfig(c *rest.Config) (*TraefikV1alpha1Client, error) {
+	config := *c
+	if err := setConfigDefaults(&config); err != nil {
+		return nil, err
+	}
+	client, err := rest.RESTClientFor(&config)
+	if err != nil {
+		return nil, err
+	}
+	return &TraefikV1alpha1Client{client}, nil
+}
+
+// NewForConfigOrDie creates a new TraefikV1alpha1Client for the given config and
+// panics if there is an error in the config.
+func NewForConfigOrDie(c *rest.Config) *TraefikV1alpha1Client {
+	client, err := NewForConfig(c)
+	if err != nil {
+		panic(err)
+	}
+	return client
+}
+
+// New creates a new TraefikV1alpha1Client for the given RESTClient.
+func New(c rest.Interface) *TraefikV1alpha1Client {
+	return &TraefikV1alpha1Client{c}
+}
+
+func setConfigDefaults(config *rest.Config) error {
+	gv := v1alpha1.SchemeGroupVersion
+	config.GroupVersion = &gv
+	config.APIPath = "/apis"
+	config.NegotiatedSerializer = scheme.Codecs.WithoutConversion()
+
+	if config.UserAgent == "" {
+		config.UserAgent = rest.DefaultKubernetesUserAgent()
+	}
+
+	return nil
+}
+
+// RESTClient returns a RESTClient that is used to communicate
+// with API server by this client implementation.
+func (c *TraefikV1alpha1Client) RESTClient() rest.Interface {
+	if c == nil {
+		return nil
+	}
+	return c.restClient
+}
diff --git a/pkg/client/clientset/versioned/typed/traefik/v1alpha1/traefikservice.go b/pkg/client/clientset/versioned/typed/traefik/v1alpha1/traefikservice.go
new file mode 100644
index 00000000..cfc5a75a
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/traefik/v1alpha1/traefikservice.go
@@ -0,0 +1,178 @@
+/*
+Copyright The Flagger Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	"context"
+	"time"
+
+	v1alpha1 "github.com/weaveworks/flagger/pkg/apis/traefik/v1alpha1"
+	scheme "github.com/weaveworks/flagger/pkg/client/clientset/versioned/scheme"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	rest "k8s.io/client-go/rest"
+)
+
+// TraefikServicesGetter has a method to return a TraefikServiceInterface.
+// A group's client should implement this interface.
+type TraefikServicesGetter interface {
+	TraefikServices(namespace string) TraefikServiceInterface
+}
+
+// TraefikServiceInterface has methods to work with TraefikService resources.
+type TraefikServiceInterface interface {
+	Create(ctx context.Context, traefikService *v1alpha1.TraefikService, opts v1.CreateOptions) (*v1alpha1.TraefikService, error)
+	Update(ctx context.Context, traefikService *v1alpha1.TraefikService, opts v1.UpdateOptions) (*v1alpha1.TraefikService, error)
+	Delete(ctx context.Context, name string, opts v1.DeleteOptions) error
+	DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error
+	Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.TraefikService, error)
+	List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.TraefikServiceList, error)
+	Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error)
+	Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.TraefikService, err error)
+	TraefikServiceExpansion
+}
+
+// traefikServices implements TraefikServiceInterface
+type traefikServices struct {
+	client rest.Interface
+	ns     string
+}
+
+// newTraefikServices returns a TraefikServices
+func newTraefikServices(c *TraefikV1alpha1Client, namespace string) *traefikServices {
+	return &traefikServices{
+		client: c.RESTClient(),
+		ns:     namespace,
+	}
+}
+
+// Get takes name of the traefikService, and returns the corresponding traefikService object, and an error if there is any.
+func (c *traefikServices) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.TraefikService, err error) {
+	result = &v1alpha1.TraefikService{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("traefikservices").
+		Name(name).
+		VersionedParams(&options, scheme.ParameterCodec).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// List takes label and field selectors, and returns the list of TraefikServices that match those selectors.
+func (c *traefikServices) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.TraefikServiceList, err error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	result = &v1alpha1.TraefikServiceList{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("traefikservices").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Watch returns a watch.Interface that watches the requested traefikServices.
+func (c *traefikServices) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	opts.Watch = true
+	return c.client.Get().
+		Namespace(c.ns).
+		Resource("traefikservices").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Watch(ctx)
+}
+
+// Create takes the representation of a traefikService and creates it.  Returns the server's representation of the traefikService, and an error, if there is any.
+func (c *traefikServices) Create(ctx context.Context, traefikService *v1alpha1.TraefikService, opts v1.CreateOptions) (result *v1alpha1.TraefikService, err error) {
+	result = &v1alpha1.TraefikService{}
+	err = c.client.Post().
+		Namespace(c.ns).
+		Resource("traefikservices").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(traefikService).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Update takes the representation of a traefikService and updates it. Returns the server's representation of the traefikService, and an error, if there is any.
+func (c *traefikServices) Update(ctx context.Context, traefikService *v1alpha1.TraefikService, opts v1.UpdateOptions) (result *v1alpha1.TraefikService, err error) {
+	result = &v1alpha1.TraefikService{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("traefikservices").
+		Name(traefikService.Name).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(traefikService).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Delete takes name of the traefikService and deletes it. Returns an error if one occurs.
+func (c *traefikServices) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("traefikservices").
+		Name(name).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *traefikServices) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	var timeout time.Duration
+	if listOpts.TimeoutSeconds != nil {
+		timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second
+	}
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("traefikservices").
+		VersionedParams(&listOpts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// Patch applies the patch and returns the patched traefikService.
+func (c *traefikServices) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.TraefikService, err error) {
+	result = &v1alpha1.TraefikService{}
+	err = c.client.Patch(pt).
+		Namespace(c.ns).
+		Resource("traefikservices").
+		Name(name).
+		SubResource(subresources...).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(data).
+		Do(ctx).
+		Into(result)
+	return
+}
diff --git a/pkg/client/informers/externalversions/factory.go b/pkg/client/informers/externalversions/factory.go
index 22eddf1c..91ed5fc8 100644
--- a/pkg/client/informers/externalversions/factory.go
+++ b/pkg/client/informers/externalversions/factory.go
@@ -31,6 +31,7 @@ import (
 	istio "github.com/weaveworks/flagger/pkg/client/informers/externalversions/istio"
 	projectcontour "github.com/weaveworks/flagger/pkg/client/informers/externalversions/projectcontour"
 	smi "github.com/weaveworks/flagger/pkg/client/informers/externalversions/smi"
+	traefik "github.com/weaveworks/flagger/pkg/client/informers/externalversions/traefik"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	runtime "k8s.io/apimachinery/pkg/runtime"
 	schema "k8s.io/apimachinery/pkg/runtime/schema"
@@ -183,6 +184,7 @@ type SharedInformerFactory interface {
 	Networking() istio.Interface
 	Projectcontour() projectcontour.Interface
 	Split() smi.Interface
+	Traefik() traefik.Interface
 }
 
 func (f *sharedInformerFactory) Appmesh() appmesh.Interface {
@@ -208,3 +210,7 @@ func (f *sharedInformerFactory) Projectcontour() projectcontour.Interface {
 func (f *sharedInformerFactory) Split() smi.Interface {
 	return smi.New(f, f.namespace, f.tweakListOptions)
 }
+
+func (f *sharedInformerFactory) Traefik() traefik.Interface {
+	return traefik.New(f, f.namespace, f.tweakListOptions)
+}
diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go
index ceda6adf..849d7881 100644
--- a/pkg/client/informers/externalversions/generic.go
+++ b/pkg/client/informers/externalversions/generic.go
@@ -29,6 +29,7 @@ import (
 	projectcontourv1 "github.com/weaveworks/flagger/pkg/apis/projectcontour/v1"
 	v1alpha1 "github.com/weaveworks/flagger/pkg/apis/smi/v1alpha1"
 	v1alpha2 "github.com/weaveworks/flagger/pkg/apis/smi/v1alpha2"
+	traefikv1alpha1 "github.com/weaveworks/flagger/pkg/apis/traefik/v1alpha1"
 	schema "k8s.io/apimachinery/pkg/runtime/schema"
 	cache "k8s.io/client-go/tools/cache"
 )
@@ -105,6 +106,10 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource
 	case v1alpha2.SchemeGroupVersion.WithResource("trafficsplits"):
 		return &genericInformer{resource: resource.GroupResource(), informer: f.Split().V1alpha2().TrafficSplits().Informer()}, nil
 
+		// Group=traefik.containo.us, Version=v1alpha1
+	case traefikv1alpha1.SchemeGroupVersion.WithResource("traefikservices"):
+		return &genericInformer{resource: resource.GroupResource(), informer: f.Traefik().V1alpha1().TraefikServices().Informer()}, nil
+
 	}
 
 	return nil, fmt.Errorf("no informer found for %v", resource)
diff --git a/pkg/client/informers/externalversions/traefik/interface.go b/pkg/client/informers/externalversions/traefik/interface.go
new file mode 100644
index 00000000..10141335
--- /dev/null
+++ b/pkg/client/informers/externalversions/traefik/interface.go
@@ -0,0 +1,46 @@
+/*
+Copyright The Flagger Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package traefik
+
+import (
+	internalinterfaces "github.com/weaveworks/flagger/pkg/client/informers/externalversions/internalinterfaces"
+	v1alpha1 "github.com/weaveworks/flagger/pkg/client/informers/externalversions/traefik/v1alpha1"
+)
+
+// Interface provides access to each of this group's versions.
+type Interface interface {
+	// V1alpha1 provides access to shared informers for resources in V1alpha1.
+	V1alpha1() v1alpha1.Interface
+}
+
+type group struct {
+	factory          internalinterfaces.SharedInformerFactory
+	namespace        string
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+}
+
+// New returns a new Interface.
+func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
+	return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
+}
+
+// V1alpha1 returns a new v1alpha1.Interface.
+func (g *group) V1alpha1() v1alpha1.Interface {
+	return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions)
+}
diff --git a/pkg/client/informers/externalversions/traefik/v1alpha1/interface.go b/pkg/client/informers/externalversions/traefik/v1alpha1/interface.go
new file mode 100644
index 00000000..2be6cd65
--- /dev/null
+++ b/pkg/client/informers/externalversions/traefik/v1alpha1/interface.go
@@ -0,0 +1,45 @@
+/*
+Copyright The Flagger Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	internalinterfaces "github.com/weaveworks/flagger/pkg/client/informers/externalversions/internalinterfaces"
+)
+
+// Interface provides access to all the informers in this group version.
+type Interface interface {
+	// TraefikServices returns a TraefikServiceInformer.
+	TraefikServices() TraefikServiceInformer
+}
+
+type version struct {
+	factory          internalinterfaces.SharedInformerFactory
+	namespace        string
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+}
+
+// New returns a new Interface.
+func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
+	return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
+}
+
+// TraefikServices returns a TraefikServiceInformer.
+func (v *version) TraefikServices() TraefikServiceInformer {
+	return &traefikServiceInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
+}
diff --git a/pkg/client/informers/externalversions/traefik/v1alpha1/traefikservice.go b/pkg/client/informers/externalversions/traefik/v1alpha1/traefikservice.go
new file mode 100644
index 00000000..6a043732
--- /dev/null
+++ b/pkg/client/informers/externalversions/traefik/v1alpha1/traefikservice.go
@@ -0,0 +1,90 @@
+/*
+Copyright The Flagger Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	"context"
+	time "time"
+
+	traefikv1alpha1 "github.com/weaveworks/flagger/pkg/apis/traefik/v1alpha1"
+	versioned "github.com/weaveworks/flagger/pkg/client/clientset/versioned"
+	internalinterfaces "github.com/weaveworks/flagger/pkg/client/informers/externalversions/internalinterfaces"
+	v1alpha1 "github.com/weaveworks/flagger/pkg/client/listers/traefik/v1alpha1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	watch "k8s.io/apimachinery/pkg/watch"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// TraefikServiceInformer provides access to a shared informer and lister for
+// TraefikServices.
+type TraefikServiceInformer interface {
+	Informer() cache.SharedIndexInformer
+	Lister() v1alpha1.TraefikServiceLister
+}
+
+type traefikServiceInformer struct {
+	factory          internalinterfaces.SharedInformerFactory
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+	namespace        string
+}
+
+// NewTraefikServiceInformer constructs a new informer for TraefikService type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewTraefikServiceInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
+	return NewFilteredTraefikServiceInformer(client, namespace, resyncPeriod, indexers, nil)
+}
+
+// NewFilteredTraefikServiceInformer constructs a new informer for TraefikService type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewFilteredTraefikServiceInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
+	return cache.NewSharedIndexInformer(
+		&cache.ListWatch{
+			ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.TraefikV1alpha1().TraefikServices(namespace).List(context.TODO(), options)
+			},
+			WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.TraefikV1alpha1().TraefikServices(namespace).Watch(context.TODO(), options)
+			},
+		},
+		&traefikv1alpha1.TraefikService{},
+		resyncPeriod,
+		indexers,
+	)
+}
+
+func (f *traefikServiceInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+	return NewFilteredTraefikServiceInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
+}
+
+func (f *traefikServiceInformer) Informer() cache.SharedIndexInformer {
+	return f.factory.InformerFor(&traefikv1alpha1.TraefikService{}, f.defaultInformer)
+}
+
+func (f *traefikServiceInformer) Lister() v1alpha1.TraefikServiceLister {
+	return v1alpha1.NewTraefikServiceLister(f.Informer().GetIndexer())
+}
diff --git a/pkg/client/listers/traefik/v1alpha1/expansion_generated.go b/pkg/client/listers/traefik/v1alpha1/expansion_generated.go
new file mode 100644
index 00000000..cb41928e
--- /dev/null
+++ b/pkg/client/listers/traefik/v1alpha1/expansion_generated.go
@@ -0,0 +1,27 @@
+/*
+Copyright The Flagger Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1alpha1
+
+// TraefikServiceListerExpansion allows custom methods to be added to
+// TraefikServiceLister.
+type TraefikServiceListerExpansion interface{}
+
+// TraefikServiceNamespaceListerExpansion allows custom methods to be added to
+// TraefikServiceNamespaceLister.
+type TraefikServiceNamespaceListerExpansion interface{}
diff --git a/pkg/client/listers/traefik/v1alpha1/traefikservice.go b/pkg/client/listers/traefik/v1alpha1/traefikservice.go
new file mode 100644
index 00000000..bf200ce8
--- /dev/null
+++ b/pkg/client/listers/traefik/v1alpha1/traefikservice.go
@@ -0,0 +1,94 @@
+/*
+Copyright The Flagger Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	v1alpha1 "github.com/weaveworks/flagger/pkg/apis/traefik/v1alpha1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/client-go/tools/cache"
+)
+
+// TraefikServiceLister helps list TraefikServices.
+type TraefikServiceLister interface {
+	// List lists all TraefikServices in the indexer.
+	List(selector labels.Selector) (ret []*v1alpha1.TraefikService, err error)
+	// TraefikServices returns an object that can list and get TraefikServices.
+	TraefikServices(namespace string) TraefikServiceNamespaceLister
+	TraefikServiceListerExpansion
+}
+
+// traefikServiceLister implements the TraefikServiceLister interface.
+type traefikServiceLister struct {
+	indexer cache.Indexer
+}
+
+// NewTraefikServiceLister returns a new TraefikServiceLister.
+func NewTraefikServiceLister(indexer cache.Indexer) TraefikServiceLister {
+	return &traefikServiceLister{indexer: indexer}
+}
+
+// List lists all TraefikServices in the indexer.
+func (s *traefikServiceLister) List(selector labels.Selector) (ret []*v1alpha1.TraefikService, err error) {
+	err = cache.ListAll(s.indexer, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1alpha1.TraefikService))
+	})
+	return ret, err
+}
+
+// TraefikServices returns an object that can list and get TraefikServices.
+func (s *traefikServiceLister) TraefikServices(namespace string) TraefikServiceNamespaceLister {
+	return traefikServiceNamespaceLister{indexer: s.indexer, namespace: namespace}
+}
+
+// TraefikServiceNamespaceLister helps list and get TraefikServices.
+type TraefikServiceNamespaceLister interface {
+	// List lists all TraefikServices in the indexer for a given namespace.
+	List(selector labels.Selector) (ret []*v1alpha1.TraefikService, err error)
+	// Get retrieves the TraefikService from the indexer for a given namespace and name.
+	Get(name string) (*v1alpha1.TraefikService, error)
+	TraefikServiceNamespaceListerExpansion
+}
+
+// traefikServiceNamespaceLister implements the TraefikServiceNamespaceLister
+// interface.
+type traefikServiceNamespaceLister struct {
+	indexer   cache.Indexer
+	namespace string
+}
+
+// List lists all TraefikServices in the indexer for a given namespace.
+func (s traefikServiceNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.TraefikService, err error) {
+	err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1alpha1.TraefikService))
+	})
+	return ret, err
+}
+
+// Get retrieves the TraefikService from the indexer for a given namespace and name.
+func (s traefikServiceNamespaceLister) Get(name string) (*v1alpha1.TraefikService, error) {
+	obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name)
+	if err != nil {
+		return nil, err
+	}
+	if !exists {
+		return nil, errors.NewNotFound(v1alpha1.Resource("traefikservice"), name)
+	}
+	return obj.(*v1alpha1.TraefikService), nil
+}
diff --git a/pkg/metrics/observers/factory.go b/pkg/metrics/observers/factory.go
index ac20884d..42a47c9c 100644
--- a/pkg/metrics/observers/factory.go
+++ b/pkg/metrics/observers/factory.go
@@ -60,6 +60,10 @@ func (factory Factory) Observer(provider string) Interface {
 		return &SkipperObserver{
 			client: factory.Client,
 		}
+	case provider == flaggerv1.TraefikProvider:
+		return &TraefikObserver{
+			client: factory.Client,
+		}
 	default:
 		return &IstioObserver{
 			client: factory.Client,
diff --git a/pkg/metrics/observers/traefik.go b/pkg/metrics/observers/traefik.go
new file mode 100644
index 00000000..4cba7fca
--- /dev/null
+++ b/pkg/metrics/observers/traefik.go
@@ -0,0 +1,76 @@
+package observers
+
+import (
+	"fmt"
+	"time"
+
+	flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
+	"github.com/weaveworks/flagger/pkg/metrics/providers"
+)
+
+var traefikQueries = map[string]string{
+	"request-success-rate": `
+	sum(
+		rate(
+			traefik_service_request_duration_seconds_bucket{
+				service=~"{{ namespace }}-{{ target }}-canary-[0-9a-zA-Z-]+@kubernetescrd",
+				code!~"5..",
+				le="+Inf"
+			}[{{ interval }}]
+		)
+	)
+	/
+	sum(
+		rate(
+			traefik_service_request_duration_seconds_bucket{
+				service=~"{{ namespace }}-{{ target }}-canary-[0-9a-zA-Z-]+@kubernetescrd",
+				le="+Inf"
+			}[{{ interval }}]
+		)
+	) * 100`,
+	"request-duration": `
+	histogram_quantile(
+		0.99,
+		sum(
+			rate(
+				traefik_service_request_duration_seconds_bucket{
+					service=~"{{ namespace }}-{{ target }}-canary-[0-9a-zA-Z-]+@kubernetescrd"
+				}[{{ interval }}]
+			)
+		) by (le)
+	)`,
+}
+
+type TraefikObserver struct {
+	client providers.Interface
+}
+
+func (ob *TraefikObserver) GetRequestSuccessRate(model flaggerv1.MetricTemplateModel) (float64, error) {
+
+	query, err := RenderQuery(traefikQueries["request-success-rate"], model)
+	if err != nil {
+		return 0, fmt.Errorf("rendering query failed: %w", err)
+	}
+
+	value, err := ob.client.RunQuery(query)
+	if err != nil {
+		return 0, fmt.Errorf("running query failed: %w", err)
+	}
+
+	return value, nil
+}
+
+func (ob *TraefikObserver) GetRequestDuration(model flaggerv1.MetricTemplateModel) (time.Duration, error) {
+	query, err := RenderQuery(traefikQueries["request-duration"], model)
+	if err != nil {
+		return 0, fmt.Errorf("rendering query failed: %w", err)
+	}
+
+	value, err := ob.client.RunQuery(query)
+	if err != nil {
+		return 0, fmt.Errorf("running query failed: %w", err)
+	}
+
+	ms := time.Duration(int64(value)) * time.Millisecond
+	return ms, nil
+}
diff --git a/pkg/router/factory.go b/pkg/router/factory.go
index cd1835b3..2d85aedf 100644
--- a/pkg/router/factory.go
+++ b/pkg/router/factory.go
@@ -128,6 +128,11 @@ func (factory *Factory) MeshRouter(provider string, labelSelector string) Interf
 			logger:     factory.logger,
 			kubeClient: factory.kubeClient,
 		}
+	case provider == flaggerv1.TraefikProvider:
+		return &TraefikRouter{
+			logger:        factory.logger,
+			traefikClient: factory.meshClient,
+		}
 	case provider == flaggerv1.KubernetesProvider:
 		return &NopRouter{}
 	default:
diff --git a/pkg/router/traefik.go b/pkg/router/traefik.go
new file mode 100644
index 00000000..04081e37
--- /dev/null
+++ b/pkg/router/traefik.go
@@ -0,0 +1,194 @@
+package router
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
+	traefikv1alpha1 "github.com/weaveworks/flagger/pkg/apis/traefik/v1alpha1"
+	clientset "github.com/weaveworks/flagger/pkg/client/clientset/versioned"
+	"go.uber.org/zap"
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+)
+
+// TraefikRouter is managing Traefik service
+type TraefikRouter struct {
+	traefikClient clientset.Interface
+	logger        *zap.SugaredLogger
+}
+
+// Reconcile creates or updates the Traefik service
+func (tr *TraefikRouter) Reconcile(canary *flaggerv1.Canary) error {
+	apexName, primaryName, canaryName := canary.GetServiceNames()
+
+	newSpec := traefikv1alpha1.ServiceSpec{
+		Weighted: &traefikv1alpha1.WeightedRoundRobin{
+			Services: []traefikv1alpha1.Service{
+				{
+					Name:      primaryName,
+					Namespace: canary.Namespace,
+					Port:      canary.Spec.Service.Port,
+					Weight:    100,
+				},
+			},
+		},
+	}
+
+	traefikService, err := tr.traefikClient.TraefikV1alpha1().TraefikServices(canary.Namespace).Get(context.TODO(), apexName, metav1.GetOptions{})
+	if errors.IsNotFound(err) {
+		tsMetadata := canary.Spec.Service.Apex
+		if tsMetadata == nil {
+			tsMetadata = &flaggerv1.CustomMetadata{}
+		}
+		if tsMetadata.Labels == nil {
+			tsMetadata.Labels = make(map[string]string)
+		}
+		if tsMetadata.Annotations == nil {
+			tsMetadata.Annotations = make(map[string]string)
+		}
+
+		traefikService = &traefikv1alpha1.TraefikService{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:        apexName,
+				Namespace:   canary.Namespace,
+				Labels:      tsMetadata.Labels,
+				Annotations: tsMetadata.Annotations,
+				OwnerReferences: []metav1.OwnerReference{
+					*metav1.NewControllerRef(canary, schema.GroupVersionKind{
+						Group:   flaggerv1.SchemeGroupVersion.Group,
+						Version: flaggerv1.SchemeGroupVersion.Version,
+						Kind:    flaggerv1.CanaryKind,
+					}),
+				},
+			},
+			Spec: newSpec,
+		}
+
+		_, err = tr.traefikClient.TraefikV1alpha1().TraefikServices(canary.Namespace).Create(context.TODO(), traefikService, metav1.CreateOptions{})
+		if err != nil {
+			return fmt.Errorf("TraefikService %s.%s create error: %w", apexName, canary.Namespace, err)
+		}
+		tr.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
+			Infof("TraefikService %s.%s created", traefikService.GetName(), canary.Namespace)
+		return nil
+	} else if err != nil {
+		return fmt.Errorf("TraefikService %s.%s get query error: %w", apexName, canary.Namespace, err)
+	}
+
+	// update TraefikService but keep the original service weights
+	if traefikService != nil {
+		if len(traefikService.Spec.Weighted.Services) == 2 {
+			newSpec.Weighted.Services = append(
+				newSpec.Weighted.Services,
+				traefikv1alpha1.Service{
+					Name:      canaryName,
+					Namespace: canary.Namespace,
+					Port:      canary.Spec.Service.Port,
+					Weight:    100,
+				},
+			)
+		}
+
+		if diff := cmp.Diff(
+			newSpec,
+			traefikService.Spec,
+			cmpopts.IgnoreFields(traefikv1alpha1.Service{}, "Weight"),
+		); diff != "" {
+
+			clone := traefikService.DeepCopy()
+			clone.Spec = newSpec
+
+			_, err = tr.traefikClient.TraefikV1alpha1().TraefikServices(canary.Namespace).Update(context.TODO(), clone, metav1.UpdateOptions{})
+			if err != nil {
+				return fmt.Errorf("TraefikService %s.%s update error: %w", apexName, canary.Namespace, err)
+			}
+			tr.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
+				Infof("TraefikService %s.%s updated", traefikService.GetName(), canary.Namespace)
+		}
+	}
+
+	return nil
+}
+
+// GetRoutes returns the destinations weight for primary and canary
+func (tr *TraefikRouter) GetRoutes(canary *flaggerv1.Canary) (
+	primaryWeight int,
+	canaryWeight int,
+	mirrored bool,
+	err error,
+) {
+	apexName, primaryName, _ := canary.GetServiceNames()
+
+	traefikService, err := tr.traefikClient.TraefikV1alpha1().TraefikServices(canary.Namespace).Get(context.TODO(), apexName, metav1.GetOptions{})
+	if err != nil {
+		err = fmt.Errorf("TraefikService %s.%s query error: %w", apexName, canary.Namespace, err)
+		return
+	}
+
+	if len(traefikService.Spec.Weighted.Services) < 1 {
+		err = fmt.Errorf("TraefikService %s.%s services not found", apexName, canary.Namespace)
+		return
+	}
+
+	for _, s := range traefikService.Spec.Weighted.Services {
+		if s.Name == primaryName {
+			primaryWeight = int(s.Weight)
+			canaryWeight = 100 - primaryWeight
+			return
+
+		}
+	}
+
+	return
+}
+
+// SetRoutes updates the destinations weight for primary and canary
+func (tr *TraefikRouter) SetRoutes(
+	canary *flaggerv1.Canary,
+	primaryWeight int,
+	canaryWeight int,
+	_ bool,
+) error {
+	apexName, primaryName, canaryName := canary.GetServiceNames()
+
+	if primaryWeight == 0 && canaryWeight == 0 {
+		return fmt.Errorf("RoutingRule %s.%s update failed: no valid weights", apexName, canary.Namespace)
+	}
+	traefikService, err := tr.traefikClient.TraefikV1alpha1().TraefikServices(canary.Namespace).Get(context.TODO(), apexName, metav1.GetOptions{})
+	if err != nil {
+		return fmt.Errorf("TraefikService %s.%s query error: %w", apexName, canary.Namespace, err)
+	}
+
+	services := []traefikv1alpha1.Service{
+		{
+			Name:      primaryName,
+			Namespace: canary.Namespace,
+			Port:      canary.Spec.Service.Port,
+			Weight:    uint(primaryWeight),
+		},
+	}
+	if canaryWeight > 0 {
+		services = append(services, traefikv1alpha1.Service{
+			Name:      canaryName,
+			Namespace: canary.Namespace,
+			Port:      canary.Spec.Service.Port,
+			Weight:    uint(canaryWeight),
+		})
+	}
+
+	traefikService.Spec.Weighted.Services = services
+
+	_, err = tr.traefikClient.TraefikV1alpha1().TraefikServices(canary.Namespace).Update(context.TODO(), traefikService, metav1.UpdateOptions{})
+	if err != nil {
+		return fmt.Errorf("TraefikService %s.%s update error: %w", apexName, canary.Namespace, err)
+	}
+	return nil
+}
+
+func (tr *TraefikRouter) Finalize(_ *flaggerv1.Canary) error {
+	return nil
+}


Test patch:
diff --git a/pkg/metrics/observers/traefik_test.go b/pkg/metrics/observers/traefik_test.go
new file mode 100644
index 00000000..355a8e2e
--- /dev/null
+++ b/pkg/metrics/observers/traefik_test.go
@@ -0,0 +1,102 @@
+package observers
+
+import (
+	"errors"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
+	"github.com/weaveworks/flagger/pkg/metrics/providers"
+)
+
+func TestTraefikObserver_GetRequestSuccessRate(t *testing.T) {
+	t.Run("ok", func(t *testing.T) {
+		expected := ` sum( rate( traefik_service_request_duration_seconds_bucket{ service=~"default-podinfo-canary-[0-9a-zA-Z-]+@kubernetescrd", code!~"5..", le="+Inf" }[1m] ) ) / sum( rate( traefik_service_request_duration_seconds_bucket{ service=~"default-podinfo-canary-[0-9a-zA-Z-]+@kubernetescrd", le="+Inf" }[1m] ) ) * 100`
+
+		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			promql := r.URL.Query()["query"][0]
+			assert.Equal(t, expected, promql)
+
+			json := `{"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1,"100"]}]}}`
+			w.Write([]byte(json))
+		}))
+		defer ts.Close()
+
+		client, err := providers.NewPrometheusProvider(flaggerv1.MetricTemplateProvider{
+			Type:      "prometheus",
+			Address:   ts.URL,
+			SecretRef: nil,
+		}, nil)
+		require.NoError(t, err)
+
+		observer := &TraefikObserver{client: client}
+
+		val, err := observer.GetRequestSuccessRate(flaggerv1.MetricTemplateModel{
+			Name:      "podinfo",
+			Namespace: "default",
+			Target:    "podinfo",
+			Service:   "podinfo",
+			Interval:  "1m",
+		})
+		require.NoError(t, err)
+
+		assert.Equal(t, float64(100), val)
+	})
+
+	t.Run("no values", func(t *testing.T) {
+		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			json := `{"status":"success","data":{"resultType":"vector","result":[]}}`
+			w.Write([]byte(json))
+		}))
+		defer ts.Close()
+
+		client, err := providers.NewPrometheusProvider(flaggerv1.MetricTemplateProvider{
+			Type:      "prometheus",
+			Address:   ts.URL,
+			SecretRef: nil,
+		}, nil)
+		require.NoError(t, err)
+
+		observer := &TraefikObserver{client: client}
+		_, err = observer.GetRequestSuccessRate(flaggerv1.MetricTemplateModel{})
+		require.True(t, errors.Is(err, providers.ErrNoValuesFound))
+	})
+}
+
+func TestTraefikObserver_GetRequestDuration(t *testing.T) {
+	expected := ` histogram_quantile( 0.99, sum( rate( traefik_service_request_duration_seconds_bucket{ service=~"default-podinfo-canary-[0-9a-zA-Z-]+@kubernetescrd" }[1m] ) ) by (le) )`
+
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		promql := r.URL.Query()["query"][0]
+		assert.Equal(t, expected, promql)
+
+		json := `{"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1,"100"]}]}}`
+		w.Write([]byte(json))
+	}))
+	defer ts.Close()
+
+	client, err := providers.NewPrometheusProvider(flaggerv1.MetricTemplateProvider{
+		Type:      "prometheus",
+		Address:   ts.URL,
+		SecretRef: nil,
+	}, nil)
+	require.NoError(t, err)
+
+	observer := &TraefikObserver{client: client}
+
+	val, err := observer.GetRequestDuration(flaggerv1.MetricTemplateModel{
+		Name:      "podinfo",
+		Namespace: "default",
+		Target:    "podinfo",
+		Service:   "podinfo",
+		Interval:  "1m",
+	})
+	require.NoError(t, err)
+
+	assert.Equal(t, 100*time.Millisecond, val)
+}
diff --git a/pkg/router/traefik_test.go b/pkg/router/traefik_test.go
new file mode 100644
index 00000000..7c5cde96
--- /dev/null
+++ b/pkg/router/traefik_test.go
@@ -0,0 +1,142 @@
+package router
+
+import (
+	"context"
+	"testing"
+
+	flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func TestTraefikRouter_Reconcile(t *testing.T) {
+	mocks := newFixture(nil)
+	mocks.canary.Spec.Service.Apex = &flaggerv1.CustomMetadata{
+		Labels: map[string]string{
+			"test": "label",
+		},
+		Annotations: map[string]string{
+			"test": "annotation",
+		},
+	}
+
+	router := &TraefikRouter{
+		traefikClient: mocks.meshClient,
+		logger:        mocks.logger,
+	}
+
+	assert.NoError(t, router.Reconcile(mocks.canary))
+	ts, err := router.traefikClient.TraefikV1alpha1().TraefikServices("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
+	assert.NoError(t, err)
+
+	services := ts.Spec.Weighted.Services
+	assert.Len(t, services, 1)
+	assert.Equal(t, uint(100), services[0].Weight)
+
+	assert.Equal(t, ts.ObjectMeta.Labels, mocks.canary.Spec.Service.Apex.Labels)
+	assert.Equal(t, ts.ObjectMeta.Annotations, mocks.canary.Spec.Service.Apex.Annotations)
+
+	for _, tt := range []struct {
+		name        string
+		primary     int
+		canary      int
+		servicesLen int
+	}{
+		{
+			name:        "should not change weights when canary is progressing",
+			primary:     60,
+			canary:      40,
+			servicesLen: 2,
+		},
+		{
+			name:        "should not change weights when canary isn't progressing",
+			primary:     100,
+			canary:      0,
+			servicesLen: 1,
+		},
+	} {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			assert.NoError(t, router.Reconcile(mocks.canary))
+			assert.NoError(t, router.SetRoutes(mocks.canary, tt.primary, tt.canary, false))
+			assert.NoError(t, router.Reconcile(mocks.canary))
+
+			ts, err := router.traefikClient.TraefikV1alpha1().TraefikServices("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
+			assert.NoError(t, err)
+
+			services := ts.Spec.Weighted.Services
+			assert.Len(t, services, tt.servicesLen)
+			assert.Equal(t, uint(tt.primary), services[0].Weight)
+			if tt.canary > 0 {
+				assert.Equal(t, uint(tt.canary), services[1].Weight)
+			}
+		})
+
+	}
+}
+
+func TestTraefikRouter_SetRoutes(t *testing.T) {
+	mocks := newFixture(nil)
+	router := &TraefikRouter{
+		traefikClient: mocks.meshClient,
+		logger:        mocks.logger,
+	}
+
+	err := router.Reconcile(mocks.canary)
+	require.NoError(t, err)
+
+	_, _, _, err = router.GetRoutes(mocks.canary)
+	require.NoError(t, err)
+
+	for _, tt := range []struct {
+		name        string
+		primary     int
+		canary      int
+		servicesLen int
+	}{
+		{name: "0%", primary: 100, canary: 0, servicesLen: 1},
+		{name: "20%", primary: 80, canary: 20, servicesLen: 2},
+		{name: "40%", primary: 60, canary: 40, servicesLen: 2},
+		{name: "60%", primary: 40, canary: 60, servicesLen: 2},
+		{name: "80%", primary: 20, canary: 80, servicesLen: 2},
+		{name: "100%", primary: 0, canary: 100, servicesLen: 2},
+		{name: "0% (promote)", primary: 100, canary: 0, servicesLen: 1},
+	} {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			err = router.SetRoutes(mocks.canary, tt.primary, tt.canary, false)
+			require.NoError(t, err)
+
+			ts, err := router.traefikClient.TraefikV1alpha1().TraefikServices("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
+			assert.NoError(t, err)
+
+			services := ts.Spec.Weighted.Services
+			assert.Len(t, services, tt.servicesLen)
+			assert.Equal(t, uint(tt.primary), services[0].Weight)
+			if tt.canary > 0 {
+				assert.Equal(t, uint(tt.canary), services[1].Weight)
+			}
+
+		})
+	}
+}
+
+func TestTraefikRouter_GetRoutes(t *testing.T) {
+	mocks := newFixture(nil)
+	router := &TraefikRouter{
+		traefikClient: mocks.meshClient,
+		logger:        mocks.logger,
+	}
+
+	err := router.Reconcile(mocks.canary)
+	require.NoError(t, err)
+
+	p, c, m, err := router.GetRoutes(mocks.canary)
+	require.NoError(t, err)
+
+	assert.Equal(t, 100, p)
+	assert.Equal(t, 0, c)
+	assert.False(t, m)
+}
diff --git a/test/README.md b/test/README.md
index cff6cd0f..e0f7167e 100644
--- a/test/README.md
+++ b/test/README.md
@@ -69,3 +69,17 @@ The e2e testing infrastructure is powered by CircleCI and [Kubernetes Kind](http
 * test the canary initialization [e2e-skipper-tests.sh](e2e-skipper-tests.sh)
 * test the canary analysis and promotion using weighted traffic and the load testing webhook [e2e-skipper-tests.sh]e2e-skipper-tests.sh)
 * cleanup test environment [e2e-skipper-cleanup.sh](e2e-skipper-cleanup.sh)
+
+### CircleCI e2e Traefik workflow
+
+* install latest stable kubectl [e2e-kind.sh](e2e-kind.sh)
+* install Kubernetes Kind [e2e-kind.sh](e2e-kind.sh)
+* create local Kubernetes cluster with kind [e2e-kind.sh](e2e-kind.sh)
+* install Traeik with Helm [e2e-traefik.sh](e2e-traefik.sh)
+* load Flagger image onto the local cluster [e2e-traefik.sh](e2e-traefik.sh)
+* install Flagger and Prometheus in the traefik namespace [e2e-traefik.sh](e2e-traefik.sh)
+* create a test namespace [e2e-traefik-tests.sh](e2e-traefik-tests.sh)
+* deploy the load tester in the test namespace [e2e-traefik-tests.sh](e2e-traefik-tests.sh)
+* deploy the demo workload (podinfo) and ingress in the test namespace [e2e-traefik-tests.sh](e2e-traefik-tests.sh)
+* test the canary initialization [e2e-traefik-tests.sh](e2e-traefik-tests.sh)
+* test the canary analysis and promotion using weighted traffic and the load testing webhook [e2e-traefik-tests.sh]e2e-traefik-tests.sh)
diff --git a/test/e2e-traefik-tests.sh b/test/e2e-traefik-tests.sh
new file mode 100755
index 00000000..f927aca7
--- /dev/null
+++ b/test/e2e-traefik-tests.sh
@@ -0,0 +1,156 @@
+#!/usr/bin/env bash
+
+# This script runs e2e tests for Canary initialization, analysis and promotion
+# Prerequisites: Kubernetes Kind, Helm and Traefik ingress controller
+
+set -o errexit
+
+REPO_ROOT=$(git rev-parse --show-toplevel)
+
+echo '>>> Creating test namespace'
+kubectl create namespace test
+
+echo '>>> Installing load tester'
+kubectl apply -k ${REPO_ROOT}/kustomize/tester
+kubectl -n test rollout status deployment/flagger-loadtester
+
+echo '>>> Initialising canary'
+kubectl apply -f ${REPO_ROOT}/test/e2e-workload.yaml
+
+cat <<EOF | kubectl apply -f -
+apiVersion: traefik.containo.us/v1alpha1
+kind: IngressRoute
+metadata:
+  name: podinfo
+  namespace: test
+spec:
+  entryPoints:
+    - web
+  routes:
+    - match: Host(`app.example.com`)
+      kind: Rule
+      services:
+        - name: podinfo
+          kind: TraefikService
+          port: 80
+EOF
+
+cat <<EOF | kubectl apply -f -
+apiVersion: flagger.app/v1beta1
+kind: Canary
+metadata:
+  name: podinfo
+  namespace: test
+spec:
+  provider: traefik
+  targetRef:
+    apiVersion: apps/v1
+    kind: Deployment
+    name: podinfo
+  progressDeadlineSeconds: 60
+  service:
+    port: 80
+    targetPort: 9898
+    apex:
+      labels:
+        test: test-label
+      annotations:
+        test: test-annotation
+  analysis:
+    interval: 60s
+    threshold: 15
+    maxWeight: 60
+    stepWeight: 10
+    metrics:
+    - name: request-success-rate
+      threshold: 99
+      interval: 1m
+    - name: request-duration
+      threshold: 500
+      interval: 1m
+    webhooks:
+      - name: "gate"
+        type: confirm-rollout
+        url: http://flagger-loadtester.test/gate/approve
+      - name: acceptance-test
+        type: pre-rollout
+        url: http://flagger-loadtester.test/
+        timeout: 10s
+        metadata:
+          type: bash
+          cmd: "curl -sd 'test' http://podinfo-canary.test/token | grep token"
+      - name: traefik-acceptance-test
+        type: pre-rollout
+        url: http://flagger-loadtester.test/
+        timeout: 10s
+        metadata:
+          type: bash
+          cmd: "curl -sd 'test' -H 'Host: app.example.com' http://traefik.traefik/token | grep token"
+      - name: load-test
+        url: http://flagger-loadtester.test/
+        timeout: 5s
+        metadata:
+          type: cmd
+          cmd: "hey -z 2m -q 5 -c 2 -host app.example.com http://traefik.traefik"
+          logCmdOutput: "true"
+EOF
+
+echo '>>> Waiting for primary to be ready'
+retries=50
+count=0
+ok=false
+until ${ok}; do
+    kubectl -n test get canary/podinfo | grep 'Initialized' && ok=true || ok=false
+    sleep 5
+    count=$(($count + 1))
+    if [[ ${count} -eq ${retries} ]]; then
+        kubectl -n traefik logs deployment/flagger
+        echo "No more retries left"
+        exit 1
+    fi
+done
+
+echo '✔ Canary initialization test passed'
+
+passed=$(kubectl -n test get svc/podinfo -o jsonpath='{.spec.selector.app}' 2>&1 | { grep podinfo-primary || true; })
+if [ -z "$passed" ]; then
+  echo -e '\u2716 podinfo selector test failed'
+  exit 1
+fi
+passed=$(kubectl -n test get traefikservice/podinfo -o jsonpath='{.metadata.labels}' 2>&1 | { grep test-label || true; })
+if [ -z "$passed" ]; then
+  echo -e '\u2716 TraefikService does not have required labels'
+  exit 1
+fi
+passed=$(kubectl -n test get traefikservice/podinfo -o jsonpath='{.metadata.annotations}' 2>&1 | { grep test-annotation || true; })
+if [ -z "$passed" ]; then
+  echo -e '\u2716 TraefikService does not have required annotations'
+  exit 1
+fi
+
+echo '✔ Canary service custom metadata test passed'
+
+echo '>>> Triggering canary deployment'
+kubectl -n test set image deployment/podinfo podinfod=stefanprodan/podinfo:3.1.1
+
+echo '>>> Waiting for canary promotion'
+retries=60
+count=0
+ok=false
+until ${ok}; do
+    kubectl -n test describe deployment/podinfo-primary | grep '3.1.1' && ok=true || ok=false
+    sleep 10
+    kubectl -n traefik logs deployment/flagger --tail 1
+    count=$(($count + 1))
+    if [[ ${count} -eq ${retries} ]]; then
+        kubectl -n test describe deployment/podinfo
+        kubectl -n test describe deployment/podinfo-primary
+        kubectl -n test logs deployment/flagger-loadtester
+        kubectl -n traefik logs deployment/flagger
+        kubectl -n traefik get all
+        echo "No more retries left"
+        exit 1
+    fi
+done
+
+echo '✔ Canary promotion test passed'
diff --git a/test/e2e-traefik.sh b/test/e2e-traefik.sh
new file mode 100755
index 00000000..5405465c
--- /dev/null
+++ b/test/e2e-traefik.sh
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+
+set -o errexit
+
+REPO_ROOT=$(git rev-parse --show-toplevel)
+TRAEFIK_CHART_VERSION="9.11.0" # traefik 2.3.3
+
+echo '>>> Creating traefik namespace'
+kubectl create ns traefik
+
+echo '>>> Installing Traefik'
+helm repo add traefik https://helm.traefik.io/traefik
+cat <<EOF | helm upgrade -i traefik traefik/traefik --version=${TRAEFIK_VERSION} --namespace traefik -f -
+additionalArguments:
+  - "--metrics.prometheus=true"
+deployment:
+  podAnnotations:
+    "prometheus.io/port": "9000"
+    "prometheus.io/scrape": "true"
+EOF
+
+kubectl -n traefik rollout status deployment/traefik
+kubectl -n traefik get all
+
+echo '>>> Loading Flagger image'
+kind load docker-image test/flagger:latest
+
+echo '>>> Installing Flagger'
+helm upgrade -i flagger ${REPO_ROOT}/charts/flagger \
+--set crd.create=false \
+--namespace traefik \
+--set prometheus.install=true \
+--set meshProvider=traefik \
+--set image.repository=test\/flagger \
+--set image.tag=latest \
+
+kubectl -n traefik rollout status deployment/flagger