Erreurs Courantes de Configuration Systemd et Comment les Corriger

Corrigez les erreurs fréquentes des fichiers unit systemd : mauvais chemins, types de service incorrects, variables d'environnement manquantes, permissions et ordre des dépendances.

Erreurs Courantes de Configuration Systemd et Comment les Corriger

Les erreurs de configuration Systemd semblent souvent plus dramatiques qu'elles ne le sont. Un service refuse de démarrer, un déploiement est annulé, ou un démarrage bloque sur un nom d'unité dont vous vous souvenez à peine avoir créé. Puis la vraie cause s'avère être une barre oblique manquante dans ExecStart=, un processus s'exécutant sous le mauvais utilisateur, ou une modification de fichier unité qui n'a jamais atteint le gestionnaire systemd parce que personne n'a exécuté daemon-reload.

La façon la plus rapide de résoudre ces problèmes est de considérer le fichier unité comme un contrat. Il indique à systemd quel processus exécuter, sous quel utilisateur, ce qui doit exister en premier, comment la disponibilité est signalée, et ce qui doit se passer après un crash. Lorsqu'un de ces détails est incorrect, systemd fait généralement exactement ce qu'on lui a dit. Le travail consiste à trouver le décalage entre ce dont l'application a besoin et ce que le fichier unité dit réellement.

1. Erreurs de Syntaxe et de Chemin dans les Fichiers Unité

L'une des causes les plus fréquentes d'échec de service est une simple faute de frappe ou un chemin mal défini dans le fichier unité.

Chemins Incorrects ou Non Absolus dans les Commandes Exec

Systemd n'exécute pas votre service depuis la même session shell que celle utilisée pour les tests. Il démarre le processus dans un environnement contrôlé, donc les hypothèses concernant les alias, les fonctions shell, l'activation de virtualenv et un PATH personnalisé échouent souvent. Utilisez des chemins absolus pour l'exécutable dans ExecStart= et soyez explicite sur chaque répertoire ou fichier dont le service a besoin.

L'Erreur :

Utiliser un nom de commande sans spécifier son emplacement.

[Service]
ExecStart=my-app-server --config /etc/config.yaml

Si my-app-server se trouve dans /usr/local/bin, systemd ne le trouvera probablement pas.

La Correction :

Utilisez toujours le chemin absolu complet vers l'exécutable.

[Service]
ExecStart=/usr/local/bin/my-app-server --config /etc/config.yaml

Avant de configurer ExecStart=, vérifiez le chemin avec command -v my-app-server ou which my-app-server. Si l'application se trouve dans un emplacement spécifique au langage, comme un environnement virtuel Python sous /opt/myapp/venv/bin/gunicorn, pointez directement vers ce binaire au lieu de vous fier aux scripts d'activation.

Erreurs Typographiques et Sensibilité à la Casse

Les directives de configuration Systemd sont sensibles à la casse et doivent être placées dans les sections correctes ([Unit], [Service], [Install]). Les fautes d'orthographe ou une capitalisation incorrecte entraîneront l'échec du chargement du service ou un comportement inattendu.

Exemple d'Erreur :

[Service]
ExecStart=/usr/bin/python3 app.py
RestartAlways=true  ; Devrait être Restart=always

La Correction :

Utilisez systemd-analyze verify <fichier_unite> avant de recharger le démon. Cela ne détectera pas toutes les erreurs d'exécution, mais cela détecte de nombreuses directives mal orthographiées, des placements de section invalides et des erreurs d'analyse avant que vous ne perdiez du temps à chercher dans les journaux d'application.

$ systemd-analyze verify /etc/systemd/system/my-service.service

2. Mauvaise Gestion des Dépendances et de l'Ordre des Services

Les dépendances définissent quelles ressources un service nécessite, tandis que l'ordre définit quand ces ressources doivent être disponibles.

Confusion entre Requires et Wants

Ces directives sont utilisées pour définir les dépendances mais gèrent les échecs différemment :

  • Wants= : Une dépendance faible. Si l'unité souhaitée échoue ou ne démarre pas, l'unité actuelle tentera toujours de démarrer. Utilisez-la pour les dépendances non critiques.
  • Requires= : Une dépendance forte. Si l'unité requise ne peut pas être démarrée, l'unité actuelle échoue également. Si l'unité requise est explicitement arrêtée, l'unité dépendante est également arrêtée.

