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

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 à 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 auto-hébergées sur mon portable. 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 inclut 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 le Base de données PostgreSQL.

Le résultat est un nombre croissant de conteneurs lancés sur mon ordinateur portable, accessibles sur localhost sur divers ports. Certains d’entre eux utilisent les ports par défaut et ne peuvent pas fonctionner en parallèle pour éviter les conflits. Par exemple, le 3000 et 8000 les ports sont communs à beaucoup de conteneurs présents sur ma machine. Pour contourner le problème, certains conteneurs utilisent des ports personnalisés que j’oublie souvent.

La 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-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 Caddie.

Description du problème

Parmi plusieurs, prenons 3 applications Web exécutées localement. Tous sont gérés avec Docker Compose :

  • Site Web d’Adalta1 conteneur, site Web statique basé sur Gatsby
  • Site internet Alliage10 conteneurs, frontend Next.js, backend Node.js et Supabase
  • Penpot6 conteneurs, Penpot frontend, services backend plus Inbucket pour les tests de messagerie (ajout personnel)

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

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

Notez qu’en fonction de votre environnement et de vos envies, 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, port 8000 est utilisé à la fois par Gatsby et Kong. Il s’agit d’un port par défaut commun à plusieurs applications. Idem pour les ports 3000, 8080, 8443

Une solution consiste à attribuer des ports distincts pour chaque service. Cependant, cette approche n’est pas évolutive. Bientôt, j’oublie à quel port chaque service est affecté.

Comportement attendu

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

  • Adalta
    • 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 en mode construction
    • api.alliage.local API personnalisée Node.js
    • studio.alliage.local Studio SupaBase
    • meta.alliage.local Supabase Méta
    • kong.alliage.local Kong HTTP
    • kong.alliage.local Kong HTTPS
    • sql.alliage.local PostgreSQLName
    • smtp.alliage.local Serveur SMTP entrant
    • mail.alliage.local Interface Web d’entrée
    • pop3.alliage.local Serveur POP3 entrant
  • Penpot
    • www.penpot.local Frontend Penpot
    • smtp.penpot.local Serveur SMTP entrant
    • mail.penpot.local Interface Web d’entrée
    • pop3.penpot.local Serveur POP3 entrant

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 à l’aide de flux de travail natifs du cloud. L’application fournit des instructions présentes dans son docker-compose.yml fichier et les conteneurs sont automatiquement exposés.

Le chemin Traefik fonctionne avec Dockeril se branche sur le socket Docker, détecte les nouveaux services et crée les routes pour vous.

Démarrer Traefik

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

version: '3'
services:
  reverse-proxy:
    
    image: traefik:v2.9
    
    command: --api.insecure=true --providers.docker
    ports:
      
      - "80:80"
      
      - "8080:8080"
    volumes:
      
      - /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 bâbord 80.

Suivant le Traefik débute avec Dockerl’intégration se fait avec la propriété traefik.http.routers.router_name.rule présent dans le labels domaine du service docker. Il définit le nom d’hôte sous lequel notre site Web est accessible sur le port 80. Il est fixé à www.adaltas.localhost parce que le .localhost TLD se résout localement par défaut. Comme je préfère utiliser le .local domaine, nous définissons 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 Docker Compose’s ports champ.

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 services Traefik et Adaltas sont définis dans le même fichier de composition Docker. Cuisson docker-compose up et tu peux:

  • http://localhost:8080: Accéder à l’interface utilisateur Web Traefik
  • http://localhost:8080/api/rawdata: Accédez 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: Pareil que http://www.adaltas.localhost

Il y a 3 limitations auxquelles nous devons faire face :

  • 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 domaine de premier niveau (TLD), par exemple, www.adaltas.local à la place de www.adaltas.localhost. La .local Le TLD ne se résout pas encore localement, un serveur DNS local doit être configuré.
  • Étiquette de port
    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 avec un autre mécanisme que le ports champ.

Réseautage interne

Lorsqu’il est défini sur des fichiers séparés, le conteneur ne peut pas communiquer. Chaque fichier Docker Compose génère un réseau dédié. 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 traefik_default réseau est créé, voir docker network list. Au lieu de créer un nouveau réseau, réutilisons-le. Enrichir le fichier Docker Compose du conteneur ciblé, le site Adaltas dans notre cas, avec le network champ:

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 uples conteneurs Traefik et Website commencent à communiquer.

Nom de domaine

Il est temps de s’attaquer au FQDN de nos services. Le TLD actuellement utilisé, .localhost, est parfaitement bien. 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 nom), nous utiliserons .local dans cet exemple.

Avis de non-responsabilité, l’utilisation d’un nom de pseudo-TLD n’est pas recommandée. La .local TLD est utilisé par le DNS multicast / réseau sans configuration. En pratique, je n’ai rencontré aucun problème. Pour atténuer les risques de conflits, RFC2606 réserve les noms TLD suivants : .test, .example, .invalid, .localhost.

Un serveur DNS local est utilisé pour résoudre le *.local adresses. J’ai eu une certaine expérience avec Lier autrefois. 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 :


brew install dnsmasq

mkdir -pv $(brew --prefix)/etc/
echo 'address=/.local/127.0.0.1' >> $(brew --prefix)/etc/dnsmasq.conf

sudo brew services start dnsmasq

sudo mkdir -v /etc/resolver
sudo bash -c 'echo "nameserver 127.0.0.1" > /etc/resolver/test'

scutil --dns

Instructions Linux avec NetworkManager (par exemple Ubuntu Desktop):


systemctl disable systemd-resolved
systemctl stop systemd-resolved
unlink /etc/resolv.conf

cat <<CONF | sudo tee /etc/NetworkManager/conf.d/00-use-dnsmasq.conf
[main]
dns=dnsmasq
CONF

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

Utilisation dig pour valider que tout FQDN utilisant notre pseudo-TLD se résout sur la machine locale :

Étiquette de port

Avec l’introduction d’un proxy inverse comme Traefik, exposer le port de conteneur 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.

Une étiquette est déjà présente pour définir le nom d’hôte du service du site Web. Traefik est livré avec beaucoup de étiquettes complémentaires. La traefik.http.services.service_name.loadbalancer.server.port La propriété 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 mes services de conteneurs s’inscrivent automatiquement dans une philosophie cloud-native. Il m’a apporté confort et simplicité. De plus, dnsmasq s’est avéré être bien documenté et rapide à s’adapter à mes diverses exigences.

Leave a Reply