Reverse Proxy Traefik avec Docker Compose et Docker Swarm

Temps de lecture ~8 minutes

Mon dernier article à propos de Docker Swarm était le premier d’une série d’articles que je tenais à écrire à propos de la stack utilisée par NLP Cloud. NLP Cloud est une API qui se base sur spaCy et les transformers HuggingFace afin de proposer Named Entity Recognition (NER), analyse de sentiments, classification de texte, summarization, et bien plus. Un des challenges est que chaque modèle tourne au sein de son propre conteneur et que de nouveaux modèles sont régulièrement ajoutés au cluster. Nous avons donc besoin d’un reverse proxy qui soit à la fois performant et flexible en face de tous ces conteneurs.

La solution que nous avons choisie est Traefik.

Je me suis dit qu’il serait intéressant d’écrire un article sur la façon dont nous avons implémenté Traefik et pourquoi nous avons fait ce choix plutôt qu’un reverse proxy classique comme Nginx.

Pourquoi Traefik

Traefik est un reverse proxy encore relativement jeune comparé à Nginx ou Apache, mais l’outil gagne rapidement en popularité. Le principal avantage de Traefik est qu’il s’intègre parfaitement à Docker, Docker Compose, et Docker Swarm (et même Kubernetes et autre) : en gros, l’intégralité de votre configuration Traefik peut se place dans votre fichier docker-compose.yml, ce qui est fort pratique. Et, dès que vous ajoutez un nouveau service à votre cluster, Traefik les découvre à la volée sans avoir à être redémarré.

Donc Traefik améliore nettement la maintenabilité d’un cluster, et est un gros progrès du point de vue haute disponibilité.

L’outil est développé en Go alors que Nginx est codé en C, donc je suppose que cela crée une légère différence en termes de performance, mais rien que je n’aie pu concrètement constater. A mon avis la pénalité en termes de performances est négligeable en regard des avantages apportés.

En revanche, l’apprentissage de Traefik demande un certain temps et, bien que la documentation soit assez bonne, il est relativement facile de faire des erreurs de configuration sans en comprendre l’origine. Laissez-moi donc vous donner quelques exemples prêts à l’emploi ci-dessous.

Installer Traefik

Il n’y a, en gros, pas grand chose à faire ici. Traefik est juste une nouvelle image Docker à ajouter à votre cluster en tant que service dans votre docker-compose.yml:

version: '3.8'
services:
    traefik:
        image: traefik:v2.3

Il existe plusieurs façons d’intégrer Traefik mais, comme je le disais ci-dessus, nous allons opter pour l’intégration avec Docker Compose.

Configuration de base

90% de la congfiguration Traefik est effectuée via les labels Docker.

Imaginons que nous ayons 3 services :

  • un site vitrine qui est simplement servi comme un site statique à l’adresse http://nlpcloud.io,
  • un modèle spaCy en_core_web_sm servi via une API Python FastAPI à l’adresse http://api.nlpcloud.io/en_core_web_sm,
  • un modèle spaCy en_core_web_lg servi via une API Python FastAPI à l’adresse http://api.nlpcloud.io/en_core_web_lg.

Plus de détails au sujet des modèles NLP spaCy ici et FastAPI ici.

version: '3.8'
services:
    traefik:
        image: traefik:v2.4
        ports:
            - "80:80"
        command:
            - --providers.docker
        volumes:
            - /var/run/docker.sock:/var/run/docker.sock:ro
    corporate:
        image: <your corporate image>
        labels:
            - traefik.http.routers.corporate.rule=Host(`localhost`)
    en_core_web_sm:
        image: <your en_core_web_sm model API image>
        labels:
            - traefik.http.routers.en_core_web_sm.rule=Host(`api.localhost`) && PathPrefix(`/en_core_web_sm`)
    en_core_web_lg:
        image: <your en_core_web_lg model API image>
        labels:
            - traefik.http.routers.en_core_web_lg.rule=Host(`api.localhost`) && PathPrefix(`/en_core_web_lg`)

Vous pouvez désormais accéder à votre site vitrine sur http://localhost, votre modèle en_core_web_sm sur http://api.localhost/en_core_web_sm, et votre modèle en_core_web_lg sur http://api.localhost/en_core_web_lg.

Comme vous pouvez le voir, c’est simple comme bonjour.

La configuration ci-dessus était pour notre configuration locale uniquement, donc nous voulons désormais faire de même pour la production dans un cluster Docker Swarm :

version: '3.8'
services:
    traefik:
        image: traefik:v2.4
        ports:
            - "80:80"
        command:
            - --providers.docker.swarmmode
        volumes:
            - /var/run/docker.sock:/var/run/docker.sock:ro
        deploy:
            placement:
                constraints:
                    - node.role == manager
    corporate:
        image: <your corporate image>
        deploy:
            labels:
                - traefik.http.routers.corporate.rule=Host(`nlpcloud.io`)
    en_core_web_sm:
        image: <your en_core_web_sm model API image>
        deploy:
            labels:
                - traefik.http.services.en_core_web_sm.loadbalancer.server.port=80
                - traefik.http.routers.en_core_web_sm.rule=Host(`api.nlpcloud.io`) && PathPrefix(`/en_core_web_sm`)
    en_core_web_lg:
        image: <your en_core_web_lg model API image>
        deploy:
            labels:
                - traefik.http.services.en_core_web_lg.loadbalancer.server.port=80
                - traefik.http.routers.en_core_web_lg.rule=Host(`api.nlpcloud.io`) && PathPrefix(`/en_core_web_lg`)