Se Fier à Requires sans Ordre Approprié

Définir une dépendance, par exemple Requires=network.target, intègre la dépendance dans la transaction. Cela ne crée pas en soi un ordre de démarrage, et network.target ne signifie pas "le réseau est utilisable pour les connexions sortantes". Si votre service nécessite un réseau configuré, utilisez network-online.target et assurez-vous que le service d'attente en ligne de la distribution est activé lorsque ce comportement est requis.

L'Erreur :

Un serveur web démarre, mais la connexion à la base de données échoue car la pile réseau est encore en cours d'initialisation.

La Correction : Utiliser After= et Before=

Pour imposer un ordre, vous devez utiliser After= (ou Before=). Une exigence courante est de s'assurer que le réseau est complètement opérationnel et configuré avant de continuer.

[Unit]
Description=Mon Service d'Application Web
Wants=network-online.target
After=network-online.target

[Service]
...

Pour la plupart des services d'application, associez l'intention de dépendance à l'intention d'ordre. Wants=postgresql.service signifie "veuillez aussi démarrer PostgreSQL". After=postgresql.service signifie "démarrez-moi après la fin du travail de démarrage de PostgreSQL". Ils résolvent des problèmes différents.

Gestion Incorrecte du Type de Service

Les services Systemd ont plusieurs types d'exécution, gérés par la directive Type=. Une mauvaise configuration est une cause courante de services qui démarrent momentanément puis échouent immédiatement.

L'Erreur : Mauvais usage de Type=forking

Si votre application est conçue pour s'exécuter au premier plan et maintenir un seul processus principal, définir Type=forking indique à systemd de s'attendre à un comportement de démon à l'ancienne. Cela peut entraîner des résultats déroutants : systemd peut attendre la sortie d'un processus parent, ne pas identifier le véritable processus principal, ou marquer le service comme actif alors que l'application n'est pas réellement prête.

Les Corrections :

  1. Pour les applications modernes : Utilisez Type=simple. C'est la valeur par défaut et elle s'attend à ce que le processus ExecStart soit le processus principal.
  2. Pour les applications legacy qui se démonifient (fork) : Définissez Type=forking et, crucialement, définissez la directive PIDFile= pour que systemd puisse suivre le processus enfant qui a survécu au fork.
[Service]
Type=forking
PIDFile=/var/run/legacy-app.pid
ExecStart=/usr/sbin/legacy-app

Il existe un autre piège courant de disponibilité : utiliser Type=simple pour une application qui met beaucoup de temps à devenir utilisable. Avec Type=simple, systemd considère le service comme démarré dès que le processus est lancé. Si un autre service démarre immédiatement après et s'y connecte, vous pouvez voir des échecs intermittents. Pour les applications qui peuvent notifier systemd lorsqu'elles sont prêtes, Type=notify est plus propre. Pour les applications qui ne le peuvent pas, évitez de faire semblant que l'unité est complètement prête simplement parce que le processus existe ; utilisez une véritable vérification de santé dans l'application dépendante, l'activation par socket, ou une vérification ExecStartPre= lorsque le prérequis est suffisamment simple à tester.

Soyez également prudent avec oneshot. Un service Type=oneshot est destiné à une commande qui effectue une tâche et se termine, comme créer un répertoire, charger une règle de pare-feu, ou exécuter une migration. Si vous l'utilisez pour un démon de longue durée, systemd ne le supervisera pas comme vous le souhaitez. Si la commande se termine avec succès et que vous souhaitez que l'unité reste "active" à des fins de dépendance, ajoutez RemainAfterExit=yes ; sinon, les unités dépendantes peuvent ne pas voir l'état que vous aviez prévu.

3. Problèmes de Contexte Environnemental et d'Utilisateur

Les échecs de service proviennent souvent du fait que le service s'exécute dans un contexte différent de celui attendu par l'application, généralement lié aux permissions ou aux variables d'environnement.

