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 !
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 :
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 ).
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 !