J'ai récemment migré le front de ce blog de Traefik 1 à Traefik 2, et le moins que l'on puisse dire, c'est que ce n'est pas une partie de plaisir.

Mon cas d'usage de Traefik

J'utilise Traefik en tant que load balancer/reverse proxy front dans une infrastructure Kubernetes. Mon utilisation est très basique. En fonction de certains chemin et/ou domaine, je redirige vers des pods distincts. Dans le cas ci dessous, je vais considérer que je n'ai qu'un pod, ce blog. Je gère aussi mes certificats avec Traefik via Let's Encrypt.

Bien que ce use case puisse paraître basique, il m'aura fallu plusieurs heures pour passer de Traefik 1. 7.20 à la version 2.1.1

Là où la documentation promet une migration simple, ce n'est malheureusement pas le cas...

Pourquoi ? Parce que la documentation prends pas mal de raccourcis sur certains points et est très succincte sur d'autres. Et c'est là que le bât blesse, si la mise en place de Traefik 1 était simple, la multiplicité des possibilités avec la nouvelle version à rendu la configuration confuse.

Je vous propose donc ci dessous de mettre en avant les points que j'ai changé entre les deux versions. Je ne reviendrais pas sur l'installation from scratch de Traefik, que vous pouvez trouver sur leur site.

Information importante sur les blocs de code YAML ci dessous : J'ai fait le choix d'installer Traefik dans un namespace dédié. Les codes ci dessous contiennent donc une référence à ce namespace. Faites attention dans le cas ou vous souhaiteriez faire du copier/coller à bien mettre à jour ce point.

Prérequis à l'installation : Des CRD (Custom Resources Definitions) et mise à jour du ClusterRole

Pour pouvoir mettre en place de manière simple la nouvelle version de Traefik, il est d'abord nécessaire de créer des CRD, ces dernières vont, très grossièrement, définir de nouveaux types de ressources que le controller Traefik pourra piloter. Je ne vais pas aller plus loins sur les CRD ici, un article complet serait nécessaire.

Nous allons donc créer un fichier traefik2-prereq.yml avec le contenu ci dessous, vous pouvez d'ailleurs retrouver ce fichier sur la documentation officielle :

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

Une fois cette étape effectuée, on peut le charger dans notre cluster Kubernetes à l'aide de la commande kubectl apply :

kubectl apply -f ./traefik2-prereq.yml

Ensuite, en raison de toutes ces nouvelles ressources, il est nécessaire de mettre à jour notre ClusterRole existant pour permettre à Traefik d'accéder à ces nouvelles informations. J'ai donc modifié mon fichier clusterrole.yml, que j'ai remplacé par le contenu suivant :

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

Une fois encore, une fois terminé, on applique ce changement :

kubectl apply -f ./clusterrole.yml

Déploiement de la nouvelle version de Traefik

Maintenant, nous allons créer le fichier de configuration afin de déployer le pod contenant Traefik 2. Pour ma part, j'ai fais le choix de déployer Traefik en tant que DaemonSet, mais l'installation est similaire si vous souhaitez le déployer dans un pod. J'ai donc le fichier daemonset.yml qui a le contenu suivant :

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

Vous l'aurez compris, certains points sont à adapter en fonction de votre cas d'usage. Encore une fois, je ne fais pas ce billet pour copier la documentation officielle, vers laquelle je vous renvoie si vous avez un doute.

A noter qu'il est possible d'activer le dashboard de traefik en ligne de commande si nécessaire en ajoutant le paramètre "--api.insecure" dans les arguments de la ligne de lancement.

Vous noterez la présence du paramétrage nécessaire pour Let's Encrypt. C'est ce paramètrage qui me permet d'avoir un certificat TLS généré automatiquement par Traefik. De plus, j'ai créé un volume en hostPath pour stocker ces certificats et éviter de les requêter à chaque mise à jour de mon pod.

A partir de là, je suis en V2... mais je n'ai plus de backend...

C'est pour cela, que l'on applique pas de suite le changement, et on va d'abord créer le nécessaire pour communiquer avec nos pods.

Mise à jour des services et création des IngressRoutes

Traefik 2 change la manière dont il fonctionne par rapport à la version précédente.

En effet, dans la première version, il y avait cette configuration :

Frontend -> Backend

Assez basique, mais suffisant. La nouvelle version apporte de nouvelle fonctionnalité, et notament les middleware, qui s'interface entre le frontend et le backend. Ces derniers permettent par exemple de rajouter du basic auth sur une application. De ce fait, nous avons maintenant cette configuration

Frontend -> Middleware -> Backend

De plus, avec les nouvelles fonctionnalités apportées par Traefik, il est nécessaire de passer d'Ingress avec des IngressRoutes, que nous avons créé via nos CRD plus tôt. C'est ce nouvel objet Kubernetes qui porte l'ensemble de la configuration nécessaire pour définir le routing jusqu'à notre backend.

Je vais donc vous mettre ci dessous les deux fichiers de configuration, que j'avais en version 1.7.20 et en 2.1.1, afin que vous puissiez voir directement les différences.

Fichier de configuration 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

Voici donc l'équivalent sur la nouvelle 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

Comme on peut le voir, le nouvel objet IngressRoute porte beaucoup plus d'informations, et notamment tout ce qui concerne le routing. A noter que les hosts définis dans les fichiers de configuration sont ceux qui seront utilisés pour générer les certificats Let's Encrypt au démarrage du pod Traefik.

Maintenant que tout est prêt, on peut appliquer nos dernières configurations :

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

La nouvelle version de Traefik est normalement maintenant déployée. Le certificat Let's Encrypt prends une à deux minutes à être importé. A noter que si vous faites des tests de configurations, je vous conseille de désactiver le TLS, en effet, Let's Encrypt possède une limite de requêtes maximales, limite que vous risquez d'atteindre, vous empêchant la génération de certificats.

Et ensuite ... ?

Maintenant que j'ai fini ma migration sur la version 2, je compte ajouter une gestion plus fine des ciphers TLS autorisés, afin d'augmenter le niveau de sécurité de mes applications. Je vous ferait sans doute un article à ce sujet prochainement.

Il est complétement possible de mettre en place une configuration beaucoup plus fine, je n'ai pas fait le tour de toutes les nouvelles fonctionnalités apportées par la version 2.

J'ai fait cet article après le constat que j'ai fait en migrant de version, en effet, bien qu'il soit annoncé des breaking changes. Je dois admettre que je ne m'attendait pas à de telles différences.

A noter qu'il existe un outil de migration mis à disposition par Traefik sur GitHub, qui permet de convertir les fichiers de configuration Kubernetes de la V1 à la V2. Néanmoins, lorsque je l'ai testé :

  • Certains point de configuration ne fonctionnent pas et renvoient vers la documentation
  • La configuration générée n'était pas fonctionnelle

J'espère que cet article vous sera utile pour votre migration Traefik, n'hésitez pas à réagir dans les commentaires !