Table des matières

Traefik, aiguilleur du ciel de vos flux "dockerisés"

Problématique : où tourne ma prod ?!

A Geco-iT, nous avons une partie de notre production qui tourne sous docker swarm. Nous passerons ici volontairement la phase de présentation de l'outil docker et son mode swarm car si vous êtes en train de lire cet article c'est que vous avez surement une idée de quoi on parle :). Notre cluster se compose de trois nodes, et nous avons plusieurs services, dont certains qui nécessite plusieurs conteneurs frontaux. Docker swarm fait bien sont travail en répartissant ces services sur nos 3 nodes. En cas de coupure d'un node, les services présents sur le node impacté sont redémarrés sur un des deux nodes restants. La problématique se dessine peu à peu quand on commence à vouloir exposer ces services sur l’extérieur…

Avant docker, nous avons opté pour HAproxy qui est un load balancer, ce dernier nous permettais de rediriger un flux entrant HTTP sur notre ip publique ( frontend ) vers un ou plusieurs serveurs de notre infrastructure ( backends ) en y ajoutant en plus la couche TLS ( redirection du HTTP en HTTPS ). C'est une solution qui marche très bien dans un environnement “statique”. Il faut déclarer dans Haproxy sur quel port écouter, et pour chaque service, vers quel backend rediriger la connexion. Pour le certificat TLS, on utilise par dessus certbot pour générer des certificats avec Let's Encrypt.

On se rend vite compte qu'ajouter un service, ou le déplacer peut vite virer au casse tête. Il faut update la configuration du frontend pour déclarer le nouveau service, ajouter la configuration du backend vers qui il doit être redirigé et s'occuper de certbot pour la génération du certificat… Et avec docker swarm, les services ne font que se déplacer sur les différents nodes du cluster, ce qui rend l'environnement dynamique !

Haproxy étant un très bon loadbalancer, il intégré la possibilité d'avoir des backends dynamiques via l’interrogation d'un serveur DNS, on peut utiliser cette fonctionnalité pour utiliser HAproxy au sein d'un cluster swarm comme décrit ici Mais vous allez voir que Traefik apporte une réponse bien plus pratique et bien plus en phase avec la méthode docker !

public:geco_haproxy.png

Traefik : la réponse au casse tête

Traefik est, comme Haproxy un reverse proxy / loadbalancer avec possibilité d'ajout TLS. Il est écrit en GO, publié par la société Containous fondée par Emile Vauge et c'est français ! La véritable force de traefik c'est que sa configuration est dynamique, comprenez par la que dans notre cas, il va se connecter au socket docker, et récupérer toutes les informations sur les containers qui tournent en temps réel de façon à pouvoir router les requêtes sur ces derniers de manière automatique ! Traefik propose la liste suivante de fonctionnalités :

Pour capter sa configuration, il peut se connecter aux “providers” suivant :

Traefikons notre swarm !

Pour notre petit exemple nous avons un cluster docker swarm 3 nodes, 1 manager et 2 worker.

ID                            HOSTNAME         STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
kkdqvkn6n2x6m7gpdacnq57cb *   docker-manager   Ready     Active         Leader           20.10.6
q79hv1twzr2vvd8l4ngab6l8k     docker-worker1   Ready     Active                          20.10.6
tmuz3rsh3x7vlilp3k07mxgz6     docker-worker2   Ready     Active                          20.10.6

Nous allons déployer une petite application “whoami” pour nos tests :

root@docker-worker1:/home/admin# docker run -p 80:80 containous/whoami 
Unable to find image 'containous/whoami:latest' locally
latest: Pulling from containous/whoami
Digest: sha256:7d6a3c8f91470a23ef380320609ee6e69ac68d20bc804f3a1c6065fb56cfa34e
Status: Downloaded newer image for containous/whoami:latest
Starting up on port 80...

On voit que le container tourne, je le lance sur le node docker-worker1 directement, voici le résultat si on essaye d’accéder sur le port 80 qui est directement exposé :

Le but de l'exercice va être de :

Pour comprendre traefik ,il suffit de comprendre les différents “objets” autour desquels ils fonctionne :

Certains objets sont définis au démarrage et donc son “intouchables” une fois que traefik tourne, ils composent la configuration dite “statique”. Et à l'inverse, certains éléments sont dit “dynamiques” car ils sont apporté par les providers et sont donc définis en permanence une fois traefik lancé. Il est important de comprendre qu'un élément dynamique ne peut pas être configuré directement dans la config de traefik, il doit forcément être apporté par un prodiver. C'est le cas pour les options TLS, d'ou l'utilisation d'un fichier séparé pour ces dernières.

Nous allons donc définir la config suivante pour traefik :

#global:
#  sendAnonymousUsage: false #=> Vous pouvez choisir de ne pas envoyer les statistiques anonymes, mais pour supporter le projet c'est mieux ;)

