Guillaume Fenollar DevOps et SysAdmin Freelance

Guillaume FENOLLAR

Ingénieur Linux/DevOps Indépendant

− Montpellier −

Gerer le lancement des conteneurs Docker avec Systemd

Cet article n’est pas là pour introduire quiconque à Docker, ni même à Systemd, les 2 étant assez documentés de par le web pour que j’ai à y mettre mon grain de sel, et les 2 étant les standards pour respectivement la conteneurisation d’applications, et la gestion de services (au sens Linux du terme).

La version manuelle de lancement de conteneurs Docker, et la première vue dans tout How-To sur le sujet, passe par la commande docker run, qui permet d’instancier un conteneur à partir d’une image Docker. Le gros inconvénient de cette méthode est l’instabilité de pareille méthode, car si le serveur reboot, ou si le processus interne s’arrête, ou si n’importe quel autre problème arrive , alors le docker va s’arrêter, et le service fourni avec, tout comme si au final on avait lançé un programme ou un script en boucle infini.

La suite logique est donc de transformer ce script en service pour que le système s’assure qu’il tourne correctement. C’est là que systemd rentre en jeu. Il serait parfaitement possible d’utiliser supervisord ou autre de ce genre mais mon choix se porte sur systemd car il est maintenant massivement adopté par défaut sur les distributions Linux récentes les plus utilisées.

Encapsuler un conteneur Docker dans un service systemd c’est s’assurer que :

  1. Le service démarrera proprement au démarrage de la machine
  2. Le conteneur redémarre automatiquement si besoin est
  3. Les logs (stdout) sont gérés par Journald et facile à requêter
  4. Tout l’écosystème systemd peut être utilisé et donc répondre à des besoins concrets, comme les dépendances, les pre/post commandes, la gestion de variables d’environnement centralisées, etc…

Certes les mauvaises langues diront que ça couvre 5% des fonctionnalités offertes par les orchestrateurs tels que Swarm ou Kubernetes, auquel je répondrai qu’il n’y a pas besoin de semi-remorque pour transporter une chaise en bois. Oui j’invente des expressions sur mon temps libre, ça me détend.

Les fichiers service

La version minimale d’un fichier /etc/systemd/system/nginx.service serait la suivante :

[Unit]
Description=Docker container

[Service]
ExecStart=/usr/bin/docker run --name nginx \
    --net host \
    -v /srv/nginx/conf.d:/etc/nginx/conf.d \
    -v /srv/nginx/index.html:/usr/share/nginx/html/index.html \
    nginx

ExecStop=/usr/bin/docker stop nginx
ExecStopPost=/usr/bin/docker rm -f nginx

[Install]
WantedBy=multi-user.target

Ce fichier tout à fait fonctionnel démarre un conteneur Docker nommé nginx, et va chercher sur le hub officiel de docker l’image ’nginx’ la plus récente, tout en lui passant en volume un dossier de configuration (que Docker créera sur l’host s’il n’existe pas). La partie WantedBy n’entrera en ligne de compte seulement avec la commande systemctl enable nginx, auquel cas le service sera activé au démarrage car sera lié à la target systemd multi-user, target par défaut. Plus de détails ici.

Par contre, ce service souffre de plusieurs faiblesses. Premièrement, en cas de redémarrage du service Docker, le service tombera en échec. Également, au démarrage de la machine, le service tentera peut-être de démarrer avant que le service Docker soit opérationnel, auquel cas cela pourrait finir par un échec également. Entre en compte les lignes BindsTo et After, qui s’assurent que le service prend Docker comme dépendance, et sera éteint en premier, si Docker venait à être stoppé.

Ensuite, dans le cas de Nginx qui est un service stateless, il serait ingénieux de prévoir une mise à jour du Docker au démarrage du service, au cas où une version plus récente serait disponible. C’est faisable en ajoutant une commande ExecStartPre responsable d’un docker pull qui viendra s’exécuter avant le ExecStart.

Également, nous pouvons spécifier que le service s’il tombe en erreur sera toujours redémarré, et ce toutes les 10 secondes si besoin, grâce aux lignes Restart et RestartSec. C’est l’équivalent de l’argument restart=onfailure à la commande docker run.

Un problème que nous avons aussi avec cette configuration, c’est que nous ne définissons pas de comportement à l’action reload. Auquel cas systemd va par défaut garder le comportement d’un restart, en appliquant stop puis start au service concerné. La méthode que j’utilise est tout simplement de définir puis d’exécuter un reload du process nginx à l’interieur de conteneur grâce à ExecReload. Il est également possible d’envoyer un signal (kill -s HUP) mais je trouve ça plus propre.

Enfin, il est possible de variabiliser les noms utilisés dans le fichier service grâce aux variables d’environnement, ligne Environment. Si les variables deviennent nombreuses ou que celles-ci deviennent communes à plusieurs fichiers service d’un même host, il est possible d’utiliser un fichier séparé pour ses variables, grâce à l’option EnvironmentFile.

Un fichier service “optimisé” ressemblerait donc à ça :

[Unit]
Description=Docker container
BindsTo=docker.service
After=docker.service

[Service]
Environment=NAME=%N
Environment=IMG=nginx
Restart=on-failure
RestartSec=10
ExecStartPre=-/usr/bin/docker kill ${NAME}
ExecStartPre=-/usr/bin/docker rm ${NAME}
ExecStart=/usr/bin/docker run --name ${NAME} \
    -p 80:80 \
    -p 443:443 \
    -v /srv/nginx/conf.d:/etc/nginx/conf.d \
    -v /srv/nginx/html:/usr/share/nginx/html/ \
    ${IMG}
ExecStop=/usr/bin/docker stop ${NAME}
ExecReload=/usr/bin/docker exec ${NAME} nginx -s reload

[Install]
WantedBy=multi-user.target

%N sera ici automatiquement remplacé par le nom du fichier du service. Ne pas oublier de lancer la commande systemctl daemon-reload à chaque changement de configuration d’un fichier .service.

Les journaux et logs des services

Un des gros intérêts de lancer tous ses fichiers service avec systemd est que la sortie standard passe dans la moulinette de journald, ce qui donne alors une méthode commune et simple de configurer et d’accéder aux logs d’une application. Pour garder l’exemple ci-dessus, l’accès aux logs de Nginx est aussi simple que :

journalctl -fu nginx

Voilà. Et en plus c’est facile à retenir.

Cela n’empêchera pas de pouvoir avoir des fichiers de logs distincts dans le cas de virtualhosts, dans quel cas il sera de bon ton de partager le dossier de logs (par défaut /var/log/nginx) du conteneur avec la machine hôte afin de ne pas les perdre au redémarrage.