I recently moved the front of this blog from Traefik 1 to Traefik 2, and to say the least, it's no picnic.

My Traefik use case

I use Traefik as a load balancer/reverse proxy front in a Kubernetes infrastructure. My use is very basic. Depending on certain path and/or domain, I redirect to separate pods. In the case below, I will consider that I have only one pod, this blog. I also manage my certificates with Traefik via Let's Encrypt.

Although this use case may seem basic, it took me several hours to upgrade from Traefik 1.7.20 to version 2.1.1.

Where the documentation promises a simple migration, it is unfortunately not the case...

Why not? Because the documentation takes a lot of shortcuts on some points and is very succinct on others. And that's where the problem is, if the implementation of Traefik 1 was simple, the multiplicity of possibilities with the new version made the configuration confusing.

So I propose you below to highlight the points that I changed between the two versions. I won't go back to the installation from scratch of Traefik, which you can find on their website.

Important information about the YAML code blocks below: I made the choice to install Traefik in a dedicated namespace. The codes below contain a reference to this namespace. Be careful in case you want to copy/paste to update this point.

Prerequisites for installation: CRDs (Custom Resources Definitions) and ClusterRole update

To be able to implement the new version of Traefik in a simple way, it is first necessary to create CRDs, which will, very roughly, define new types of resources that the Traefik controller will be able to control. I won't go further on CRDs here, a full article would be necessary.

So we will create a traefik2-prereq.yml file with the content below, you can find this file on the official documentation :

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: ingressroutes.traefik.containo.us

spec:
  group: traefik.containo.us
  version: v1alpha1
  names:
    kind: IngressRoute
    plural: ingressroutes
    singular: ingressroute
  scope: Namespaced

---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: ingressroutetcps.traefik.containo.us

spec:
  group: traefik.containo.us
  version: v1alpha1
  names:
    kind: IngressRouteTCP
    plural: ingressroutetcps
    singular: ingressroutetcp
  scope: Namespaced

---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: middlewares.traefik.containo.us

spec:
  group: traefik.containo.us
  version: v1alpha1
  names:
    kind: Middleware
    plural: middlewares
    singular: middleware
  scope: Namespaced

---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: tlsoptions.traefik.containo.us

spec:
  group: traefik.containo.us
  version: v1alpha1
  names:
    kind: TLSOption
    plural: tlsoptions
    singular: tlsoption
  scope: Namespaced

---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: traefikservices.traefik.containo.us

spec:
  group: traefik.containo.us
  version: v1alpha1
  names:
    kind: TraefikService
    plural: traefikservices
    singular: traefikservice

Once this is done, we can load it into our Kubernetes cluster using the kubectl apply command:

kubectl apply -f ./traefik2-prereq.yml

Then, because of all these new resources, it is necessary to update our existing ClusterRole to allow Traefik to access this new information. So I modified my Clusterrole.yml file, which I replaced with the following content :

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: traefik-ingress-controller
  namespace: traefik
rules:
  - apiGroups:
      - ""
    resources:
      - services
      - endpoints
      - secrets
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - extensions
    resources:
      - ingresses
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - extensions
    resources:
      - ingresses/status
    verbs:
      - update
  - apiGroups:
      - traefik.containo.us
    resources:
      - middlewares
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - traefik.containo.us
    resources:
      - ingressroutes
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - traefik.containo.us
    resources:
      - ingressroutetcps
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - traefik.containo.us
    resources:
      - tlsoptions
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - traefik.containo.us
    resources:
      - traefikservices
    verbs:
      - get
      - list
      - watch

Once again, once completed, we apply this change:

kubectl apply -f ./clusterrole.yml

Deployment of the new version of Traefik

Now we will create the configuration file to deploy the pod containing Traefik 2. I have chosen to deploy Traefik as a DaemonSet, but the installation is similar if you want to deploy it in a pod. So I have the daemonset.yml file which has the following content :

apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
 name: traefik-ingress-controller
 namespace: traefik
 labels:
   k8s-app: traefik-ingress-lb
   kubernetes.io/cluster-service: "true"
spec:
 template:
   metadata:
     labels:
       k8s-app: traefik-ingress-lb
       name: traefik-ingress-lb
   spec:
     hostNetwork: true
     serviceAccountName: traefik-ingress-controller
     terminationGracePeriodSeconds: 60
     tolerations:
     - key: node-role.kubernetes.io/master
       effect: NoSchedule
     containers:
     - image: traefik:v2.1.1
       name: traefik-ingress-lb
       imagePullPolicy: Always
       resources:
         requests:
           cpu: 100m
           memory: 20Mi
       volumeMounts:
         - mountPath: "/cert/"
           name: cert
       args:
       - --providers.kubernetescrd
       - --accesslog
       - --entrypoints.web.address=:80
       - --entrypoints.websecure.address=:443
       - --certificatesresolvers.le.acme.email=masuperadressemail@monmail.com
       - --certificatesresolvers.le.acme.storage=acme.json
       - --certificatesresolvers.le.acme.storage=/cert/acme.json
       - --certificatesResolvers.le.acme.httpChallenge.entryPoint=web
       ports:
       - name: web
         containerPort: 80
       - name: websecure
         containerPort: 443
       - name: admin
         containerPort: 8080
     volumes:
     - name: cert
       hostPath:
         path: /home/kube/traefik/certs/
         type: Directory