entryPoints:                 #=> Définition des points d'entrée...

  ping: 
    address: ":8082"         #=> Le ping est un entrypoint particulier qui sert juste à contrôler que traefik "va bien", on le défini ici sur le port 8082
 
    => ici on défini deux entrypoints,
    
  http:                      #=> http sur le 80 qui redirige vers https    
    address: ":80"
    http:    
      redirections:
        entryPoint:
          to: https
          scheme: https
  https:                     #=> et https qui écoute sur le 443 et active le TLS
    address: ":443"
    http:
      tls:
        certResolver: le     #=> on choisi notre resolver que j’appelle ici "le" pour "let's encrypt"
      middlewares:
       - tls-headers@file    #=> on ajout un middleware qui va ajouter des entêtes en rapport avec la sécurité TLS.
 
    
providers:                   #=> la déclaration de nos deux providers, la socket docker en mode swarm et un simple file yaml pour les option TLS.
  docker:
    exposedbydefault: false
    swarmMode: true
  file:
    filename: /etc/traefik/config/tls.yaml
    
certificatesresolvers:      #=> On défini les options pour let's encrypt
  le:
    acme:
      email: xxxx@geco-it.fr
      #caserver: https://acme-staging-v02.api.letsencrypt.org/directory #Uncomment to go stagging
      storage: /certificates/acme.json
      keyType: 'RSA4096'
      dnsChallenge:    # => avec une challenge de type dns qui se fera via l'API ovh
        provider: ovh
        delayBeforeCheck: 0 
 
 
api: {} #=> activation de la webui de traefik

log: {}

accesslog: {}

metrics:
  prometheus: {}
  
ping:
  entryPoint: "ping"

Voici le file déclaré dans une config docker, que l'on va remonter dans le container traefik pour les options TLS, sans entrer dans les details, on s'assure de choisir des ciphers et des algorithme bien securisés en ajoutant en plus les entetes pour forcer le HTTPS :

tls:
  options:
    default:
      curvePreferences:
        - CurveP521
        - CurveP384
      preferServerCipherSuites: true
      sniStrict: true
      minVersion: VersionTLS12
      cipherSuites:
       - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
       - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
       - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
       - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
       - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
       - TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
       - TLS_RSA_WITH_AES_128_CBC_SHA
       - TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
       - TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
       - TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
       
http:
  middlewares:
    tls-headers:                   #=> Ici le middleware tls-headers qui est aussi déclaré dans la config de l'entrypoint "https"
      headers:
        sslRedirect: true
        #sslForceHost: true
        #sslHost: 'whoami.geco-it.net'
        forceSTSHeader: true
        stsIncludeSubdomains: true
        stsPreload: true
        stsSeconds: 31536000

La stack docker de traefik :

version: '3.3'
services:
  traefik:
    image: traefik:latest
    environment:
      OVH_APPLICATION_KEY_FILE: /run/secrets/ovh-application-key
      OVH_APPLICATION_SECRET_FILE: /run/secrets/ovh-application-secret
      OVH_CONSUMER_KEY_FILE: /run/secrets/ovh-consumer-key
      OVH_ENDPOINT: ovh-eu
    ports:     #443 pour HTTPS et 80 justement pour pouvoir rediriger le HTTP vers le HTTPS
    - target: 80
      published: 80
      protocol: tcp
      mode: host    # => petite particularité ici, on passe en mode host pour s'affranchir du routage swarm et permettre à traefik de vraiment bénéficier de l'IP réelle de la machine
    - target: 443
      published: 443
      protocol: tcp
      mode: host
    volumes:
     - /var/run/docker.sock:/var/run/docker.sock:ro    #Obligatoire pour que traefik pour avoir accès aux informations docker, préférable de le faire via TCP, mais bon c'est un labo...
     - /mnt/GFS/traefik/certs:/certificates            # L'endroit ou stocker les certificats TLS générés 
    networks:
     - traefiked         #Le network qui va permettre la communication entre traefik et nos services
    secrets:
     - traefik-auth      #Le fichier qui contient le user/pass pour l'authentification que l'on veut mettre en place à la fois sur notre petit service "whoami" et sur la webUI de traefik
     - ovh-application-key        
     - ovh-application-secret      # les informations de connexion pour l'api ovh sont stocké dans des secrets docker swarm !
     - ovh-consumer-key  
    configs:
    -
      source: traefik-config-v4
      target: /etc/traefik/traefik.yaml    #La conf de traefik, stocké dans une config swarm
     -
      source: traefik-config-tls
      target: /etc/traefik/config/tls.yaml #La conf des options TLS, qu'on doit forcément apporter dans un fichier séparé car forcément "dynamique"

    deploy:
      labels:    #Le coeur de la guerre sur traefik, les labels ! C'est par ces dernières que l'on configure comment on veut accéder a nos conteneurs  
        traefik.http.services.traefik-public.loadbalancer.server.port: '8080'                   #Le port sur lequel traefik doit rediriger notre service
        traefik.http.middlewares.traefik-auth.basicauth.usersFile: /run/secrets/traefik-auth    #Ajout d'un middleware "traefik-auth" pour l'authentication a la webui de traeffik
        traefik.http.routers.traefik-public.rule: Host(`traefik.geco-it.net`)                   #Regle de routage : pour le host traefik.geco-it.net....
        traefik.http.routers.traefik-public.service: api@internal                               # ...  je dirige vers la webui de traefik
        traefik.http.routers.traefik-public.middlewares: traefik-auth                           #Association du router "traefik-public" avec le middleware "traefik-auth".
        traefik.docker.network: traefiked                                                       #Le network utilisé pour vers transiter la connexion entre traefik et les containers
        traefik.enable: 'true'                                                                  #Activation de traefik pour ce container, on active traefik sur lui-même
      placement:
        constraints:
        - node.role==manager    #Comme j'expose la socket docker via Unix, je dois faire tourner mon traefik sur un node manager, d'ou la contrainte de placement !
