Traefik, Docker et dnsmasq pour simplifier la mise en réseau des conteneurs

Traefik, Docker et dnsmasq pour simplifier la mise en réseau des conteneurs

Vous appréciez notre travail......nous recrutons !

Ne ratez pas nos articles sur l'open source, le big data et les systèmes distribués, fréquence faible d’un email tous les deux mois.

Les bonnes aventures technologiques commencent par une certaine frustration, un besoin ou une exigence. C’est l’histoire de la façon dont j’ai simplifié la gestion et l’accès de mes applications Web locales avec l’aide de Traefik et dnsmasq. Le raisonnement s’applique tout aussi bien pour un serveur de production utilisant Docker.

Mon environnement de développement est composé d’un nombre croissant d’applications web hébergées sur ma machine. Ces applications incluent plusieurs sites Web, outils, éditeurs, registres, … Elles utilisent des bases de données, des API REST ou des backends plus complexes. Prenons l’exemple de Supabase, le fichier Docker Compose comprend le Studio, la passerelle API Kong, le service d’authentification, le service REST, le service temps réel, le service de stockage, le méta service et la base de données PostgreSQL.

Le résultat est un nombre croissant de conteneurs démarrés sur ma machine, accessibles en localhost sur différents ports. Certains d’entre eux utilisent les ports par défaut et ne peuvent pas fonctionner en parallèle sans entrer en conflit. Par exemple, les ports 3000 et 8000 sont communs à pas mal de containers présents sur ma machine. Pour contourner le problème, certains conteneurs utilisent des ports personnalisés que j’oublie souvent.

Une meilleur solution consiste à créer des noms de domaine locaux faciles à mémoriser et à utiliser un proxy Web pour acheminer les requêtes vers le bon conteneur. Traefik aide au routage et à la découverte de ces services et dnsmasq fournit un domaine de premier niveau personnalisé (pseudo top-level domain, pseudo-TLD) pour y accéder.

Une autre utilisation de Traefik est un serveur de production utilisant plusieurs fichiers Docker Compose pour divers sites Web et applications Web. Les conteneurs communiquent à l’intérieur d’un réseau interne et sont exposés via un service proxy, dans notre cas implémenté avec Caddy.

Description du problème

Parmi beaucoup d’autres, prenons 3 applications Web exécutées localement. Toutes sont gérées avec Docker Compose :

  • Site Web Adaltas, 1 conteneur, site Web statique basé sur Gatsby
  • Site Web Alliage, 10 conteneurs, frontend Next.js, backend Node.js et Supabase
  • Penpot, 6 conteneurs, Penport frontend, services backend plus Inbucket pour les tests de messagerie (ajout personnel)

Par défaut, ces conteneurs exposent les ports suivants sur localhost :

  • Adaltas
    • 8000 Serveur Gatsby en mode développement
    • 9000 Service Gatsby pour servir un site Web de build
  • Alliage
    • 3000 Site Web Next.js à la fois en mode développement et en mode build
    • 3001 API personnalisée Node.js
    • 3000 Supabase Studio
    • 5555 Supabase Méta
    • 8000 Kong HTTP
    • 8443 Kong HTTPS
    • 5432 PostgreSQL
    • 2500 Inbucket, Serveur SMTP entrant
    • 9000 Inbucket, Interface Web
    • 1100 Inbucket, Serveur POP3 entrant
  • Penpot
    • 2500 Inbucket, Serveur SMTP entrant
    • 9000 Inbucket, Interface Web
    • 1100 Inbucket, Serveur POP3 entrant
    • 9001 Frontend Penpot

Notez qu’en fonction de votre environnement et de vos besoins, certains ports peuvent être restreints tandis que d’autres peuvent être accessibles.

Comme vous pouvez le voir, de nombreux ports entrent en collision les uns avec les autres. Ce ne sont pas seulement les 2 instances d’Inbucket qui fonctionnent en parallèle. Par exemple, le port 8000 est utilisé à la fois par Gatsby et Kong. Il s’agit d’un port par défaut commun à plusieurs applications. Il en va de même pour les ports 3000, 8080, 8443, …

Une solution consiste à attribuer des ports distincts pour chaque service. Cependant, cette approche n’est pas évolutive. Très rapidement, j’oublie à quel port chaque service est affecté.

Comportement attendu

Une meilleure solution consiste à utiliser un reverse-proxy avec des noms d’hôte faciles à retenir. Voici ce que nous attendons :

  • Adaltas
    • www.adaltas.local Serveur Gatsby en mode développement
    • build.adaltas.local Service Gatsby pour servir un site Web de construction
  • Alliage
    • www.alliage.local Site Web Next.js à la fois en mode développement et construction
    • api.alliage.local API personnalisée Node.js
    • studio.alliage.local Supabase Studio
    • meta.alliage.local Supabase Méta
    • kong.alliage.local Kong HTTP
    • kong.alliage.local Kong HTTPS
    • sql.alliage.local PostgreSQL
    • smtp.alliage.local Serveur SMTP entrant
    • mail.alliage.local Interface Web Inbucket
    • pop3.alliage.local Serveur POP3 d’entrée
  • Penpot
    • www.penpot.local Interface Penpot
    • smtp.penpot.local Serveur SMTP entrant
    • mail.penpot.local Interface Web Inbucket
    • pop3.penpot.local Serveur POP3 d’entrée