Permission Refusée ou Fichiers Manquants

Lorsque vous testez une application manuellement, elle s'exécute généralement sous votre compte utilisateur avec les permissions appropriées. Lorsqu'elle est exécutée par systemd, elle utilise par défaut l'utilisateur root ou l'utilisateur spécifié dans le fichier unité.

L'Erreur :

Les symptômes typiques sont directs : Permission denied, No such file or directory, Failed to open log file, ou une erreur spécifique à l'application indiquant qu'elle ne peut pas créer un socket, écrire un fichier PID, ou lire un fichier de configuration. L'unité peut fonctionner lorsque vous exécutez la commande manuellement en tant que root, puis échouer sous User=app.

La Correction :

  1. Définir un Utilisateur Non-Root : Spécifiez toujours un utilisateur et un groupe dédiés et à faibles privilèges pour votre service.

    [Service]
    User=www-data
    Group=www-data
    ...
    
  2. Vérifier la Propriété : Assurez-vous que le répertoire de travail du service, les fichiers journaux et les fichiers de configuration appartiennent à l'User= et au Group= spécifiés.

    sudo chown -R www-data:www-data /var/www/my-app
    
  3. Vérifier chaque chemin touché par le service : Ne vous arrêtez pas au répertoire de l'application. Vérifiez WorkingDirectory=, les répertoires de journaux, les répertoires de téléchargement, les répertoires de cache, les fichiers de clés TLS, les sockets Unix, et tout chemin référencé par un fichier d'environnement.

    sudo -u www-data test -r /etc/my-app/config.yml
    sudo -u www-data test -w /var/lib/my-app
    sudo -u www-data /usr/local/bin/my-app --check-config
    

Si le service doit se lier au port 80 ou 443, ne l'exécutez pas automatiquement en tant que root. Sur de nombreux systèmes, vous pouvez placer un proxy inverse devant lui, utiliser l'activation par socket, ou accorder au binaire la capacité spécifique dont il a besoin. Le bon choix dépend du service, mais un processus root généralisé ne devrait pas être la réponse par défaut.

Un autre détail de permission surprend les gens : les répertoires parents ont besoin de la permission d'exécution. Un fichier peut sembler lisible, mais le service ne peut toujours pas y accéder car /opt, /opt/myapp, ou un autre répertoire parent bloque la traversée pour l'utilisateur du service. namei -l /opt/myapp/config.yml est utile car il affiche les permissions pour chaque composant du chemin au lieu du seul fichier final.

Variables d'Environnement Manquantes

Les services Systemd s'exécutent dans un environnement minimal. Toute variable d'environnement cruciale (comme les clés API, les chaînes de connexion à la base de données, ou les chemins de bibliothèques personnalisées) doit être explicitement transmise.

La Correction : Utiliser Environment= ou EnvironmentFile=

Pour les variables simples, utilisez Environment= :

[Service]
Environment="APP_PORT=8080"
Environment="API_KEY=ABCDEFG"

Pour des variables complexes ou nombreuses, utilisez EnvironmentFile= pointant vers un fichier .env standard :

[Service]
EnvironmentFile=/etc/default/my-app.conf

Gardez les secrets hors des fichiers unité lisibles par tout le monde. Un fichier unité sous /etc/systemd/system est généralement lisible par les utilisateurs locaux. Si vous mettez des clés API directement dans Environment=, supposez qu'elles sont exposées à quiconque peut lire les fichiers unité ou inspecter les métadonnées des processus. Préférez un fichier d'environnement appartenant à root avec des permissions restrictives, un gestionnaire de secrets, ou des identifiants systemd sur les distributions qui les prennent en charge.

Rappelez-vous également que EnvironmentFile= n'est pas un script shell. Des lignes telles que export APP_PORT=8080 ou des substitutions de commandes comme TOKEN=$(cat /run/token) ne sont pas interprétées comme elles le seraient dans Bash. Utilisez des affectations simples :

APP_PORT=8080
APP_ENV=production