networks:
  traefiked:
    external: true     #Le network dedié a la communication entre traefik et les services docker pour eviter d'avoir à les exposer 
configs:
  traefik-config-tls:
    external: true
  traefik-config-v4:
    external: true
secrets:
  traefik-auth:
    external: true
  ovh-application-key:
    external: true
  ovh-application-secret:
    external: true
  ovh-consumer-key:
    external: true

Avec notre stack fraichement deployée, on accès à la webUI de traefik !

Si on se rend dans les routers de la section http, on va retrouver ceux deja definie par la conf

Et leur services correspondant :

Deployons maintenant notre service whoami dans le swarm avec la stack suivante :

version: '3.3'
services:
  geco-whoami:
    image: containous/whoami:latest
    networks:
     - traefiked
    logging:
      driver: json-file
    deploy:
      replicas: 3
      labels:
        traefik.http.routers.geco-whoami.rule: Host(`whoami.geco-it.net`)                            #On déclare le routeur pour notre app, avec la règle qui match le nom d'hote "whoami.geco-it.net"
        traefik.http.routers.geco-whoami.entrypoints: https                                          #Sur quel entrypoint ce routeur va être effectif, seulement https dans notre cas
        traefik.http.routers.geco-whoami.middlewares: geco-whoami-auth, geco-whoami-path             #On applique deux middleware à notre routeur
        traefik.http.services.geco-whoami.loadbalancer.server.port: '80'                             #Le port d’écoute réel de notre container whoami
        traefik.http.middlewares.geco-whoami-path.addPrefix.prefix: /foo                             #Un middleware pour ajouter le prefix /foo à la requête juste pour la demo
        traefik.http.middlewares.geco-whoami-auth.basicauth.usersFile: /run/secrets/traefik-auth     #Un autre middleware pour ajouter la même authentication que la webui traefik
        traefik.docker.network: traefiked
        traefik.enable: 'true'
networks:
  traefiked:
    external: true

Une fois la stack deployée ont peut retourner voir la liste des routers pour voir le nouveau “whoami” :

Et son service correspondant :

Le détail du service whoami

Si maintenant on scale notre service pour le passer de 3 à 6 instances :

admin@docker-manager:~$ sudo docker service ls
ID             NAME                  MODE         REPLICAS   IMAGE                           PORTS
oac8dhrgzwn8   traefik_traefik       replicated   1/1        traefik:latest                  
jsj85zzenu24   whoami_geco-whoami    replicated   3/3        containous/whoami:latest        
admin@docker-manager:~$ sudo docker scale jsj85zzenu24=6
admin@docker-manager:~$ sudo docker service scale jsj85zzenu24=6
jsj85zzenu24 scaled to 6
overall progress: 6 out of 6 tasks 
1/6: running   [==================================================>] 
2/6: running   [==================================================>] 
3/6: running   [==================================================>] 
4/6: running   [==================================================>] 
5/6: running   [==================================================>] 
6/6: running   [==================================================>] 
verify: Service converged 
admin@docker-manager:~$ sudo docker service ls
ID             NAME                  MODE         REPLICAS   IMAGE                           PORTS
oac8dhrgzwn8   traefik_traefik       replicated   1/1        traefik:latest                  
jsj85zzenu24   whoami_geco-whoami    replicated   6/6        containous/whoami:latest

Et qu'on retourne sur les détails du service :

On voit bien les 3 instances supplémentaires ! Il est important de souligner que à aucun moment nous avons touché la configuration de traefik depuis son déploiement…

Et si on interroge le service en question depuis un naviguateur, on tombe bien sur notre application whoami, avec le chemin /foo en plus ( voir loigne “GET” ), l'auth ( voir ligne “Authorization” ) et le TLS ( cadenat OK sur le naviguateur ).

Le mot de la fin

J’espère vous avoir ouvert les yeux sur les possibilités et la flexibilité immenses de ce petit outil fort sympathique ! Il peut vous permettre de déléguer des accès à votre cluster swarm ou kubernetes, à une équipe de développeurs par exemple, et leur donner la possibilité de gérer eux mêmes l'accès à leur environnement de dev via le déploiement de leur stack ! Traefik permet aussi de constituer un environnement hybride, par exemple avec une partie de la prod sous docker, et une autre partie physique. Traefik peut gérer les deux à la fois, ce qui rend en plus votre load balancer hautement disponible puisque traefik est dans un cluster swarm !