Docker, noms de domaines des conteneurs

Utilisant Docker quotidiennement et travaillant sur plusieurs projets, je souhaitais harmoniser les configurations et utiliser des noms de domaines locaux pour accéder à mes conteneurs.

Il y a plusieurs solutions existantes pour faire ça, comme Traefik ou DNS Proxy Server.

Traefik fonctionne bien, apporte beaucoup de fonctionnalités, mais n’est pas « compatible » avec mysql en utilisant les routeurs TCP sur TLS (voir cette issue pour plus de détails).

DNS Proxy Server (DPS) fonctionne pas trop mal mais pose problème sur la résolution des noms de domaines externes aux conteneurs. Un simple ping google.fr ne passe pas si on utilise pas le réseau en mode bridge.

Ne trouvant pas mon bonheur dans ces solutions, j’ai décidé de créer un petit script shell qui se chargerai de maintenir à jour le fichier /etc/hosts.

Le principe est simple : écouter les événements de Docker lorsque des conteneurs sont démarrés, arrêtés, détruits ou tués, et mettre à jour le fichier /etc/hosts avec la liste des conteneurs en fonctionnement, en se basant sur la configuration hostname des conteneurs et leur IP assignée.

Écouter les événements Docker

Créons un script bash :

echo '!#/bin/bash' > docker-hosts.sh && chmod +x docker-hosts.sh

Pour écouter les événements Docker, j’ai trouvé ce gist que j’ai adapté :

function listen_docker_events() {
    docker events --filter 'event=start' --filter 'event=stop' --filter 'event=kill' --filter 'event=destroy' | while read event
    do
        #update_hosts
    done
}

listen_docker_events

Ajoutons des marqueurs d’emplacement dans /etc/hosts afin de cloisonner la mise à jour des IPs. J’ai mis des marqueurs largement inspirés des marqueurs des recettes flex.

###> docker-hosts ###
###< docker-hosts ###

Dès qu’un conteneur est démarré ou arrêté, on mettra à jour la liste des IPs <> Noms d’hôtes dans notre fichier /etc/hosts.

Mise à jour de /etc/hosts

Pour la mise à jour du fichier /etc/hosts nous allons créer une fonction nommée update_hosts.