Dans un cadre traditionnel, le proxy inverse est configuré avec un ou plusieurs fichiers de configuration avec toutes les informations de routage. Cependant, une configuration centrale n’est pas si pratique. Il est préférable que chaque service déclare le nom d’hôte qu’il résout.

Enregistrement automatique du routage

Tous mes services web sont gérés avec Docker Compose. Idéalement, je m’attends à ce que des informations soient présentes dans le fichier Docker Compose. Traefik est natif du cloud dans le sens où il se configure avec un workflow cloud-native. L’application fournit quelques instructions présentes dans son fichier docker-compose.yml et les containers sont automatiquement exposés.

La façon dont Traefik fonctionne avec Docker, il se branche sur la prise Docker, détecte de nouveaux services et crée les itinéraires pour vous.

Démarrage de Traefik

Démarrer Traefik dans Docker est simple (ne dites jamais facile). Le fichier docker-compose.yml est :

version: '3'
services:
  reverse-proxy:
    # The official v2 Traefik docker image
    image: traefik:v2.9
    # Enables the web UI and tells Traefik to listen to Docker
    command: --api.insecure=true --providers.docker
    ports:
      # The HTTP port
      - "80:80"
      # The Web UI (enabled by --api.insecure=true)
      - "8080:8080"
    volumes:
      # So that Traefik can listen to the Docker events
      - /var/run/docker.sock:/var/run/docker.sock

Enregistrement de nouveaux services

Considérons un service supplémentaire. Le site Web d’Adaltas est un conteneur unique basé sur Gatsby. En mode développement, il démarre un serveur Web sur le port 8000. Je m’attends à ce qu’il soit accessible avec le nom d’hôte www.adaltas.local sur le port 80.

En référence au tutoriel Traefik sur Docker, l’intégration se fait avec la propriété traefik.http.routers..rule présente dans le champ labels du service docker. Il définit le nom d’hôte sous lequel notre site Web est accessible sur le port 80. Il est défini sur www.adaltas.localhost car le TLD .localhost se résout localement par défaut. Puisque je préfère utiliser le domaine .local, nous avons défini le domaine sur www.adaltas.local plus tard en utilisant dnsmasq. Le trafic est ensuite acheminé vers l’IP du conteneur sur le port 8000. Le port du conteneur est obtenu par Traefik à partir du champ ports de Docker Compose.

version: '3'
services:
  www:
    container_name: adaltas-www
    ...
    labels:
    - "traefik.http.routers.adaltas-www.rule=Host(`www.adaltas.localhost`)"
    ports:
    - "8000:8000"

Cela fonctionne lorsque les conteneurs Traefik et Adaltas sont définis dans le même fichier Docker Compose. Lancez docker-compose up et vous pouvez :

  • http://localhost:8080 : Accéder à l’interface utilisateur Web Traefik
  • http://localhost:8080/api/rawdata : Accéder aux données brutes de l’API de Traefik
  • http://www.adaltas.localhost : Accéder au site Adaltas en mode développement
  • http://localhost:8080 : Identique à http://www.adaltas.localhost

Il y a 3 limitations que nous allons résoudre :

  • Réseautage interne
    Cela ne fonctionne que parce que tous les services sont déclarés dans le même fichier Docker Compose. Avec des fichiers Docker Compose séparés, un réseau interne doit être utilisé pour communiquer entre le conteneur Traefic et les conteneurs ciblés.
  • Nom de domaine
    Je souhaite utiliser un pseudo-TLD, par exemple, www.adaltas.local au lieu de www.adaltas.localhost. Le TLD .local ne se résout pas encore localement, un serveur DNS local doit être configuré.
  • Port label
    Le port d’Adaltas est défini dans le fichier Docker Compose. Ainsi, il est exposé sur la machine hôte et entre en collision avec d’autres services. La redirection de port doit être désactivée et Traefik doit être informé du port par un autre mécanisme que la présence du champ ports.

Réseau interne

Lorsqu’ils sont définis sur des fichiers séparés, les conteneurs ne peuvent communiquer les uns avec les autres. Chaque fichier Docker Compose possède un réseau isolé. Le service ciblé est visible dans l’interface utilisateur Traefik. Cependant, la requête ne parvient pas à être acheminée.

