Servir plusieurs applications web sur un même hôte ou derrière un même point d’entrée d’un réseau est une pratique aussi classique qu’indispensable dans de nombreuses situations. Sa mise en œuvre fait souvent appel à un laborieux reverse proxy Apache ou NginX. Mais voici Traefik Proxy, le couteau suisse du routage d’applications, léger et aux multiples atouts.
En dépit de son logo de rongeur, hérité du langage Go dans lequel ce logiciel est écrit, Traefik Proxy est, sous licence MIT, un véritable contrôleur aérien pour le Cloud. Notre première rencontre fut occasionnée par la nécessité pour moi de faire tourner, sur ma station de développement, une quantité respectable de micro-services d’un applicatif très distribué, regroupés dans une API à l’accès unifié. Depuis, outre sa compétence locale, il conserve une place de choix dans mes propositions d’architectures chaque fois qu’il m’est demandé de ne pas utiliser les fonctionnalités équivalentes de gateway et load balancing des fournisseurs de plateformes Cloud.
1. Les difficultés du routage
Il est fréquent de devoir rassembler des micro-services sur un même domaine, sous des chemins d’URL différents. Parfois encore, on souhaite héberger plusieurs noms de domaines sur un même serveur. Les solutions les plus simples à déployer, telles que les VirtualHost et le mod_proxy d’Apache, présentent l’inconvénient de la rigidité : ajouter un nouveau service ou domaine nécessite au minimum la reconfiguration du proxy, pour y inscrire le port privé ou le dossier racine.
Cette démarche devient encore plus fastidieuse, voire impraticable, dès lors que les serveurs sous-jacents sont en fait des conteneurs qui se renouvellent dans le cadre du déploiement continu. Dans ce cas, il est possible d’utiliser le programme Traefik qui ne fait que du routage, le fait très bien, et sait découvrir tout seul les services visés.
2. Description du montage
Nous allons étudier en laboratoire le dispositif présenté en figure 1, pour les besoins internes d’une université imaginaire : un hôte, sur lequel des applications web s’exécutent dans des conteneurs Docker. Prévoyons un service pour gérer des salles, un autre les fiches des étudiants ; puis un site concernant le stade, réparti en deux applications sur le même domaine, mais gérant des requêtes différenciées par leur préfixe. Enfin, un autre site est associé à un nom de domaine public, sur HTTPS exclusivement.
Après avoir montré comment reproduire sous Traefik une configuration figée équivalente à un ProxyPass Apache, nous étudierons l’utilisation en découverte automatique, qui convient particulièrement au Cloud.
3. Mise en œuvre de Traefik
Le logiciel traefik est un proxy applicatif spécialiste du Cloud. Il se présente sous la forme d’un petit binaire ayant pour vocation d’écouter les ports HTTP(S) et de diriger les requêtes vers différents services en fonction d’un ensemble de règles. Les destinations ne sont pas obligatoirement des conteneurs, mais c’est le cas d’utilisation le plus typique, comme nous le verrons dans la suite. Traefik s’intègre si bien avec Docker, qu’il est d’autant plus naturel de le conteneuriser lui aussi. C’est ce que nous ferons pour notre expérimentation.
Après une première étape dans laquelle nous décrirons statiquement les règles de routage vers des services explicitement identifiés, nous verrons comment Traefik prend tout son sens dans des systèmes dynamiques où les services démarrent à volonté.
Avant d’engager les expérimentations, assurons-nous de ne pas avoir déjà un serveur web allumé :
3.1 Installation
L’image Docker officielle est prête à l’emploi :
Avant de lancer un conteneur Traefik en écoute sur les ports web, nous devons préparer un fichier de configuration. Deux formats sont acceptés : TOML [1] et YAML. J’utiliserai ce dernier par simple préférence.
Créons dans le répertoire courant un dossier traefik qui peut rester vide pour le moment. Nous allons l’élaborer au cours de notre manipulation.
Nous pouvons maintenant démarrer le conteneur de routage. Il n’y a encore rien à desservir, mais c’est là toute l’idée : il n’est pas nécessaire de décrire la topologie à l’avance, Traefik s’adaptera.
Détaillons les paramètres pour docker :
- -d pour démarrer un conteneur en arrière-plan ;
- --rm pour le supprimer automatiquement lorsqu’on le stoppera ;
- -p connecte le port 80 de l’hôte sur le 80 du conteneur. C’est le port par défaut sur lequel Traefik écoute. Ainsi, c’est le conteneur qui prendra en charge le trafic web entrant sur l’hôte. C’est du Routing-as-a-Service !
- -v pour monter le répertoire local traefik du répertoire courant à l’intérieur du conteneur, en lecture seule ;
et à présent le paramètre passé au conteneur créé à partir de l’image traefik:2.4.2 :
- --providers.file.directory donne le chemin du dossier de configuration. C’est le chemin virtuel, à l’intérieur du conteneur, que nous avons monté grâce à -v.
Plusieurs providers de configuration existent, comme nous allons le voir, et ils peuvent même se combiner. Pour cette première étape, nous activons le file provider.
Tous les fichiers présents dans ce répertoire sont chargés au démarrage par Traefik, qui va aussi en surveiller les changements et mettre à jour sa configuration en cours d’exécution. Ce comportement est débrayable [2], mais c’est justement ce que nous voulons et nous le laissons actif par défaut.
Les fichiers surveillés, ainsi que les autres providers, constituent ce qu’on appelle la configuration dynamique de Traefik. Certains paramètres (tels que le port d’écoute et les réglages TLS) se font obligatoirement par une configuration statique, que nous verrons en chapitre 5, et qui ne peut pas changer pendant l’exécution du programme.
Testons dès maintenant l’état de notre routeur :
Une réponse tout à fait normale : le serveur web (Traefik) nous entend bien, mais ne sait pas quoi faire de cette requête.
Le répertoire de configuration est encore vide. Mettons en place notre topologie à la volée.
3.2 Déclaration explicite
Dans cette section, nous lançons des serveurs web dans des conteneurs, et nous notons leurs adresses IP internes, au sein du réseau Docker, que nous reportons dans la configuration dynamique de Traefik.
Créons dans un nouveau conteneur un serveur web élémentaire, par exemple avec l’image officielle d’Apache httpd:alpine. Il en serait bien sûr de même avec des applicatifs Node, PHP ou autres.
Et notons son adresse IP, extraite de la sortie de la commande docker inspect :
Nous allons lui écrire un texte explicite comme page d’accueil, pour faciliter l’expérience. Dans cette distribution, le dossier racine par défaut des documents web est /usr/local/apache2/htdocs :
ce qui nous donne immédiatement :
Notre premier serveur web répond bien sur sa propre adresse IP virtuelle. Notez que, puisque ce conteneur ne publie aucun port (pas de flag -p pour Docker), il n’est pas accessible directement de l’extérieur de l’hôte. C’est bien ce que ne nous voulons.
Faisons de même avec un deuxième serveur web :
Il est temps d’indiquer à Traefik les coordonnées de nos deux services. Créons dans notre répertoire local traefik/ un fichier config.yml avec ce contenu :
Cette configuration déclare deux routeurs (lignes 03 et 09), tous deux joignables sur le point d’entrée http (il s’agit ici d’un identifiant arbitraire, pas du nom du protocole. Mais il est défini par défaut par Traefik et désigne évidemment le HTTP sur le port 80 ; nous en reparlerons dans la suite). À chacun de ces routeurs, nous attribuons un service (lignes 06 et 12) ainsi qu’une règle de routage (lignes 07 et 13) : il s’agit ici d’aiguiller en fonction du nom d’hôte contacté.
Notez les `back-quotes` pour encadrer les chaînes de caractères dans la règle Host : c’est une exigence de Traefik, originaire du langage Go.
Place ensuite à la déclaration des services (ligne 15) : nous en avons deux, qui correspondent aux noms promis en lignes 06 et 12. Chacun doit obligatoirement apporter son groupe de serveurs sous la rubrique loadBalancer. Dans notre cas, nous n’avons qu’une seule instance par service, mais la configuration doit tout de même être fournie sous forme de liste. Le loadBalancer peut avoir d’autres options liées au health-check des nœuds cibles, ou encore à l’affinité des sessions (stickiness). Nous n’aborderons pas le sujet du Load Balancing de Traefik dans ces pages.
Nous renseignons donc pour ces services une URL de destination. Il s’agit ici d’adresses internes, vues depuis le programme Traefik. Il est tout à fait possible de cibler d’autres hôtes physiques sur un réseau local, ou même des adresses publiques.
Toutes les déclarations que nous venons de faire sont des éléments de dynamic configuration : ils définissent des règles de routage et les emplacements de serveurs destinataires, c’est-à-dire des informations qui peuvent évoluer dans le temps, sans redémarrage de Traefik. De fait, celui-ci étant en mode watch sur le fichier, notre configuration est aussitôt prise en compte.
Essayons :
Dans cette commande, nous passons au proxy (localhost:80) un en-tête qui indique quel hôte nous voulons solliciter. En effet, l’adresse IP sur laquelle on initie la connexion HTTP n’est pas le seul critère qui détermine quel applicatif est ciblé via ce protocole. C’est ce qui se produit automatiquement lorsqu’on visite une URL avec un navigateur : le nom saisi dans la barre d’adresse est passé comme en-tête dans la requête.
Et nous voyons ici que Traefik a bien transféré notre requête vers le bon serveur, en fonction de l’hôte demandé. Pour s’en convaincre, le lecteur sceptique pourra utiliser une résolution de nom artificielle par /etc/hosts pour requêter directement le nom d’hôte http://students.local/ sans l’argument -H.
/etc/hosts
Pour résoudre l’adresse IP qui correspond à un nom de domaine, le système s’adresse normalement à un service DNS. Mais il existe la possibilité de forcer une correspondance, au moyen du fichier /etc/hosts (détenu par le root, car d’une sensibilité sécuritaire cruciale).
Peut-être sans même le savoir, vous l’exploitez déjà abondamment puisque c’est celui qui permet de traduire localhost en 127.0.0.1 sans besoin de questionner un serveur DNS.
Ce fichier contient des lignes de correspondances « adresse nom » qui sont prioritaires pour le système pour toute résolution de nom d’hôte. Tout processus qui veut effectuer une sortie réseau vers un hôte nommé obtient d’abord du système l’adresse IP indiquée de force dans ce fichier.
Ainsi pour notre lab, on associerait les noms fictifs à la machine locale en enrichissant comme ceci le fichier /etc/hosts :
Plus généralement, si on utilisait un autre ordinateur pour envoyer des requêtes vers le proxy Traefik dont l’adresse IP serait 10.0.0.20, aiguillant le domaine my.domain.com, le fichier /etc/hosts de cette autre station devrait contenir :
3.3 Les Providers de configuration
Dussions-nous lancer un nouveau service pour répondre à un autre nom d’hôte, nous ajouterions les sections adéquates à notre fichier YAML, et le tour serait joué.
Mais les conteneurs sont des êtres volatiles qui s’éteignent et renaissent parfois intempestivement, à des adresses imprévisibles. Voyons maintenant une approche plus efficace pour automatiser le suivi de la configuration de routage.
En plus des fichiers de configuration, Traefik est capable de se connecter à d’autres sources de données pour déclarer et suivre la définition des routeurs. Parmi les plus classiques, citons les bases de données clé-valeur que sont Redis et Etcd. Mais, Docker lui-même peut constituer une source, comme détenteur de ses propres conteneurs.
En effet, qui est mieux placé que le Docker Engine pour savoir quand démarrent et s’arrêtent les conteneurs, et à quelle adresse les joindre ? Il ne reste plus qu’à associer aux conteneurs des métadonnées qui précisent le rôle que chacun joue dans l’infrastructure. Or Docker offre la possibilité de leur attribuer des étiquettes, ou labels [3] : il s’agit de couples clé-valeur, immuables durant le cycle de vie du conteneur, et qui peuvent être consultés en ligne de commandes ainsi que via l’API Docker.
Voyons maintenant comment coupler Traefik à Docker en tant que Provider.
4. Découverte automatique
Sans entrer trop en détail dans l’architecture de Docker, rappelons simplement que le moteur utilise classiquement un socket Unix pour communiquer avec les outils clients (tels que la commande docker elle-même).
Le chemin du socket est par défaut /var/run/docker.sock et nous allons en permettre la lecture au processus Traefik afin qu’il soit informé des événements de démarrage et arrêt de conteneurs.
Stoppons d’abord notre ancien Traefik :
pour en relancer un autre ayant accès au socket en lecture seule.
Nous avons conservé l’ancien provider (fichier), mais cette fois nous activons en plus le provider Docker. Traefik connaît donc nos deux premiers serveurs web comme précédemment, et nous allons ajouter de nouvelles instances, auto-configurées au moyen de labels, que Traefik va découvrir tout seul.
4.1 Labels
Lançons un troisième serveur HTTP, semblable aux deux précédents ; mais cette fois, nous lui attachons des étiquettes :
En démarrant ce nouveau conteneur, nous lui donnons un label compris par Traefik, qui stipule la règle de routage souhaitée (attention avec l’utilisation des back-quotes en ligne de commandes ! Il faut les protéger par un \).
Donnons-lui du contenu identifiable :
et vérifions que ce nouvel aiguillage se produit aussitôt :
C’est gagné : il a suffi d’étiqueter le conteneur lors de sa création, pour qu’il entre dans le périmètre de routage. Pourtant, nous n’avons pas spécifié le port à utiliser ! Comment Traefik sait-il que ce conteneur écoute le port 80 ? Réponse : c’est le seul port exposé par son image ; il est automatiquement câblé.
Mais il est évidemment possible de spécifier un port différent si besoin, et c’est même nécessaire si le conteneur en expose plusieurs. Cela se fait au moyen d’un autre label : traefik.http.services.my-service.loadbalancer.server.port=3000.
4.2 Préfixes d’URL
Allons plus loin : supposons que notre application Stadium App prenne en charge une partie admin, gérée par un autre conteneur, avec des URL en /admin/*.
Il répond au même Host, mais avec une règle supplémentaire sur le préfixe du Path :
Il a fallu déclarer un nouveau nom de routeur, car ils doivent être uniques. Mais le nom choisi n’a pas d’autre utilité.
La règle peut combiner plusieurs conditions avec && et || (ainsi que des expressions portant sur les paramètres d’URL, des segments dans le Path avec ou sans RegExp, des valeurs d’en-têtes HTTP, etc.)
Toutefois, le Path de l’URL est passé tel quel en sortie du routage. Donc le conteneur doit servir des URL ayant le même préfixe. Nous allons créer dans notre petit httpd un répertoire admin :
Vérifions la bonne marche de ces deux règles sur le même host : l’une avec un préfixe, l’autre par défaut pour toutes les autres routes :
5. Certificats TLS
Si les échanges entre services au sein d’un réseau privé peuvent se faire en HTTP, il n’est généralement pas envisageable de s’abstenir de chiffrement pour les communications externes.
Traefik agit comme une terminaison SSL, c’est-à-dire qu’en tant que proxy il échange en chiffré avec le visiteur, mais à l’intérieur du périmètre qu’il route, il communique en clair avec les services. En d’autres termes, nos petits serveurs httpd ne doivent pas s’occuper de SSL du tout.
La prise en charge du HTTPS, au niveau de Traefik, peut se faire de deux façons : soit avec des fichiers de certificats précisés manuellement comme on l’aurait fait classiquement dans la configuration Apache ou Nginx ; soit avec des certificats automatiques grâce au protocole ACME. C’est cette deuxième approche que nous allons utiliser ici, avec Let’s Encrypt.
5.1 Configuration
Les paramètres concernant les certificats et leur obtention automatique se déclarent obligatoirement dans la partie statique de la configuration. Plusieurs méthodes existent pour la fournir [4] (arguments de ligne de commandes comme précédemment, fichier dans /etc/, variables d’environnement, etc.) ; nous opterons pour le fichier /etc/traefik/traefik.yml au sein du conteneur, que Traefik charge tacitement s’il existe.
La contrainte importante à connaître est que ces méthodes sont mutuellement exclusives : une fois fourni un fichier de configuration statique, il n’est plus possible d’ajouter des options sur la ligne de commandes.
Préparons un fichier local nommé arbitrairement etc.yml ayant le contenu suivant :
Explications :
- Nous déclarons, en 01-05, deux points d’entrée sur Traefik : l’un sur le port 80, que nous nommons ici « clear », l’autre que nous appelons « secure » sur le 443. La négociation automatique de certificats exige que Let’s Encrypt puisse contacter notre Traefik sur le port 80 en clair : c’est le sens des lignes 12-13. Notez qu’en l’absence de configuration, le point d’entrée par défaut pour Traefik se nomme « http » et nous l’avons déjà utilisé comme entryPoint dans nos routeurs au chapitre 3.2 Déclaration explicite.
- Le gestionnaire de certificats est configuré en lignes 07-13. Nous nommons le nôtre arbitrairement « auto », mais c’est vraiment la clé acme de la ligne 09 qui fait comprendre à Traefik que nous contactons Let’s Encrypt pour ce travail. L’adresse e-mail est utilisée pour recevoir les alertes d’échecs et autres informations importantes liées au renouvellement. Enfin, la ligne 11 indique le fichier du magasin de certificats, dans lequel la négociation conservera ses données. Muni de ce paramétrage, Traefik va obtenir pour nous les certificats correspondants aux domaines mentionnés dans la règle Host des conteneurs dynamiques que nous allons lancer. Il va même prendre en charge le renouvellement automatique trente jours avant leur expiration. Le magasin de certificats doit persister par-delà les redémarrages de Traefik (et notamment la suppression du conteneur), car c’est lui qui maintient les clés, l’état des demandes, les dates de renouvellement, etc. Aussi, nous allons le monter en tant que volume dans le conteneur Traefik.
- Nous avons choisi de démarrer Traefik sur une configuration statique précisée dans son fichier conventionnel du répertoire /etc. Par conséquent, nous ne pouvons plus lui fournir d’éléments de configuration statique via des flags en ligne de commandes. Aussi, c’est dans ce fichier que nous devons activer la découverte dynamique par Docker, ce que nous faisons en ligne 16. Cela ne nous empêche pas, d’ailleurs, d’activer aussi en 17-18 le file provider en la personne de ce même fichier ! Ce faisant, Traefik y puisera des éléments de configuration dynamique (voir alinéa suivant sur le middleware).
- En ligne 21, nous déclarons un élément de configuration dynamique : un middleware, que nous choisissons de nommer force-https et dont le rôle se comprend de lui-même. Nous pourrons ensuite brancher ce middleware sur les services de notre choix.
Voici finalement la manipulation de lancement :
5.2 Terminaison TLS
Il est temps de lancer un nouveau service, associé à un domaine réel (le nom de domaine affiché dans la suite est bien sûr factice) :
Prenons le temps d’examiner ce que l’on vient d’écrire :
- Comme vu plus haut, nous créons un routeur que nous choisissons d’appeler safe, ayant pour rôle de traiter les requêtes pour l’hôte my.domain.com sur l’entrée secure (déclarée dans la configuration statique vue précédemment).
- Ce routeur safe doit exploiter la terminaison TLS de Traefik, et obtenir son certificat depuis le dispositif qu’on a nommé auto dans la configuration statique.
- Nous créons aussi un routeur nommé bump pour ce même service ! Mais il écoute sur le port en clair, et nous demandons à Traefik de faire passer le transit à travers le middleware intitulé force-https. Ce dernier a pour effet d’émettre une redirection de HTTP vers HTTPS, en sorte que les clients seront automatiquement contraints d’utiliser HTTPS avec ce domaine.
Dès le démarrage de ce conteneur, Traefik est informé qu’on lance un nouveau Host my.domain.com en TLS. Laissons-lui quelques secondes pour se connecter à Let’s Encrypt et obtenir le certificat.
Puis, vérifions le résultat :
Vérifions aussi le certificat :
Et pour finir, assurons-nous que la visite en HTTP rebascule sur HTTPS :
Conclusion
Un proxy d’une telle simplicité, automatiquement réglé sur Docker, c’est un gain de temps garanti pour le développement.
Le projet revendique des performances respectables en production, quoique pas encore du niveau de celles de NGINX. Concernant le passage à l’échelle, Traefik sait gérer la répartition de charge sur les services, et peut être lui-même distribué grâce à ses multiples connecteurs vers des systèmes de persistance (Redis, ZooKeeper, etc.) pour synchroniser l’état sur la santé des serveurs et les certificats. Le programme intègre également un tableau de contrôle visuel (figure 2), ce qui en fait une solution fiable et complète.
Références
[1] Format de configuration TOML : https://toml.io/en/
[2] Flags de la commande Traefik : https://doc.traefik.io/traefik/reference/static-configuration/cli/
[3] Les labels Docker : https://docs.docker.com/engine/reference/commandline/run/#set-metadata-on-container--l---label---label-file
[4] Les possibilités mutuellement exclusives de configuration statique :
https://doc.traefik.io/traefik/getting-started/configuration-overview/#the-static-configuration