As you will have understood, some points are to be adapted according to your case of use. Once again, I am not making this post to copy the official documentation, to which I refer you if you have any doubt.

Note that it is possible to activate the traefik dashboard from the command line if necessary by adding the parameter "--api.insecure" in the arguments of the launch line.

You will note the presence of the necessary setting for Let's Encrypt. It is this setting that allows me to have a TLS certificate automatically generated by Traefik. Moreover, I created a volume in hostPath to store these certificates and avoid to request them each time I update my pod.

From there, I'm in V2... but I don't have a backend anymore...

That's why we don't apply the change right away, and we will first create the necessary to communicate with our pods.

Updating of services and creation of IngressRoutes

Traefik 2 changes the way it works compared to the previous version.
Indeed, in the first version, there was this configuration :

Frontend -> Backend

Pretty basic, but enough. The new version brings new functionality, especially middleware, which interfaces between the frontend and the backend. The latter allows for example to add basic auth on an application. As a result, we now have this configuration

Frontend -> Middleware -> Backend

Moreover, with the new features brought by Traefik, it is necessary to switch from Ingress with IngressRoutes, which we created via our CRDs earlier. It is this new Kubernetes object that carries all the necessary configuration to define the routing to our backend.

So I'm going to put below the two configuration files, which I had in version 1.7.20 and 2.1.1, so that you can see the differences directly.

Configuration file 1.7.20

apiVersion: v1
kind: Service
metadata:
 name: traefik-web-ui
 namespace: traefik
spec:
 selector:
   k8s-app: traefik-ingress-lb
 ports:
 - port: 8080
   targetPort: 8080
   name: api
 - port: 443
   targetPort: 443
   name: https
 - port: 80
   targetPort: 80
   name: http

---

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
 name: traefik-web-ui
 namespace: default
 annotations:
   kubernetes.io/ingress.class: traefik
spec:
 rules:
 - http:
     paths:
     - path: /
       backend:
         serviceName: ghost-tfe-fr
         servicePort: 2368

Here is the equivalent on the new version:

apiVersion: v1
kind: Service
metadata:
 name: traefik-web-ui
 namespace: traefik
spec:
 selector:
   k8s-app: traefik-ingress-lb
   app: traefik-ingress-lb
 ports:
 - port: 8080
   targetPort: 8080
   name: api
 - port: 443
   targetPort: 443
   name: websecure
 - port: 80
   targetPort: 80
   name: web
   
---

apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: traefik-web-ui
  namespace: default
spec:
  entryPoints:
    - web
  routes:
  - kind: Rule
    match: Host(`tferdinand.net`) && PathPrefix(`/`)
    services:
    - name: ghost-tfe-fr
      port: 2368
      helthcheck:
        path: /
        host: tferdinand.net
        intervalSeconds: 10
        timeoutSeconds: 5

---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: traefik-web-ui-tls
  namespace: default
spec:
  entryPoints:
    - websecure
  routes:
  - kind: Rule
    match: Host(`tferdinand.net`) && PathPrefix(`/`)
    services:
    - name: ghost-tfe-fr
      port: 2368
      helthcheck:
        path: /
        host: tferdinand.net
        intervalSeconds: 10
        timeoutSeconds: 5
  tls:
    certResolver: le

As you can see, the new IngressRoute object carries much more information, including everything related to routing. Note that the hosts defined in the configuration files are the ones that will be used to generate the Let's Encrypt certificates at Traefik pod startup.

Now that everything is ready, we can apply our latest :

kubectl apply -f ./daemonset.yml
kubectl apply -f ./service_ingress.yml

The new version of Traefik is now normally deployed. The Let's Encrypt certificate takes one to two minutes to be imported. Please note that if you are testing configurations, I advise you to disable the TLS, indeed, Let's Encrypt has a maximum query limit, limit that you may reach, preventing you from generating certificates.

And then... ?

Now that I've finished my migration to version 2, I plan to add a finer management of authorized TLS ciphers, in order to increase the security level of my applications. I'll probably write an article about it soon.

It's completely possible to set up a much finer configuration, I haven't done a complete overview of all the new features brought by version 2.

I made this article after the observation that I made by migrating from version, indeed, although it is announced breaking changes. I must admit that I did not expect such differences.

Note that there is a migration tool made available by Traefik on GitHub, which allows to convert Kubernetes configuration files from V1 to V2. However, when I tested it :

  • Some configuration points do not work and refer to the documentation
  • The generated configuration was not functional

I hope this article will be useful for your Traefik migration, don't hesitate to react in the comments!