• BLOG

  • Installer un frontal pour services docker (FR)

    ven. 01 septembre 2017 Dan Lousqui

    Share on: Twitter - Facebook - Google+

    docker

    Vous avez installé Docker sur un serveur, vous y avez préparé et configuré de superbes services (ex: GloWeeChat, Gitlab, Seafile, Owncloud, ...), mais vous êtes face à plusieurs soucis :

    • Le service en question ne gère pas HTTPS, et vous souhaitez utiliser letsencrypt de façon automatique pour ne pas vous embêter avec ça, et avoir une note de "A" au test ssllabs ;
    • Vous ne pouvez pas exposer chaque service sur les ports 80/443 (HTTP/HTTPS), vu qu'un port ne peut être exposé que par un seul conteneur ;
    • Vous ne faites pas confiance à la stack HTTP du conteneur (gunicorn, expresJS, ...) et préférez que l'écoute sur Internet s'effectue avec un serveur Web "standard" (nginx, apache, ...).

    Nous allons voir comment régler ces problématiques avec des solutions dédiées à cela.

    Rappel du fonctionnement de Docker

    Lorsque vous montez un conteneur Docker exposé sur l'extérieur, vous lancez une commande du type :

    docker run -d --name service_instance_1 -p 80:8080 -p 443:8443 MonSuperEditeur1/MonSuperService1

    Cela permet de créer un conteneur appelé service_instance_1, qui sera une instance de MonSuperEditeur1/MonSuperService1 (complètement fictif).

    Le conteneur MonSuperEditeur1/MonSuperService1 écoute sur les ports 8080 et 8443, donc les options -p 80:8080 et -p 443:8443 permettent à Docker de faire en sorte que l'hôte pointe les ports 80 et 443 (HTTP/HTTPS) vers le service, afin que lorsque l'on contacte http(s)://ip_du_serveur, cela fonctionne.

    Du coup, les ports 80 et 443 sont réservés à MonSuperEditeur1/MonSuperService1.

    Imaginons maintenant que l'on souhaite installer sur ce même serveur le service MonSuperEditeur2/MonSuperService2.

    Nous ne pouvons pas le déployer sur 80 et 443, nous devons donc le déployer sur 81 et 444 (par exemple):

    docker run -d --name service_instance_2 -p 81:8080 -p 444:8443 MonSuperEditeur2/MonSuperService2

    On peut schématiser cette solution avec l'illustration suivante :

    docker

    Du coup, on devra accéder au service1 via http(s)://ip_du_serveur, et au service2 via http(s)://ip_du_serveur:81(444). Ce n'est pas très pratique ... surtout si avec nombre de service plus important, voir dynamique.

    Solution, Reverse Proxy

    Pour rappel, un Reverse Proxy (HTTP) est un serveur que l'on place entre un serveur HTTP et Internet (ou toute autre zone d'où un utilisateur peut venir), et qui transmettra les requêtes du serveur HTTP afin que les utilisateurs ne communiquent pas directement avec.

    On peut illustrer cela ainsi :

    • Sans Reverse Proxy :

    docker

    • Avec Reverse Proxy :

    docker

    Ce que l'on peut noter :

    • Le serveur web n'est plus directement accessible depuis Internet. Ainsi, si l'on forge des requêtes HTTP non valides, c'est le Reverse Proxy qui se chargera de les filtrer ;
    • Le port TLS/SSL du serveur web n'est plus utilisé, c'est le Reverse Proxy qui se charge de chiffrer les communications. Du coup, si le service web ne gère pas TLS/SSL, ce n'est pas grave, le Reverse Proxy s'en chargera.

    Si l'on souhaite appliquer ce mécanisme sur nos conteneurs, le schéma cible est le suivant :

    docker

    Cela permet :

    • D'avoir tous les services exposés sur les ports HTTP standards (80 et 443) ;
    • De centraliser la gestion des certificats TLS/SSL ;
    • De ne plus exposer aucun service Docker (autre que le Reverse Proxy) sur Internet.

    Le serveur n'écoutant "que" sur 80 et 443, afin de distinguer si l'utilisateur veut utiliser MonSuperService1 ou MonSuperService2, le "switch" peut être fait de deux façons différentes :

    • Via le dns, en configurant un wildcard sur *.domain.tld, afin d'accéder à MonSuperService1 via http(s)://service1.domain.tld et MonSuperService2 via http(s)://service2.domain.tld ;
    • Ou via le path, afin d'accéder à MonSuperService1 via http(s)://domain.tld/service1 et MonSuperService2 via http(s)://domain.tld/service2.

    Pour des raisons de compatibilité, je préfère la première option.

    On peut aussi noter que le Reverse Proxy est un conteneur. Ce n'est pas obligatoire, mais c'est ce qui est utilisé dans les deux solutions que je souhaite présenter :

    Préparation

    Pour tester les deux solutions, nous allons utiliser comme "service" de test emilevauge/whoami. Il s'agit d'un simple service, qui écoute sur le port 80 et qui renvoie des informations sur le conteneur l'hébergeant.

    Pour lancer ce service, les commandes suivantes suffisent :

    docker run -d --name service_instance_1 **XXX** emilevauge/whoami
    docker run -d --name service_instance_2 **XXX** emilevauge/whoami
    

    (Les **XXX** seront remplacés par les options suivant la méthode choisie)

    Il faut également configurer le DNS notre serveur afin d'avoir le wildcard *.domain.tld pointant vers le serveur.

    Sans Reverse Proxy

    Sans Reverse Proxy, pas le choix, on doit :

    • Exposer les conteneurs sur Internet ;
    • Utiliser un port à exposer diffèrent sur les deux services ;
    • Le service ne proposant pas de TLS/SSL, impossible de faire du HTTPS.

    Les commandes sont les suivantes :

    docker run -d --name service_instance_1 -p 80:80 emilevauge/whoami
    docker run -d --name service_instance_2 -p 81:80 emilevauge/whoami
    

    Et les services sont accessibles via :

    Avec jwilder/nginx-proxy (github)

    jwilder/nginx-proxy est une solution permettant de déployer un serveur nginx qui jouera automatiquement le rôle de Reverse Proxy vers les conteneurs ayant une variable d'environnement VIRTUAL_HOST.

    Il peut être utilisé conjointement avec JrCs/docker-letsencrypt-nginx-proxy-companion, afin d'automatiser la génération de certificat TLS/SSL via les variables d'environnement LETSENCRYPT_HOST et LETSENCRYPT_EMAIL.

    Pour installer et configurer ces deux outils :

    mkdir -p /docker/proxy/
    chmod -R 777 /docker/proxy/ # Peut probablement faire mieux, je suis ouvert à toute proposition
    
    docker run -d \
      -e DEFAULT_HOST=domain.tld \                    # Domaine par défaut
      -v "/docker/proxy/certs:/etc/nginx/certs:ro" \  # Certificats
      -v "/docker/proxy/vhost.d:/etc/nginx/vhost.d" \ # Configurations
      -v "/docker/proxy/html:/usr/share/nginx/html" \ # Letsencrypt challenge
      -v "/var/run/docker.sock:/tmp/docker.sock:ro" \ # Docker socket
      -l com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy=true \
      -p 80:80 \
      -p 443:443 \
      jwilder/nginx-proxy:latest
    
    docker run -d \
      -v "/docker/proxy/certs:/etc/nginx/certs:rw" \
      -v "/var/run/docker.sock:/var/run/docker.sock:ro" \
      -v "/docker/proxy/vhost.d:/etc/nginx/vhost.d" \
      -v "/docker/proxy/html:/usr/share/nginx/html" \
      jrcs/letsencrypt-nginx-proxy-companion:latest
    

    Il ne reste plus qu'a démarrer les services avec les bons paramètres :

    docker run -d \
      --name service_instance_1 \
      -e "VIRTUAL_HOST=service1.domain.tld" \
      -e "LETSENCRYPT_HOST=service1.domain.tld" \
      -e "LETSENCRYPT_EMAIL=contact@domain.tld" \
      emilevauge/whoami
    
    docker run -d \
      --name service_instance_2 \
      -e "VIRTUAL_HOST=service2.domain.tld" \
      -e "LETSENCRYPT_HOST=service2.domain.tld" \
      -e "LETSENCRYPT_EMAIL=contact@domain.tld" \
      emilevauge/whoami
    

    Ces commandes relanceront (automatiquement) le Reverse Proxy, et les certificats seront automatiquement générés sur letsencrypt. Les services seront ensuite accessibles via :

    Note: le port HTTP (80) fonctionne aussi, mais il s'agira d'une redirection avec HSTS vers du HTTPS (443).

    Avec Traefik (website)

    Traefik est quant à lui un outil à part entière (développé en Go), dont le but est exactement de répondre à la problématique de routage dynamique de Reverse Proxy vers des services.

    Il peut être déployé via Docker, et peut router vers des services hébergés sur des conteneurs.

    Contrairement à jwilder/nginx-proxy, Traefik :

    • N'utilise pas des variables d'environnement pour détecter les conteneurs à router ;
    • N'a pas besoin de monter des dossiers de configurations en tant que volume, seul deux fichiers de configuration sont nécessaires.

    La configuration de Traefik se fait via des fichiers toml. Nous utiliserons les fichiers suivants :

    • /docker/traefik/traefik.toml (configuration de Traefik)
    defaultEntryPoints = ["http", "https"]
    [web]
    address = ":8080"
    [acme]
    email = "contact@domain.tld"
    storageFile = "/etc/traefik/acme.json"
    entryPoint = "https"
    acmeLogging = true
    onDemand = true
    [accessLog]
    filePath = "/path/to/log/log.txt"
    format = "common"
    [entryPoints]
      [entryPoints.http]
      address = ":80"
        [entryPoints.http.redirect]
        entryPoint = "https"
      [entryPoints.https]
      address = ":443"
         [entryPoints.https.tls]
    [docker]
    endpoint = "unix:///var/run/docker.sock"
    domain = "domain.tld"
    exposedbydefault = false
    watch = true
    
    • /docker/traefik/acme.json (certificats TLS/SSL)

    Pour initialiser /docker/traefik/acme.json, simplement faire un touch /docker/traefik/acme.json (pour dire à Docker de le traiter comme un fichier, et non un dossier).

    Pour lancer Traefik, la commande est la suivante :

    docker run -d \
     -v /var/run/docker.sock:/var/run/docker.sock \
     -v /docker/traefik/traefik.toml:/etc/traefik/traefik.toml \
     -v /docker/traefik/acme.json:/etc/traefik/acme.json \
    # Si vous souhaitez acceder au dashboard de Traefik via traefik.domain.tld
    #  -l "traefik.frontend.rule=Host:traefik.domain.tld" \
    #  -l "traefik.port=8080" \
      -p 80:80 \
      -p 443:443 \
     traefik \
     --docker --logLevel=DEBUG
    

    Il ne reste plus qu'a démarrer les services avec les bons paramètres :

    docker run -d \
      --name service_instance_1 \
      -l "traefik.backend=service1" \
      -l "traefik.frontend.rule=Host:service1.domain.tld" \
      emilevauge/whoami
    
    docker run -d \
      --name service_instance_2 \
      -l "traefik.backend=service2" \
      -l "traefik.frontend.rule=Host:service2.domain.tld" \
      emilevauge/whoami
    

    Ces commandes relanceront (automatiquement) le Reverse Proxy, et les certificats seront automatiquement générés sur letsencrypt. Les services seront ensuite accessibles via :

    Comparatif

    D'un point de vue fonctionnel, Traefik et jwilder/nginx-proxy semblent équivalents. Il faudra cependant bien choisir entre l'un et l'autre.

    Chacun ont leurs forces :

    • jwilder/nginx-proxy est basé sur nginx et bien que plus d'aspect patchwork (la configuration du Reverse Proxy est générée à partir de template à chaque modification des containeurs), permet d'avoir une customisation des paramètres nginx pour chaque hostname. Ça permet par exemple de faire du whitelisting d'IP propre par hostname, une authentification LDAP, ... Par contre, le développement est uniquement maintenu par jwilder, son déploiement en production est donc à évaluer ...

    • Traefik est un projet spécialisé pour faire du Reverse Proxy, il le fera bien, de façon efficace et rapide, et possède même des fonctions avancées très intéressantes (haute disponibilité, load balancing, dashboard avec visibilité des temps de réponse, SLA, ...). De plus, le projet est maintenu par une entreprise ayant des contrats de support avec ses clients, une plus grande confiance peut être donnée à cette solution pour une mise en production.

  • Comments