Traefik, Docker et dnsmasq pour simplifier la mise en réseau des conteneurs
By WORMS David
17 nov. 2022
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éveloppement9000
Service Gatsby pour servir un site Web de build
- Alliage
3000
Site Web Next.js à la fois en mode développement et en mode build3001
API personnalisée Node.js3000
Supabase Studio5555
Supabase Méta8000
Kong HTTP8443
Kong HTTPS5432
PostgreSQL2500
Inbucket, Serveur SMTP entrant9000
Inbucket, Interface Web1100
Inbucket, Serveur POP3 entrant
- Penpot
2500
Inbucket, Serveur SMTP entrant9000
Inbucket, Interface Web1100
Inbucket, Serveur POP3 entrant9001
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éveloppementbuild.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 constructionapi.alliage.local
API personnalisée Node.jsstudio.alliage.local
Supabase Studiometa.alliage.local
Supabase Métakong.alliage.local
Kong HTTPkong.alliage.local
Kong HTTPSsql.alliage.local
PostgreSQLsmtp.alliage.local
Serveur SMTP entrantmail.alliage.local
Interface Web Inbucketpop3.alliage.local
Serveur POP3 d’entrée
- Penpot
www.penpot.local
Interface Penpotsmtp.penpot.local
Serveur SMTP entrantmail.penpot.local
Interface Web Inbucketpop3.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.
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 Traefikhttp://localhost:8080/api/rawdata
: Accéder aux données brutes de l’API de Traefikhttp://www.adaltas.localhost
: Accéder au site Adaltas en mode développementhttp://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 dewww.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 champports
.
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.