Si l'application a besoin d'une configuration de shell de connexion, c'est généralement un signe de mauvaise conception. Mettez le véritable environnement dans le fichier unité, le fichier d'environnement, ou la propre configuration de l'application au lieu de dépendre de .bashrc.

Pour les environnements d'exécution de langage, pointez systemd vers l'environnement d'exécution que vous avez réellement testé. Un service Python devrait généralement appeler directement le binaire de l'environnement virtuel, comme /opt/myapp/venv/bin/python ou /opt/myapp/venv/bin/gunicorn. Un service Node installé via un gestionnaire de versions peut fonctionner dans votre terminal mais échouer sous systemd car nvm ou asdf ont modifié uniquement votre shell interactif. Dans les unités de production, les chemins explicites surpassent la magie de démarrage du shell.

4. Le Flux de Travail de Débogage Crucial

L'erreur de configuration la plus courante est d'oublier l'étape cruciale entre la modification du fichier unité et la tentative de redémarrage du service.

Oublier de Recharger le Démon

Systemd ne surveille pas automatiquement les modifications des fichiers unité. Après toute modification d'un fichier dans /etc/systemd/system/, le gestionnaire systemd doit être invité à recharger son cache de configuration.

L'Erreur :

Vous modifiez le fichier, exécutez systemctl restart my-service, mais l'ancienne configuration est toujours utilisée.

La Correction : Exécutez daemon-reload

Exécutez toujours cette commande immédiatement après avoir enregistré une modification de fichier unité :

sudo systemctl daemon-reload
sudo systemctl restart my-service

Si vous avez modifié une surcharge de type drop-in avec systemctl edit my-service, la même règle s'applique. La surcharge générée est stockée sous /etc/systemd/system/my-service.service.d/, et systemd a toujours besoin de recharger son cache d'unités avant que les nouveaux paramètres ne prennent effet.

Lorsqu'un redémarrage se comporte étrangement, inspectez l'unité fusionnée exacte que systemd voit :

systemctl cat my-service.service
systemctl show my-service.service -p FragmentPath -p DropInPaths -p User -p ExecStart

Cela permet de détecter une erreur courante : modifier une unité fournisseur dans /usr/lib/systemd/system alors qu'une surcharge dans /etc/systemd/system modifie toujours le paramètre, ou modifier une copie d'une unité qui n'est pas celle chargée par systemd.

Utilisation Efficace des Outils de Journalisation

Lorsqu'un service échoue, fiez-vous aux outils officiels pour un diagnostic précis.

  1. Vérifier l'État du Service : Cela vous donne l'état immédiat, les codes de sortie et les dernières lignes de journal.

    systemctl status my-service.service
    
  2. Inspecter le Journal : Le journal contient la sortie complète (stdout/stderr) du service. Recherchez des indices comme "Permission denied" ou "No such file or directory".

    # Voir les journaux récents spécifiquement pour votre unité
    journalctl -u my-service.service --since '1 hour ago' 
    
    # Voir les journaux et suivre la sortie en temps réel
    journalctl -f -u my-service.service
    

Un Passage Pratique de Dépannage

Lorsque je révise une unité défaillante, je fais généralement un passage dans cet ordre :

systemctl status my-service.service
journalctl -u my-service.service --since "15 minutes ago"
systemctl cat my-service.service
systemd-analyze verify /etc/systemd/system/my-service.service

Ensuite, je pose des questions simples. ExecStart= pointe-t-il vers un exécutable réel ? L'User= configuré peut-il l'exécuter ? WorkingDirectory= existe-t-il ? Les variables d'environnement sont-elles présentes sans dépendre d'un shell ? Le Type= est-il honnête sur le comportement du processus ? Wants= et After= sont-ils tous deux présents lorsque j'ai besoin qu'une autre unité soit démarrée et ordonnée avant celle-ci ?

Après chaque modification, rechargez et testez une chose :

sudo systemctl daemon-reload
sudo systemctl restart my-service.service
systemctl status my-service.service --no-pager

Si le service échoue toujours, résistez à l'envie de continuer à modifier le fichier unité aveuglément. Le journal vous dit généralement si le problème suivant est la configuration systemd, la configuration de l'application, les permissions, ou une dépendance qui échoue d'elle-même.