Les conteneurs doivent partager un réseau commun pour communiquer. Lorsque le conteneur Traefik est démarré, un réseau traefik_default est créé, voir la liste des réseaux docker avec docker network list. Au lieu de créer un nouveau réseau, réutilisons-le. Enrichissez le fichier Docker Compose du conteneur ciblé, le site Adaltas dans notre cas, avec le champ network :

version: '3'
services:
 www:
   container_name: adaltas-www
   # ...
networks: default:   name: traefik_default

Après avoir démarré les 2 configurations Docker Compose avec docker-compose up, les conteneurs Traefik et Website commencent à communiquer.

Nom de domaine

Il est temps de s’attaquer au FQDN (nom de domaine complet) de nos services. Le TLD actuellement utilisé, .localhost, convient parfaitement. Il fonctionne par défaut et il est officiellement réservé à cet usage. Cependant, je souhaite utiliser mes propres domaines de premier niveau (pseudo-TLD), nous utiliserons .local dans cet exemple.

Attention, l’utilisation d’un pseudo-TDL n’est pas recommandée. Le TLD .local est utilisé par les réseaux DNS multicast et zero-trust. En pratique, je n’ai rencontré aucun problème. Pour limiter les risques de conflits, la RFC 2606 réserve les noms TLD suivants : .test, .example, .invalid, .localhost.

Un serveur DNS local est utilisé pour résoudre les adresses *.local. J’ai eu une certaine expérience avec Bind dans le passé. Une option plus simple et plus légère est l’utilisation de dnsmasq. Les instructions ci-dessous couvrent l’installation sur MacOS et Ubuntu Desktop. Dans les deux cas, dnsmaq est installé et configuré pour ne pas interférer avec les paramètres DNS actuels.

Instructions MacOS :

# Install dnsmasq
brew install dnsmasq
# Setup the `.local` pseudo-TLD
mkdir -pv $(brew --prefix)/etc/
echo 'address=/.local/127.0.0.1' >> $(brew --prefix)/etc/dnsmasq.conf
# Start dnsmasq
sudo brew services start dnsmasq
# Add to resolver
sudo mkdir -v /etc/resolver
sudo bash -c 'echo "nameserver 127.0.0.1" > /etc/resolver/test'
# Test
scutil --dns

Instructions Linux avec NetworkManager (par exemple Ubuntu Desktop) :

# Disable systemd-resolved
systemctl disable systemd-resolved
systemctl stop systemd-resolved
unlink /etc/resolv.conf
# Activate the dnsmasq plugin
cat <<CONF | sudo tee /etc/NetworkManager/conf.d/00-use-dnsmasq.conf
[main]
dns=dnsmasq
CONF
# Setup the public DNS and the `.local` pseudo-TLD
cat <<CONF | sudo tee /etc/NetworkManager/dnsmasq.d/00-dns-public.conf
server=8.8.8.8
CONF
cat <<CONF | sudo tee /etc/NetworkManager/dnsmasq.d/00-address-local.conf
address=/.local/127.0.0.1
CONF
systemctl restart NetworkManager

Utilisez dig pour valider que n’importe quel FQDN utilisant notre pseudo-TLD se résout sur la machine locale :

dig test.local +short

Port label

Avec l’introduction d’un reverse proxy comme Traefik, exposer le port des conteneurs sur la machine hôte n’est plus nécessaire, éliminant ainsi le risque de collision entre le port exposé et ceux d’autres services.

Un label Docker est déjà présente pour définir le nom d’hôte du service du site Web. Traefik est livré avec de nombreux label complémentaires. La propriété traefik.http.services.{service_name}.loadbalancer.server.port indique à Traefik d’utiliser un port spécifique pour se connecter à un conteneur.

Le fichier Docker Compose final ressemble à ceci :

version: '3'
services:
  www:
    container_name: adaltas-www
    image: node:18
    volumes:
      - .:/app
    user: node
    working_dir: /app
    command: bash -c "yarn install && yarn run develop"
    labels:
    - "traefik.http.routers.adaltas-www.rule=Host(`www.adaltas.local`)"
    - "traefik.http.services.adaltas-www.loadbalancer.server.port=8000"
networks:
 default:
   name: traefik_default

Conclusion

Avec Traefik, j’aime l’idée que les services de mes conteneurs soient publiés automatiquement dans une philosophie cloud-native. Il m’apporte confort et simplicité. De plus, dnsmasq s’est avéré être bien documenté et rapide à s’adapter à mes diverses exigences.

Partagez cet article

Canada - Maroc - France

Nous sommes une équipe passionnée par l'Open Source, le Big Data et les technologies associées telles que le Cloud, le Data Engineering, la Data Science le DevOps…

Nous fournissons à nos clients un savoir faire reconnu sur la manière d'utiliser les technologies pour convertir leurs cas d'usage en projets exploités en production, sur la façon de réduire les coûts et d'accélérer les livraisons de nouvelles fonctionnalités.

Si vous appréciez la qualité de nos publications, nous vous invitons à nous contacter en vue de coopérer ensemble.

Support Ukrain