function update_hosts() {
    CONTAINERS=$(docker ps -q | awk '{ print $1 }')

    HOST_PLACEHOLDER_START='###> docker\-hosts ###'
    HOST_PLACEHOLDER_END='###< docker\-hosts ###'
    HOST_ITEMS=''

    while read -r CONTAINER; do
        HOST_ITEMS+=$(echo -e $(docker inspect  --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}\t\t{{ .Config.Hostname }}' $CONTAINER))
        HOST_ITEMS+='\n'
    done <<< "$CONTAINERS"

    sed -i -e "/${HOST_PLACEHOLDER_START}/,/${HOST_PLACEHOLDER_END}/c\\${HOST_PLACEHOLDER_START}\n${HOST_ITEMS}${HOST_PLACEHOLDER_END}" /etc/hosts
}

Décomposons cette fonction :

CONTAINERS=$(docker ps -q | awk '{ print $1 }')

On récupère la liste des identifiants des conteneurs en cours de fonctionnement et on la stocke dans la variable CONTAINERS.

HOST_PLACEHOLDER_START='###> docker\-hosts ###'
HOST_PLACEHOLDER_END='###< docker\-hosts ###'
HOST_ITEMS=''

On définis les marqueurs qui serviront au remplacement et la variable qui contiendra la liste des IPs <> Noms d’hôte (HOST_ITEMS).

while read -r CONTAINER; do
# ...
done <<< "$CONTAINERS"

On boucle sur la liste des identifiants des conteneurs en cours de fonctionnement.

HOST_ITEMS+=$(echo -e $(docker inspect  --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}\t\t{{ .Config.Hostname }}' $CONTAINER))

À l’aide de la commande docker inspect, on récupère les informations utiles de chaque conteneur : Adresse IP et nom d’hôte à l’aide du format suivant :

{{ if gt (len .NetworkSettings.Networks) 1 }}
    {{ range \$name,\$network := .NetworkSettings.Networks }}
        {{ \$hasUnderscore := gt (len (split \$name "_")) 0 }}
        {{ if \$hasUnderscore }}
            {{ range \$part := (split \$name "_") }}
                {{ if eq \$part "default" }}
                    {{ \$network.IPAddress }}
                {{ end }}
            {{ end }}
        {{ else }}
            {{ .NetworkSettings.Networks.IPAddress }}
        {{ end }}
    {{ end }}
{{ else }}
    {{ range .NetworkSettings.Networks }}
        {{ .IPAddress }}
    {{ end }}
{{ end }}
\t\t
{{ .Config.Hostname }}
sed -i -e "/${HOST_PLACEHOLDER_START}/,/${HOST_PLACEHOLDER_END}/c\\${HOST_PLACEHOLDER_START}\n${HOST_ITEMS}${HOST_PLACEHOLDER_END}" /etc/hosts

On remplace tout le contenu situé entre le marqueur de début ###> docker-hosts ### et le marqueur de fin ###< docker-hosts ### par la liste des IPs et noms d’hôtes de la variable HOST_ITEMS.

Vu que /etc/hosts n’est inscriptible qu’en ayant les droits root, on pourra tester le script en essayant sur un fichier host de test situé dans le même répertoire que le script : cat /etc/hosts > ./hosts

Ensuite, on remplace dans la commande sed le fichier sur lequel le remplacement se fera :

sed -i -e « […] » ./hosts

Déclarer le daemon

Déclarons un service systemd pour démarrer automatiquement ce script lors du démarrage de l’OS.

# /etc/systemd/system/docker-host.service
[Unit]
Description=Docker host daemon
After=docker.service

[Service]
ExecStart=<CHEMIN_VERS_VOTRE_SCRIPT>/docker-hosts.sh
Restart=on-failure

[Install]
WantedBy=multi-user.target

On active ensuite le service et on le lance :

sudo systemctl enable docker-host.service
sudo systemctl start docker-host.service

Exemple avec docker-compose

On utilise un docker-compose.yml avec 2 services basiques :

version: '3'
services:
    hello-world-1:
        image: strm/helloworld-http
        container_name: hello-world-1
        hostname: hello-world-1.papsou

    hello-world-2:
        image: strm/helloworld-http
        container_name: hello-world-2
        hostname: hello-world-2.papsou

Notre fichier /etc/hosts ressemble à ça avant le lancement :

# /etc/hosts

127.0.0.1              localhost localhost.localdomain localhost4 localhost4.localdomain4
::1                    localhost localhost.localdomain localhost6 localhost6.localdomain6

# Some fixed IP

# [...]

###> docker-hosts ###
###< docker-hosts ###

On lance ces 2 services :

docker-compose up -d --build
Creating hello-world-1 ... done
Creating hello-world-2 ... done

Et voici le fichier après le lancement :

# /etc/hosts

127.0.0.1              localhost localhost.localdomain localhost4 localhost4.localdomain4
::1                    localhost localhost.localdomain localhost6 localhost6.localdomain6

# Some fixed IP

# [...]

###> docker-hosts ###
192.168.48.3		hello-world-2.papsou
192.168.48.2		hello-world-1.papsou
###< docker-hosts ###

On accède donc au premier service hello-world-1 en se rendant sur tapant http://hello-world-1.papsou et sur hello-world-2 en se rendant sur tapant http://hello-world-2.papsou.

On coupe les services :

docker-compose down && cat /etc/hosts
Stopping hello-world-2 ... done
Stopping hello-world-1 ... done
Removing hello-world-2 ... done
Removing hello-world-1 ... done
Removing network hello-world_default

Et voici le contenu de /etc/hosts après la coupure :

# /etc/hosts

127.0.0.1              localhost localhost.localdomain localhost4 localhost4.localdomain4
::1                    localhost localhost.localdomain localhost6 localhost6.localdomain6

# Some fixed IP

# [...]

###> docker-hosts ###
###< docker-hosts ###

Le script final

#!/bin/bash

function update_hosts() {
    CONTAINERS=$(docker ps -q | awk '{ print $1 }')

    HOST_PLACEHOLDER_START='###> docker\-hosts ###'
    HOST_PLACEHOLDER_END='###< docker\-hosts ###'
    HOST_ITEMS=''
    INSPECT_FORMAT_STRING=$(cat << EOT
{{ if gt (len .NetworkSettings.Networks) 1 }}
    {{ range \$name,\$network := .NetworkSettings.Networks }}
        {{ \$hasUnderscore := gt (len (split \$name "_")) 0 }}
        {{ if \$hasUnderscore }}
            {{ range \$part := (split \$name "_") }}
                {{ if eq \$part "default" }}
                    {{ \$network.IPAddress }}
                {{ end }}
            {{ end }}
        {{ else }}
            {{ .NetworkSettings.Networks.IPAddress }}
        {{ end }}
    {{ end }}
{{ else }}
    {{ range .NetworkSettings.Networks }}
        {{ .IPAddress }}
    {{ end }}
{{ end }}
\t\t
{{ .Config.Hostname }}
EOT
    )

    while read -r CONTAINER; do
        HOST_ITEMS+=$(echo -e $(docker inspect  --format "${INSPECT_FORMAT_STRING}" $CONTAINER))
        HOST_ITEMS+='\n'
    done <<< "$CONTAINERS"

    sed -i -e "/${HOST_PLACEHOLDER_START}/,/${HOST_PLACEHOLDER_END}/c\\${HOST_PLACEHOLDER_START}\n${HOST_ITEMS}${HOST_PLACEHOLDER_END}" /etc/hosts
}

function listen_docker_events() {
    docker events --filter 'event=start' --filter 'event=stop' --filter 'event=kill' --filter 'event=destroy' | while read event
    do
        update_hosts
    done
}

listen_docker_events

Un commentaire