Vous pouvez désormais accéder à votre site vitrine sur http://nlpcloud.io, votre modèle en_core_web_sm sur http://api.nlpcloud.io/en_core_web_sm, et votre modèle en_core_web_lg sur http://api.nlpcloud.io/en_core_web_lg.

C’est toujours relativement simple, mais il est important de noter les points suivants :

  • il faut explicitement utiliser le provider docker.swarmmode au lieu de docker,
  • les labels doivent désormais être placés dans la section deploy,
  • il nous faut déclarer manuellement le port de chaque service en utilisant la directive loadbalancer (cela doit être fait manuellement car Docker Swarm ne permet pas d’exposer automatiquement les ports),
  • nous devons nous assurer que Traefik sera déployé sur un noeud manager du Swarm en utilisant constraints.

Vous avez désormais un cluster parfaitement fonctionnel grâce à Docker Swarm et Traefik. Maintenant il est probable que vous ayez des exigences spécifiques à votre projet, et aucun doute que la documentation Trafik vous aidera. Mais laissez-moi vous montrer quelques fonctionnalités que nous utilisons chez NLP Cloud.

Déporter l’authentification

Disons que vos points d’accès à l’API NLP sont protégés et que les utilisateurs ont besoin d’un token pour y accéder. Une bonne solution pour ce cas d’usage est de mettre à profit Traefik ForwardAuth.

En deux mots, Traefik redirigera toutes les requêtes utilisateurs vers une page dédiée créée pour l’occasion. Cette page se chargera d’examiner les en-têtes de la requête (et peut-être extraire un token d’authentification par exemple) et de déterminer si l’utilisateur a le droit d’accéder à la ressource. Si c’est le cas, la page doit retourner un code HTTP 2XX.

Si un code 2XX est retourné, Traefik effectuera alors la vraie requête vers le point d’accès API final. Sinon, il retournera une erreur.

Veuillez noter que, pour des raisons de performance, Traefik redirige uniquement les en-têtes des requêtes vers votre page d’authentification, et non pas le corps de la requête. Il n’est donc pas possible d’autoriser une requête utilisateur à partir d’éléments situés dans le corps de la requête.

Voici comment s’y prendre :

version: '3.8'
services:
    traefik:
        image: traefik:v2.4
        ports:
            - "80:80"
        command:
            - --providers.docker.swarmmode
        volumes:
            - /var/run/docker.sock:/var/run/docker.sock:ro
        deploy:
            placement:
                constraints:
                    - node.role == manager
    corporate:
        image: <your corporate image>
        deploy:
            labels:
                - traefik.http.routers.corporate.rule=Host(`nlpcloud.io`)
    en_core_web_sm:
        image: <your en_core_web_sm model API image>
        deploy:
            labels:
                - traefik.http.services.en_core_web_sm.loadbalancer.server.port=80
                - traefik.http.routers.en_core_web_sm.rule=Host(`api.nlpcloud.io`) && PathPrefix(`/en_core_web_sm`)
                - traefik.http.middlewares.forward_auth_api_en_core_web_sm.forwardauth.address=https://api.nlpcloud.io/auth/
                - traefik.http.routers.en_core_web_sm.middlewares=forward_auth_api_en_core_web_sm
    api_auth:
        image: <your api_auth image>
        deploy:
            labels:
                - traefik.http.services.en_core_web_sm.loadbalancer.server.port=80
                - traefik.http.routers.en_core_web_sm.rule=Host(`api.nlpcloud.io`) && PathPrefix(`/auth`)

Chez NLP Cloud, le service api_auth est en réalité une image Django + Django Rest Framework en charge de l’authentification des requêtes.

Pages d’erreur personnalisées

Peut-être que vous ne voulez pas afficher des pages d’erreurs Traefik brutes à vos utilisateurs. Dans ce cas, il est possible de remplacer ces pages d’erreur par défaut par des pages d’erreur personnalisées.

Traefik ne stocke pas de page d’erreur personnalisée en mémoire, mais il peut utiliser des pages d’erreur servies par l’un de vos services. Lorsque vous contactez votre service afin de récupérer la page d’erreur custom, Traefik passe aussi le code erreur HTTP comme argument positionnel, de façon à ce que vous puissiez afficher différentes pages d’erreur selon l’erreur HTTP initiale.

Imaginons que vous ayez un petit site statique servi par Nginx qui serve vos pages d’erreurs personnalisées. Vous voulez désormais utiliser ses pages d’erreur pour toutes les erreurs HTTP 400 à 599. Voici comment faire :

