Les changements entre Traefik v1 et v2 sont assez importants car tous les concepts sont redéfinis entre les deux versions.
Cette page résume les étapes de migrations effectuées chez Picasoft.
Notez que ce mini-guide n’est valide que pour les versions de Traefik au dessus de v2.2
, qui a apporté de nombreuses améliorations. Auparavant, la migration nécessitait de très nombreuses redondances (redirection HTTP vers HTTPS, utilisation de TLS, utilisation de Let’s Encrypt, etc, à indiquer sur chaque conteneur).
Un aperçu complet des fichiers est disponible à la fin pour faciliter la compréhension.
Au moment de de la migration, les fonctionnalités recherchées sont :
L’objectif est de minimiser la configuration à effectuer via les labels, et de mettre le maximum dans les fichiers de configuration.
Avec Traefik v1, on configurait l’essentiel de Traefik via traefik.toml
: entrypoints (80/443), HTTP → HTTPS, Let’s Encrypt. Le fichier utilisé pour la v1 se trouve sur Gitlab.
Il s’agit d’un fichier statique, c’est-à-dire qu’il est chargé au démarrage de Traefik et ne change pas.
La configuration dynamique désigne toute la configuration de routage, et elle est amenée à changer souvent : démarrage d’un nouveau conteneur, etc.
La configuration dynamique se construit grâce à des providers (des composants existants qui donnent des informations sur où router, par exemple).
Dans notre cas, elle utilisera le provider docker
pour regarder les nouveaux conteneurs démarrés et router vers ceux-ci, et se servira du provider file
pour lire dans un fichier.
En effet, avec Traefik v1, on pouvait se contenter d’un fichier statique et de labels sur les conteneurs. Pour nos besoins, avec Traefik v2, il faudra créer un nouveau fichier (dynamique).
On commence par créer le fichier dynamique, qui contient normalement la configuration des routeurs et des middlewares.
frontend
de Traefik v1 : un par conteneur. Il analyse la requête pour décider où elle doit aller en fonction de critères comme l’URL.Dans Traefik v2, les options TLS et les middlewares sont configurées pour chaque routeur. Néanmoins, depuis Traefik v2.2, une fonctionnalité permet d’appliquer les options TLS et les middlewares à utiliser par défaut (voir prochaine section). Dans ce fichier, on se contentera de définir les middlewares et les options TLS, sans les appliquer à un routeur spécifique.
On ajoute les middlewares pour ajouter les headers de sécurité et le middleware pour compresser les flux :
[http] [http.middlewares.hardening.headers] addVaryHeader = true browserXssFilter = true contentTypeNosniff = true forceSTSHeader = true frameDeny = true stsIncludeSubdomains = true stsPreload = true customFrameOptionsValue = "SAMEORIGIN" referrerPolicy = "same-origin" featurePolicy = "vibrate 'self'" stsSeconds = 315360000 [http.middlewares.compression.compress] excludedContentTypes = ["text/event-stream"]
On ajoute la configuration TLS :
[tls.options] [tls.options.tls12] minVersion = "VersionTLS12" cipherSuites = [ "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", "TLS_AES_256_GCM_SHA384", "TLS_CHACHA20_POLY1305_SHA256" ] curvePreferences = ["CurveP521","CurveP384"]
hardening
, compression
et tls12
sont des noms arbitraires, qui permettent de les “importer” dans un routeur, par exemple.
Un outil développé par l’équipe de Containous permet de migrer traefik.toml
. Néanmoins, il est vraiment peu utile dans notre cas, il ne migre que les options simples et demande de faire le reste à la main.
On commence donc par les options basiques. Comme attendu, les providers permettent de construire la configuration dynamique depuis Docker et depuis le fichier construit plus haut.
[global] sendAnonymousUsage = false checkNewVersion = true [providers] providersThrottleDuration = "2s" [providers.docker] watch = true endpoint = "unix:///var/run/docker.sock" exposedByDefault = false network = "proxy" [providers.file] filename = "/traefik_dynamic.toml" watch = true [log] level = "INFO"
Notez le réseau proxy
qui sera celui dans lequel devront se trouver Traefik et tous les services.
On configure ensuite un fournisseur de certificat. Let’s Encrypt est utilisé par défaut. Cette configuration est similaire à la v1, seuls les nom des paramètres changent :
[certificatesResolvers] [certificatesResolvers.letsencrypt] [certificatesResolvers.letsencrypt.acme] email = "picasoft@assos.utc.fr" storage = "/certs/acme.json" [certificatesResolvers.letsencrypt.acme.httpChallenge] entryPoint = "web"
Notez le nom letsencrypt
, arbitraire, qui permettra de s’y référer.
Depuis Traefik v2.2, on retrouve la possibilité de définir dans la configuration statique, pour un entrypoint :
Pas besoin de configurer ces options pour chaque routeur, ce qui évite de se retrouver avec 10 labels par conteneur. Il est évidemment possible d’écraser ces options sur un conteneur spécifique.
[entryPoints] [entryPoints.web] address = ":80" [entryPoints.web.http.redirections.entryPoint] to = "websecure" scheme = "https" [entryPoints.websecure] address = ":443" [entryPoints.websecure.http] middlewares = ["hardening@file", "compression@file"] [entryPoints.websecure.http.tls] certResolver = "letsencrypt" options = "tls12@file"
On retrouve :
hardening@file
et compression@file
, qui se réfèrent aux middlewares déclarés dans le fichier de configuration dynamiquetls12@file
, qui se réfère aux options TLS déclarés dans le fichier de configuration dynamiqueletsencrypt
, qui se réfère au fournisseur de certificat déclaré dans la section certificatesResolvers
de la configuration statique.
Note : ce que fait cette configuration ne déroge pas aux concepts de Traefik v2, c’est juste une astuce de configuration. Elle crée un routeur attaché à l’entrypoint web
, et lui attache un middleware qui redirige vers l’entrypoint websecure
. Ensuite, elle copie les options TLS, le fournisseur de certificat utilisé et les middlewares sur chaque routeur créé sur l’entrypoint websecure
, ce qui évite d’écrire nous même les labels.
Avec Traefik v1, les labels ajouté sur les conteneurs pour activer le routage vers ce conteneur ressemblaient à ceci :
labels: traefik.frontend.rule: Host:X.picasoft.net traefik.port: 80 traefik.enable: true
Il y a plein d’autres options, mais c’est tout ce qu’on utilise chez Picasoft. Grâce aux options par défaut de Traefik v2.2, les labels à utiliser ne sont pas beaucoup plus nombreux :
labels: # websecure est l'entrypoint HTTPS - nom arbitraire défini dans la config traefik.http.routers.<service>.entrypoints: websecure traefik.http.routers.<service>.rule: Host(`X.picasoft.net`) traefik.http.services.<service>.loadbalancer.server.port: 80 traefik.enable: true
Où <service>
est un nom unique arbitraire.
Notez qu’une règle du style Host:voice.picasoft.net;Path:/metrics
devient :
Host(`voice.picasoft.net`) && Path(`/metrics`)
Le format du fichier acme.json
doit être migré pour fonctionner avec Traefik v2. On peut le faire avec traefik-migration-tool, mais cela nécessite que l’ensemble des services soient éteints.
Pour notre migration, nous avons purement et simplement supprimé le fichier, car il contenait de nombreux certificats inutilisés. Si le rate-limiting de Let’s Encrypt n’est pas un problème, tous les certificats seront re-générés lors du démarrage des conteneurs applicatifs.
$ cd /DATA/docker/certs $ sudo rm acme.json # Valid empty JSON object $ echo '{}' | sudo tee acme.json $ sudo chmod 600 acme.json
Il faudra simplement mettre à jour la version de Traefik et monter le fichier de configuration dynamique :
version: '3.7' # Common network for Traefik and services networks: proxy: # Same name than in traefik.toml name: 'proxy' services: traefik: image: traefik:2.3 [...] volumes: - /var/run/docker.sock:/var/run/docker.sock - ./traefik.toml:/traefik.toml - ./traefik_dynamic.toml:/traefik_dynamic.toml [...] networks: - proxy [...]
Pour le coup, c’est difficile sans interruption de service. Pour les services critiques, on peut combiner les labels de Traefik v1 et Traefik v2 et redémarrer ces services.
Ensuite, on peut essayer Traefik v2, et si ça ne fonctionne pas, revenir à Traefik v1 (nécessite des dossiers de configuration séparés pour passer facilement de l’un à l’autre).
Une fois qu’on est certain que Traefik v2 fonctionne bien, on peut éteindre les conteneurs applicatifs (via Compose ou autre), puis éteindre Traefik v1 et supprimer le conteneur.
On re-crée ensuite Traefik, puis les conteneurs applicatifs, et on surveille les logs.
Important:
Cette partie n’est pas encore en production. Il faut notamment créer les pages d’erreurs et les certificat wildcard avant de pouvoir le déployer
Traefik v2 permet de paramétrer aisément des pages personnalisées d’erreur. Pour cela, il faut définir un middleware qui va intercepter les code de retour d’erreur et appeler un service personnalisé à la place.
Le middleware peut être configuré au moyen de labels sur le conteneur ou via la configuration dynamique. Pour un soucis de lisibilité, on préférera le mettre dans la configuration dynamique.
errorspages
de type errorsnginx-errors
[http.middlewares.errorspages.errors] status = ["400-408", "500-505", 418] service = "nginx-errors" query = "{status}.html"
Le fichier statique doit maintenant appeler le middleware supplémentaire :
middlewares = ["hardening@file", "compression@file", "errorspages@file"]
Il faut maintenant ajouter le service nginx-errors
qui va servir les pages d’erreurs personnalisée. Pour cela, on crée un conteneur docker basé sur nginx pour servir les différentes pages.
FROM nginx:alpine COPY html /usr/share/nginx/html
le dossier html
doit contenir les différentes pages d’erreurs.
Dans un deuxième temps, on va ajouter ce conteneur en temps que service dans le docker-compose
de traefik.
errors: image: nginx-errors container_name: traefik_errors build: . labels: traefik.http.routers.traefik_errors.rule: HostRegexp(`{subdomain:.*}.test.picasoft.net`) traefik.http.routers.traefik_errors.priority: 1 traefik.http.services.traefik_errors.loadbalancer.server.port: 80 traefik.enable: true networks: - proxy restart: unless-stopped
Attention:
Afin d’éviter d’avoir des erreurs bizarres sur des sous-domaine non utilisés, on choisit de rediriger tous les domaines vers ce service. Cependant on rentre en conflit avec les autres services présents sur la machine. En effet, Traefik utilise une règle de longueur du match pour choisir la priorité d’un conteneur. Ainsi, la règle la plus longue aura la priorité la plus forte. le problème ici c’est que la règle .*
peut matcher une chaine de n’importe quelle longueur et à donc la priorité la plus haute. Il faut donc définir une priorité de 1 à la main pour laisser la préseance aux autre conteur.
Important:
Attention ! la récupération de tous les sous domaines par un même conteneur de manière sécurisé avec let’s encrypt nécessite de mettre en place un certificat wildcard. (TODO)
De même, il faut créer une page index.html
dans le conteneur errors-pages pour accueillir les visiteurs.
[global] sendAnonymousUsage = false checkNewVersion = true [entryPoints] [entryPoints.web] address = ":80" [entryPoints.web.http.redirections.entryPoint] to = "websecure" scheme = "https" [entryPoints.websecure] address = ":443" [entryPoints.websecure.http] middlewares = ["hardening@file", "compression@file", "errorspages@file"] [entryPoints.websecure.http.tls] certResolver = "letsencrypt" options = "tls12@file" [providers] providersThrottleDuration = "2s" [providers.docker] watch = true endpoint = "unix:///var/run/docker.sock" exposedByDefault = false network = "proxy" [providers.file] filename = "/traefik_dynamic.toml" watch = true [log] level = "INFO" [certificatesResolvers] [certificatesResolvers.letsencrypt] [certificatesResolvers.letsencrypt.acme] email = "picasoft@assos.utc.fr" storage = "/certs/acme.json" [certificatesResolvers.letsencrypt.acme.httpChallenge] entryPoint = "web"
[tls.options] [tls.options.tls12] minVersion = "VersionTLS12" cipherSuites = [ "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", "TLS_AES_256_GCM_SHA384", "TLS_CHACHA20_POLY1305_SHA256" ] curvePreferences = ["CurveP521","CurveP384"] [http] [http.middlewares.hardening.headers] addVaryHeader = true browserXssFilter = true contentTypeNosniff = true forceSTSHeader = true frameDeny = true stsIncludeSubdomains = true stsPreload = true customFrameOptionsValue = "SAMEORIGIN" referrerPolicy = "same-origin" featurePolicy = "vibrate 'self'" stsSeconds = 315360000 [http.middlewares.compression.compress] excludedContentTypes = ["text/event-stream"] [http.middlewares.errorspages.errors] status = ["400-408", "500-505", 418] service = "nginx-errors" query = "{status}.html"
version: '3.7' networks: proxy: name: 'proxy' services: traefik: image: traefik:2.3 container_name: traefik ports: - 80:80 - 443:443 volumes: - /etc/localtime:/etc/localtime:ro - /var/run/docker.sock:/var/run/docker.sock - ./traefik.toml:/traefik.toml - ./traefik_dynamic.toml:/traefik_dynamic.toml - /DATA/docker/traefik/certs:/certs networks: - proxy restart: unless-stopped errors: image: nginx-errors container_name: traefik_errors build: . labels: traefik.http.routers.traefik_errors.rule: HostRegexp(`{subdomain:.*}.test.picasoft.net`) traefik.http.routers.traefik_errors.priority: 1 traefik.http.services.traefik_errors.loadbalancer.server.port: 80 traefik.enable: true networks: - proxy restart: unless-stopped