Projet de Fin d'Études : Partie 2 - Configurer Traefik v2 avec une API Node.js sous Docker

Nous avons désormais une configuration fonctionnelle avec Node.js et Docker, nous pouvons maintenant configurer le serveur de production.

Je voulais qu’il soit facile à utiliser pour les déploiements, et le plus autonome et sécurisé possible avec la génération automatique de certificats SSL et du load balancing.

L’API utilise MongoDB, et tout fonctionne dans des conteneurs docker. J’ai choisi Traefik comme reverse proxy car il fournit un support natif pour le reverse proxying des conteneurs docker, il fonctionne bien avec letencrypt pour obtenir des certificats automatiquement, et il supporte le load balancing sur les conteneurs docker. Et également parce que je voulais essayer une alternative NGINX pour satisfaire ma curiosité.

Cependant, s’il y a BEAUCOUP d’informations en ligne sur la version 1.7 de Traefik, il n’y en a pas tant pour la version 2, et cette v2 comporte de nombreuses modifications importantes. Et à l’heure actuelle, la documentation officielle n’est pas facile à comprendre et de nombreuses fonctionnalités ne sont pas documentées.

Mise en place de Traefik dans Docker

La configuration finale ressemblera à cela :

Tous les conteneurs des dockers.

Tous les conteneurs des dockers.

Explication rapide du fonctionnement de Traefik

Traefik traite les requêtes de cette manière :

La chaîne de Traefik.

La chaîne de Traefik.

Voici le routeur utilisé dans cette chaîne (notez la partie rule), avec sa configuration TLS (méthode letencrypt http) et le middleware utilisé (basicauth).

Le routeur du tableau de bord de Traefik.

Le routeur du tableau de bord de Traefik.

Cela correspond à : “Pour une requête avec host=traefik.domain.com venant du port 443, nous passons par le routeur traefik-secure@docker (parce que l’hôte satisfait à la règle du routeur), puis par tout middleware utilisé dans ce routeur (ici un basicauth) et enfin, jusqu’au service (ici api@internal fait référence à l’api interne de traefik, qui est le tableau de bord web Traefik)

Structure des fichiers

J’aime utiliser Docker-Compose pour gérer mes conteneurs persistants (conteneurs que je veux pouvoir redémarrer facilement).

Pour commencer avec une architecture claire, notre répertoire traefik devrait ressembler à ceci :

traefik/
├── acme.json
├── config
│   └── config.yml
├── docker-compose.yml
└── traefik.yml

Explication :

  • acme.json stockera tous les certificats SSL obtenus par Traefik, nous le monterons en volume pour les conserver lors des redémarrages des conteneurs.
  • Le répertoire config/ contient nos fichiers de configuration dynamiques (où nous définissons manuellement certaines routes et règles “à la NGINX”). Traefik surveille ce répertoire et intègre automatiquement toutes les règles créées dans les fichiers qu’il contient.
  • docker-compose.yml sera utilisé pour gérer le conteneur Traefik.
  • traefik.yml est le fichier de configuration statique, lu une fois au démarrage de Traefik.

Configuration de Traefik

acme.json devrait être vide pour le premier départ.

Commençons par traefik.yml:

# Voici le tableau de bord de traefik, accessible via traefik.domain.com
api:
  dashboard: true
  debug: true

# Seulement avec les ports habituels :80 et :443, mais traefik v2 peut aussi prendre d'autres ports comme :8080 si vous le souhaitez !
entryPoints:
  http:
    address: ":80"
  https:
    address: ":443"

# Définition de l'endroit où nos services et middlewares peuvent être trouvés
providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
  file:
    directory: /config
    watch: true

log:
  level: "ERROR"
  filePath: "log/error.log"
  format: "json"

accessLog:
  filePath: "log/access.log"
  format: "json"

# Configuration pour le cert resolver Letsencrypt
certificatesResolvers:
  http:
    acme:
      email: yourmail@domain.com
      storage: acme.json
      httpChallenge:
        entryPoint: http

Maintenant, notre fichier de configuration dynamique config/config.yml :

http:

  # Création de nos middlewares
  middlewares:
  	 # Lorsqu'il est utilisé, il redirige le client vers le point d'entrée https
    https-redirect:
      redirectScheme:
        scheme: https
	 
	 # Applique certains en-têtes lorsqu'il est utilisé
    default-headers:
      headers:
        frameDeny: true
        sslRedirect: true
        browserXssFilter: true
        contentTypeNosniff: true
        forceSTSHeader: true
        stsIncludeSubdomains: true
        stsPreload: true

    # Lorsqu'il est utilisé, applique le middleware default-headers
    secured:
      chain:
        middlewares:
        - default-headers

  # Il s'agit d'une redirection globale http -> https. Avec ceci, vous pouvez seulement définir un routeur https pour vos services, ceci redirigera les utilisateurs venant de http vers votre routeur https.
  # Si vous n'en avez pas besoin, supprimez tout ce qui se trouve sous cette ligne
  routers:
    https-redirouter:
      rule: HostRegexp(`{any:.*}`)
      middlewares: [https-redirect]
      service: dummy

  services:
    dummy:
      loadBalancer:
        servers:
          - url: localhost

Et enfin, le docker-compose.yml:

version: '3.5'