version: '3.8'
services:
    traefik:
        image: traefik:v2.4
        ports:
            - "80:80"
        command:
            - --providers.docker.swarmmode
        volumes:
            - /var/run/docker.sock:/var/run/docker.sock:ro
        deploy:
            placement:
                constraints:
                    - node.role == manager
            labels:
                - traefik.http.middlewares.handle-http-error.errors.status=400-599
                - traefik.http.middlewares.handle-http-error.errors.service=errors_service
                - traefik.http.middlewares.handle-http-error.errors.query=/{status}.html
    corporate:
        image: <your corporate image>
        deploy:
            labels:
                - traefik.http.routers.corporate.rule=Host(`nlpcloud.io`)
                - traefik.http.routers.corporate.middlewares=handle-http-error
    errors_service:
        image: <your static website image>
        deploy:
            labels:
                - traefik.http.routers.corporate.rule=Host(`nlpcloud.io/errors`)

Par exemple, grâce à l’exemple ci-dessus, une page d’erreur 404 utiliserait désormais cette page : http://nlpcloud.io/errors/404.html

HTTPS

Une fonctionnalité sympa de Traefik est sa capacité à provisionner et utiliser automatiquement les certificats TLS Let’s Encrypt.

Il ont un excellent tuto sur la façon de mettre cela en place avec Docker donc je me contente de vous diriger vers la bonne ressource : https://doc.traefik.io/traefik/user-guides/docker-compose/acme-tls/

Réhausser la limite d’upload

La taille limite d’upload par défaut est assez basse pour des raisons de performance (je crois que c’est 4194304 octets mais je ne suis pas 100% certain parce que ce n’est pas dans la doc).

Afin de réhausser cette limite, il faut utiliser la directive maxRequestBodyBytes :

version: '3.8'
services:
    traefik:
        image: traefik:v2.4
        ports:
            - "80:80"
        command:
            - --providers.docker.swarmmode
        volumes:
            - /var/run/docker.sock:/var/run/docker.sock:ro
        deploy:
            placement:
                constraints:
                    - node.role == manager
    corporate:
        image: <your corporate image>
        deploy:
            labels:
                - traefik.http.routers.corporate.rule=Host(`nlpcloud.io`)
                - traefik.http.middlewares.upload-limit.buffering.maxRequestBodyBytes=20000000
                - traefik.http.routers.backoffice.middlewares=upload-limit

Dans l’exemple ci-dessus, nous avons réhaussé la limite d’upload à 20Mo.

Mais n’oubliez pas que le fait d’uploader un gros fichier d’un coup n’est pas nécessairement la meilleure option. A la place il est plus intéressant de découper le fichier en morceaux et d’uploader chaque morceau indépendamment. J’écrirai peut-être un article sur le sujet à l’avenir.

Debugging

Il y a plusieurs options que vous pouvez activer afin de vous aider à débugger Traefik.

La première chose est d’activer le mode debug qui vous montrera des tonnes de trucs à propos de l’exécution de Traefik.

Deuxièmement, activez les logs d’accès afin de voir toutes les requêtes HTTP entrantes.

Enfin, Traefik fournit un dashboard natif très sympa qui aide à débugger votre configuration. Il est très utile car il est parfois ardu de comprendre pourquoi les choses ne fonctionnent pas.

Afin d’activer les fonctionnalités ci-dessus, faites ce qui suit :

version: '3.8'
services:
    traefik:
        image: traefik:v2.4
        ports:
            - "80:80"
        command:
            - --providers.docker.swarmmode
            - --log.level=DEBUG
            - --accesslog
            - --api.dashboard=true
        volumes:
            - /var/run/docker.sock:/var/run/docker.sock:ro
        deploy:
            placement:
                constraints:
                    - node.role == manager
            labels:
                - traefik.http.routers.dashboard.rule=Host(`dashboard.nlpcloud.io`)
                - traefik.http.routers.dashboard.service=api@internal
                - traefik.http.middlewares.auth.basicauth.users=<your basic auth user>.<your basic auth hashed password>.
                - traefik.http.routers.dashboard.middlewares=auth

Dans cet example, nous avons activé le debugging, les logs d’accès, et le dashboard qui est accessible à l’adresse http://dashboard.nlpcloud.io via une authentification basic auth.

Conclusion

Comme vous pouvez le voir, Traefik est parfaitement intégré à votre configuration Docker Compose. Si vous souhaitez changer la config d’un service, ou ajouter ou supprimer un service, il suffit de modifier votre docker-compose.yml, et de redéployer votre cluster Docker Swarm. Les nouveaux changement seront pris en compte, et les services qui n’ont pas été modifiés n’ont même pas besoin d’être redémarrés, ce qui est super question haute disponibilité.

Je vais continuer d’écrire quelques articles à propos de la stack que nous utilisons derrière NLP Cloud. Je pense que le prochain sera à propos de notre frontend et de comment nous utilisons HTMX au lieu de gros frameworks javascript.

Si vous avez des questions, n’hésitez pas !

Also available in English