services:
  traefik:
    image: traefik:v2.0
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - web
    ports:
      - 80:80
      - 443:443
    volumes:
    	# Synchronisation de l'heure avec l'hôte
      - /etc/localtime:/etc/localtime:ro
       # Accès au socket docker pour contrôler les autres conteneurs
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik.yml:/traefik.yml:ro
      - ./acme.json:/acme.json
      - ./config:/config:ro
      - ./log:/log
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.traefik.entrypoints=http"
      - "traefik.http.routers.traefik.rule=Host(`traefik.domain.com`)"
      - "traefik.http.middlewares.traefik-auth.basicauth.users=admin:$apr1$Z3U0xxxxxxxxxxxxxxxxxRz1"
      - "traefik.http.middlewares.traefik-https-redirect.redirectscheme.scheme=https"
      - "traefik.http.routers.traefik.middlewares=traefik-https-redirect"
      - "traefik.http.routers.traefik-secure.entrypoints=https"
      - "traefik.http.routers.traefik-secure.rule=Host(`traefik.domain.com`)"
      - "traefik.http.routers.traefik-secure.middlewares=traefik-auth"
      - "traefik.http.routers.traefik-secure.tls=true"
      - "traefik.http.routers.traefik-secure.tls.certresolver=http"
      - "traefik.http.routers.traefik-secure.service=api@internal"

# Mettre traefik dans un réseau docker externe. 
# Les autres conteneurs qui doivent être accessibles seront déclarés dans le même réseau
networks:
  web:
    external: true

⚠️ Partager la socket docker avec un conteneur docker peut être dangereux, car cela signifie que ce conteneur peut avoir un accès root à la machine hôte. L’utilisation d’un proxy sur la socket est recommandé.

Notez la section labels dans ce docker-compose. Traefik lira les labels de chaque conteneur dans son réseau (web), et appliquera les instructions comme configuration dynamique.

Ici, nous définissons donc 2 routeurs : traefik pour http et traefik-secure pour https.

  • traefik utilise un middleware pour la redirection vers https ( également défini ici, c’est traefik-https-redirect).
  • traefik-secure utilise un middleware de basic-auth avec une combinaison user:password.

Cette configuration est celle que nous avons vue plus tôt

Démarrage du conteneur Traefik

Une fois la configuration terminée, il suffit de faire :

docker-compose up -d

Les données de log peuvent être trouvées dans le répertoire log/.

Reverse-Proxying vers d’autres conteneurs

Attacher la configuration traefik à chaque conteneur

Maintenant que Traefik est opérationnel, nous pouvons l’utiliser pour servir d’autres conteneurs, comme notre API.

J’utilise le même fichier docker-compose dont j’ai parlé dans la première partie de cette série, vous pouvez le trouver ici.

Nous devons le connecter au réseau web, et appliquer la configuration de traefik appropriée dans les labels pour chaque conteneur. Nous devons éviter que le conteneur MongoDB soit accessible à traefik, il sera donc interdit.

api-ecobol/docker-compose.yml

version: '3'
services:
  api-ecobol:
    env_file:
      - .env
    restart: on-failure
    build: .
    links:
      - mongo
    depends_on:
      - mongo
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.api-ecobol.entrypoints=http"
      - "traefik.http.routers.api-ecobol.rule=Host(`api.example.com`)"
      - "traefik.http.routers.api-ecobol.middlewares=https-redirect@file"
      - "traefik.http.routers.api-ecobol-secure.middlewares=secured@file"
      - "traefik.http.routers.api-ecobol-secure.entrypoints=https"
      - "traefik.http.routers.api-ecobol-secure.rule=Host(`api.example.com`)"
      - "traefik.http.routers.api-ecobol-secure.tls=true"
      - "traefik.http.routers.api-ecobol-secure.tls.certresolver=http"
      - "traefik.http.routers.api-ecobol-secure.service=api-ecobol"
      - "traefik.http.services.api-ecobol.loadbalancer.server.port=3000"
      - "traefik.docker.network=web"
    ports:
      - '127.0.0.1:3001:3000'
    networks:
      - web
  mongo:
    image: 'mongo:4'
    volumes:
      - './data:/data/db'
    restart: on-failure
    labels:
      - "traefik.enable=false"
    ports:
      - '127.0.0.1:27018:27017'
    networks:
      - web
networks:
  web:
    external: true

Note : Si vous avez gardé la redirection http -> https globale dans config/config.yml, vous pouvez supprimer ces lignes (le routeur http) des labels :

- "traefik.http.routers.api-ecobol.entrypoints=http"
- "traefik.http.routers.api-ecobol.rule=Host(`api.example.com`)"
- "traefik.http.routers.api-ecobol.middlewares=https-redirect@file"

Il y a deux nouvelles choses ici : la ligne loadbalancer.server.port, qui indique à traefik sur quel port l’application tourne à l’intérieur du conteneur, et la ligne traefik.enable=false, pour empêcher traefik d’exposer le conteneur.

Suite à mon dernier article, cette configuration peut être spécifiée uniquement pour la production dans docker-compose.prod.yml.

Comme toujours :

docker-compose up -f docker-compose.yml -f docker-compose.prod.yml -d

Scaling des conteneurs : Load Balancing

Maintenant, nous pouvons essayer le load balancing. Avec Docker-Compose, la commande pour scale un conteneur est :

docker-compose scale api-ecobol=3

Cela créera un total de 3 conteneurs, et Traefik les détectera automatiquement, et équilibrera le trafic entrant entre chacun d’eux, tout en surveillant leur statut. Vous pouvez observer cela dans le tableau de bord à l’adresse traefik.domain.com.