Dépendances et conditions systemd : Requires, After
Requires, Wants, After, Before, ExecStartPre et Condition n’ont pas le même rôle. Apprenez à les distinguer et à diagnostiquer une unité qui ne démarre jamais.
Requires ne signifie pas After
Les dépendances et les conditions systemd sont quatre mécanismes distincts que la plupart des fichiers d’unité utilisent comme s’il s’agissait d’un seul. Requires= et Wants= déterminent les autres unités à démarrer. After= et Before= déterminent l’ordre de démarrage des unités. ExecStartPre= exécute un contrôle qui peut faire échouer l’unité. Les familles Condition et Assert déterminent si l’unité s’exécute ou non. Chaque mécanisme est indépendant des autres. Une unité peut donc en exiger une autre et démarrer malgré tout exactement au même moment.
Cette dernière phrase explique presque tous les rapports du type « cela fonctionne quand je le démarre manuellement, mais échoue au démarrage ».
[Unit]
Description=Inventory API
Requires=postgresql.service
[Service]
ExecStartPre=/usr/bin/pg_isready -h 127.0.0.1 -t 5
ExecStart=/usr/local/bin/inventory-apiRequires=postgresql.service ajoute PostgreSQL à la même transaction de démarrage. Cette directive n’attend pas PostgreSQL. systemd démarre les deux jobs en parallèle. pg_isready s’exécute donc pendant que PostgreSQL ouvre encore son répertoire de données. La commande se termine avec le code 2, car aucun processus n’écoute encore. L’unité échoue donc avant même d’atteindre ExecStart. Exécuter sudo systemctl start inventory-api une heure plus tard fonctionne, car PostgreSQL est déjà démarré. Rien n’a changé dans le fichier d’unité. C’est pourquoi celui-ci semble correct.
La correction tient en une ligne.
[Unit]
Requires=postgresql.service
After=postgresql.serviceUn détail important se trouve au même endroit. Une dépendance Requires= défaillante empêche votre unité de démarrer uniquement si vous définissez également After= sur cette dépendance. Sans cette contrainte d’ordre, systemd a déjà démarré votre unité lorsque l’autre échoue. Il n’y a donc plus rien à annuler. Requires= seul ne fournit pas la protection que l’on croit obtenir. Ajoutez After= à côté de chaque Requires= et de chaque Wants=, sauf raison précise de ne pas le faire.
Ce que garantissent Requires, Wants, Requisite et BindsTo
Tous ces paramètres définissent des dépendances. Aucun ne définit l’ordre de démarrage.
Wants=: inclut l’autre unité. Si celle-ci échoue ou n’existe pas, cette unité démarre quand même. C’est ce que créesystemctl enablesous forme de lien symbolique dans un répertoire.wants/.Requires=: inclut l’autre unité. Si celle-ci échoue et que vous avez également définiAfter=, cette unité ne démarre pas. Si l’autre unité est ensuite arrêtée explicitement, cette unité est arrêtée avec elle.Requisite=: n’inclut pas l’autre unité. Si celle-ci n’est pas déjà active, cette unité échoue immédiatement.BindsTo=: fonctionne commeRequires=, et cette unité s’arrête également dès que l’autre unité s’arrête, quelle qu’en soit la raison, notamment si du matériel disparaît.PartOf=: l’arrêt et le redémarrage se propagent de l’autre unité vers celle-ci. Le démarrage ne se propage pas.Conflicts=: le démarrage de cette unité arrête l’autre unité.
Pour un daemon qui communique avec un autre daemon, Wants= et After= constituent généralement le bon duo. Requires= lie les durées de vie : si vous arrêtez la base de données pour une opération de maintenance, votre application s’arrête avec elle et ne redémarre pas lorsque la base de données revient. Wants= et After= fournissent l’ordre de démarrage sans ce couplage, et une politique de redémarrage gère le cas où la dépendance disparaît ultérieurement.
Vous héritez également de dépendances que vous n’avez jamais écrites. Avec DefaultDependencies=yes, qui est le comportement par défaut, un service classique obtient automatiquement Requires=sysinit.target, After=sysinit.target basic.target et Conflicts=shutdown.target. C’est pourquoi un service dont la section [Unit] est presque vide démarre tout de même tard pendant le boot et s’arrête correctement lors de l’arrêt du système.
After et Before ordonnent la transaction, rien de plus
After= et Before= servent uniquement à définir l’ordre. Ils n’imposent aucune exigence. After=redis.service dans une unité qui ne fait intervenir Redis par aucune autre dépendance ne produit aucun effet : si redis.service ne fait pas partie de la transaction, il n’y a rien à attendre et votre unité démarre immédiatement.
Cela mérite d’être répété, car c’est exactement la forme de l’erreur network-online.target présentée plus loin. L’ordre n’attend que les unités qui sont déjà démarrées dans la même transaction.
La paire est symétrique. After=b.service écrit dans a.service signifie la même chose que Before=a.service écrit dans b.service. Utilisez donc l’un des deux, et placez-le dans l’unité que vous administrez. L’ordre s’inverse automatiquement à l’arrêt : After=b.service signifie également que votre unité est arrêtée avant b.service.
After= attend que le service soit « démarré », et Type= définit ce que cela signifie
After= attend que l’autre unité ait terminé son démarrage. La définition de « terminé son démarrage » dépend entièrement du Type= de cette unité.
Type=simple: dès que systemd a créé le processus avecfork. Le programme n’a peut-être pas encore analysé sa configuration, et encore moins ouvert un socket.Type=exec: dès queexecve()a réussi. C’est légèrement plus strict, mais cela ne dit toujours rien sur la disponibilité du service.Type=forking: lorsque le processus parent initial se termine.Type=oneshot: lorsque le processus se termine. Ici, « démarré » signifie réellement que le travail est terminé.Type=notify: lorsque le service envoieREADY=1sur son socket de notification. C’est le seul type qui signale une véritable disponibilité.
Ainsi, After= sur un daemon Type=simple constitue une promesse limitée. C’est la deuxième moitié de la condition de concurrence du premier exemple. Si l’unité dont vous dépendez est fournie avec Type=simple, l’ordonner après cette unité ne signifie pas qu’elle accepte déjà les connexions. Deux solutions sont possibles. Ordonnez plutôt le démarrage après son unité de socket. Le kernel mettra ainsi les connexions entrantes en file d’attente pendant le démarrage du daemon. Vous pouvez aussi faire réessayer votre propre service et laisser la stratégie de redémarrage s’en charger. Le type utilisé par une unité est visible dans systemctl cat. Consultez le paramètre Type= et ce que chaque valeur indique à systemd avant de vous fier à l’ordonnancement.
ExecStartPre peut empêcher l’activation de l’unité
ExecStartPre= s’exécute avant ExecStart=. S’il se termine avec un code différent de zéro, l’activation est annulée et l’unité passe à failed. ExecStart= ne s’exécute jamais. C’est la cause d’une grande partie des unités qui échouent sans message du programme lui-même, car le programme n’a jamais été démarré.
Points souvent méconnus :
- Ce n’est pas un shell. Il n’y a ni tubes, ni redirections, ni globbing, ni
&&. Le premier token doit être un chemin absolu. Encadrez la ligne avec/bin/sh -c '...'si vous avez besoin d’une syntaxe shell. - Le préfixe
-rend un code de sortie différent de zéro non bloquant :ExecStartPre=-/usr/bin/optional-check. - Chaque
ExecStartPre=doit se terminer avant l’exécution du suivant. Il ne peut pas lancer un processus qui s’exécute en continu. - Toutes les lignes
ExecStartPre=partagentTimeoutStartSec=avecExecStart=. Un contrôle préalable qui attend une base de données consomme le délai d’expiration du démarrage. L’unité échoue alors avecResult: timeoutaprès l’apparition destart operation timed out. Terminating.dans le journal.
La ligne d’erreur indique le processus de contrôle, pas le processus principal :
inventory-api.service: Control process exited, code=exited, status=2/INVALIDARGUMENT
inventory-api.service: Failed with result 'exit-code'.Lisez attentivement ce nom symbolique. systemd associe les petits codes de sortie à une table fixe. Ainsi, 2 affiche toujours INVALIDARGUMENT, quelle que soit la signification que le programme lui attribue. status=203/EXEC est celui qui fournit l’information réelle : systemd n’a pas pu exécuter le binaire, car le chemin est incorrect ou le fichier n’est pas exécutable.
N’utilisez pas ExecStartPre= pour créer des répertoires. RuntimeDirectory=, StateDirectory=, LogsDirectory= et CacheDirectory= les créent avec le propriétaire et le mode appropriés, et RuntimeDirectory= est supprimé à l’arrêt du service. Ils se comportent également correctement avec DynamicUser=, contrairement à un mkdir écrit manuellement.
Condition silencieuse, assertion explicite
Les familles Condition et Assert exécutent les mêmes tests. Elles diffèrent uniquement par le comportement adopté lorsqu’un test échoue.
Un Condition...= qui échoue ignore l’unité. Le job de démarrage est signalé comme réussi. L’unité reste inactive (dead), rien n’est marqué comme ayant échoué, aucune alerte n’est déclenchée et le journal contient une seule ligne :
Condition check resulted in Inventory API being skipped.Avec systemd 250 et les versions ultérieures, systemctl status affiche directement la raison :
Active: inactive (dead)
Condition: start condition unmet at Thu 2026-08-20 09:14:02 UTC; 2min agoLa ligne indentée en dessous indique la directive exacte qui a échoué, par exemple ConditionPathExists=/etc/inventory/api.conf was not met.
Un Assert...= qui échoue fait échouer l’unité. Le journal indique Assertion failed for Inventory API. et l’unité se termine dans l’état failed (Result: assert), ce qui permet aux outils de supervision de le détecter.
Choisissez entre les deux en vous demandant ce que signifie l’échec du test. Condition signifie « cette unité ne s’applique pas à cette machine ». Assert signifie « cette condition doit être vraie ; sinon, prévenez quelqu’un ». La plupart des unités utilisent Condition. Utilisez Assert uniquement lorsqu’une absence d’action silencieuse est plus problématique qu’une unité en échec.
La famille Condition comporte deux pièges.
Tout d’abord, une condition échouée ne fait pas échouer les unités qui en dépendent. Si a.service contient Requires=b.service et que b.service est ignorée à cause d’une condition, le job de démarrage de b.service est tout de même considéré comme terminé. a.service démarre donc normalement alors que b ne fonctionne pas. Une condition protège uniquement l’unité dans laquelle elle est définie.
Ensuite, les conditions sont évaluées à chaque démarrage de l’unité, au moment où le job s’exécute. Une unité déclenchée par un timer systemd sur un VPS peut être ignorée cent fois de suite sans jamais apparaître comme étant en échec. Il s’agit de la même catégorie de non-opération silencieuse qu’un job cron qui s’exécute sans rien faire. Vous la détectez de la même manière : consultez le journal de l’unité au lieu de vous fier à son état de sortie.
Les conditions à connaître sur un serveur :
ConditionPathExists=/etc/inventory/api.confet sa négationConditionPathExists=!/etc/inventory/api.conf.ConditionFileNotEmpty=etConditionDirectoryNotEmpty=, pour un fichier de configuration ou un répertoire de données qu’un package a créé mais laissé vide.ConditionVirtualization=, afin qu’une unité nécessitant une véritable interface du kernel puisse utiliserConditionVirtualization=!container. Vérifiez ce que votre machine indique avecsystemd-detect-virt.ConditionHost=compare le hostname ou l’ID de la machine. Cela permet à un même fichier d’unité partagé d’avoir un comportement différent sur deux serveurs.ConditionKernelCommandLine=etConditionKernelVersion=, pour les unités liées à un paramètre de boot ou à une version minimale du kernel.
Une affectation vide efface la liste. C’est ainsi qu’un drop-in supprime une condition fournie par un package :
[Unit]
ConditionPathExists=
ConditionPathExists=/srv/inventory/api.confPourquoi network.target ne signifie pas que le réseau est opérationnel
network.target est un point de synchronisation, pas un état. Au démarrage, être ordonné après cette cible signifie que le logiciel de gestion du réseau a été démarré. Cela ne signifie pas qu’une interface possède une adresse ni qu’une route vers Internet existe. Cette cible sert surtout dans l’autre sens : une unité ordonnée After=network.target est arrêtée avant l’arrêt du réseau lors de l’extinction.
network-online.target est la cible qui attend réellement. Elle s’appuie sur un service wait-online fourni par le gestionnaire de réseau utilisé :
systemd-networkd-wait-online.servicelorsque systemd-networkd gère les liens, ce qui est le cas habituel sur un serveur Ubuntu configuré avec netplan.NetworkManager-wait-online.serviceavec NetworkManager.
Les configurations ifupdown plus anciennes obtiennent le même comportement avec networking.service. Dans tous les cas, utiliser correctement cette cible nécessite deux lignes, pas une seule.
[Unit]
Wants=network-online.target
After=network-online.targetnetwork-online.target ne fait pas partie de la transaction de démarrage par défaut, et rien ne l’ajoute automatiquement. Si vous écrivez uniquement After=, vous ordonnez le démarrage par rapport à une unité qui n’a jamais été mise en file d’attente. Cet ordre ne produit donc aucun effet. C’est le no-op décrit plus haut, dans sa forme la plus coûteuse. La ligne Wants= ajoute la cible à la transaction, afin que la ligne After= ait une unité à attendre.
Le deuxième point à connaître est que la notion d’« opérationnel » est définie par l’implémentation de wait-online, et non par systemd. systemd-networkd-wait-online renvoie lorsque les liens qu’il gère atteignent un état configuré. Il ne vérifie pas que la résolution DNS fonctionne et ne vérifie pas qu’un hôte distant est joignable.
Cette définition provoque un problème fréquent sur les VPS. Une machine possédant une deuxième interface pour un réseau privé, déclarée dans netplan mais sans adresse, laisse le service wait-online attendre jusqu’à l’expiration du délai :
systemd-networkd-wait-online[612]: Timeout occurred while waiting for network connectivity.
systemd-networkd-wait-online.service: Failed with result 'exit-code'.Le démarrage prend 2 minutes supplémentaires, car le délai par défaut est de 120 secondes. Deux solutions sont possibles. Marquez l’interface inutilisée optional: true dans le fichier netplan, afin que networkd cesse de l’attendre. Vous pouvez aussi ajouter un drop-in au service wait-online, en indiquant le lien concerné avec --interface=, ou en lui passant --any pour qu’il renvoie dès qu’un lien est opérationnel.
Mieux encore, évitez d’avoir besoin de cette cible. De nombreux services sont ordonnés après network-online.target uniquement parce qu’ils se lient à une adresse précise et échouent au démarrage avec une ligne comme celle-ci :
nginx: [emerg] bind() to 203.0.113.10:443 failed (99: Cannot assign requested address)Le kernel refuse la liaison, car cette adresse n’est pas encore disponible. Définir net.ipv4.ip_nonlocal_bind=1 permet à un processus de se lier à une adresse que la machine ne possède pas encore, et une policy de redémarrage suffit pour le reste. Retarder tout le démarrage en attendant que le réseau soit opérationnel est une solution disproportionnée pour un problème qui concerne généralement un seul socket.
Comment lire les dépendances systemd réelles sur un serveur en fonctionnement
Ne vous basez jamais uniquement sur le fichier d’unité. Les drop-ins, les liens symboliques .wants/ et les dépendances implicites par défaut ajoutent des relations que le fichier n’affiche pas.
systemctl cat inventory-api.serviceCette commande affiche le fichier d’unité et tous les drop-ins, dans l’ordre dans lequel ils s’appliquent, avec le chemin source au-dessus de chaque bloc. Exécutez-la en premier. Un override de cinq lignes dans /etc/systemd/system/inventory-api.service.d/ prend le dessus sur le fichier fourni par le paquet et reste sinon invisible.
systemctl show inventory-api.service -p Requires -p Wants -p After -p Before -p ConditionResult -p AssertResultCette commande affiche les valeurs résolues, après l’application des drop-ins et l’ajout des dépendances implicites par systemd. ConditionResult=no répond directement à la question : « l’unité a indiqué qu’elle avait réussi, mais n’a rien fait ».
systemctl list-dependencies inventory-api.service
systemctl list-dependencies --reverse inventory-api.service
systemctl list-dependencies --after inventory-api.service
systemctl list-dependencies --before inventory-api.serviceLa forme simple parcourt Requires= et Wants= vers le bas. --reverse indique quelles unités démarrent la vôtre, ce qui permet de trouver la target qui la lance au démarrage. --after et --before affichent l’ordre de démarrage. C’est cette paire qu’il faut consulter pour déterminer si quelque chose a effectivement attendu.
journalctl -b -u inventory-api.service --no-pager
journalctl -b -o short-precise -u inventory-api.service -u postgresql.serviceLa deuxième commande entrelace deux unités avec des horodatages à la milliseconde. Elle permet de prouver une race dans l’ordre de démarrage au lieu de la supposer. L’échec pg_isready apparaît avant que PostgreSQL n’écrive database system is ready to accept connections dans les journaux, et l’écart entre les deux est visible directement dans la sortie.
systemd-analyze verify /etc/systemd/system/inventory-api.service
systemd-analyze critical-chain inventory-api.serviceverify charge l’unité comme le ferait systemd et signale les directives inconnues, les dépendances vers des unités inexistantes, les cycles d’ordre et la syntaxe qu’il ne peut pas analyser. Cette commande ne modifie rien sur le système. critical-chain affiche la chaîne d’ordre qui a retardé l’unité, avec l’heure à laquelle chaque étape est devenue active. Elle fonctionne uniquement pour une unité démarrée pendant le boot courant.
Après avoir modifié un fichier d’unité, exécutez sudo systemctl daemon-reload. Pour modifier une unité fournie par un paquet, utilisez sudo systemctl edit inventory-api.service, qui crée automatiquement un drop-in. Modifier le fichier du fournisseur sous /usr/lib/systemd/system/ fonctionne jusqu’à la prochaine mise à niveau du paquet, qui le remplace. Le même mécanisme de drop-in permet d’attribuer des limites de mémoire et de CPU à un service sans modifier un fichier géré par le paquet.
Cycles d’ordonnancement et trace laissée dans le journal
Ajoutez l’ordonnancement dans les deux directions et systemd casse la boucle en supprimant l’une des tâches :
systemd[1]: Found ordering cycle on inventory-api.service/start
systemd[1]: Job postgresql.service/start deleted to break ordering cycle starting with inventory-api.service/startsystemd choisit la tâche à supprimer, et ce n’est pas nécessairement celle que vous auriez choisie. Le résultat se manifeste par un service absent après certains redémarrages et présent après d’autres. Ce comportement est difficile à diagnostiquer depuis l’extérieur. La plupart des cycles proviennent d’unités qui définissent DefaultDependencies=no, puis ajoutent malgré tout une relation d’ordonnancement avec basic.target, ou de l’ajout de Before= à une unité qui contenait déjà After= et pointait vers votre unité. systemd-analyze verify permet de les détecter sans redémarrer.
L’unité fixe
[Unit]
Description=Inventory API
Wants=postgresql.service network-online.target
After=postgresql.service network-online.target
ConditionPathExists=/etc/inventory/api.conf
[Service]
Type=notify
StateDirectory=inventory
ExecStart=/usr/local/bin/inventory-api
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.targetChaque ligne a une fonction précise. Wants= ajoute les deux dépendances à la transaction sans lier le cycle de vie de cette unité au leur. After= gère l’attente et doit répéter les deux noms, car les dépendances et l’ordonnancement sont des paramètres distincts. ConditionPathExists= signifie qu’une machine qui possède le paquet mais pas la configuration ignore silencieusement l’unité au lieu de générer une alerte. C’est le comportement adapté à un service piloté par la configuration. Type=notify signifie que tout ce qui est ordonné après cette unité attend qu’elle soit réellement prête, et pas simplement qu’un processus ait effectué un fork. Restart=on-failure gère la disparition de la base de données longtemps après le démarrage, car l’ordonnancement ne s’applique qu’au premier démarrage. Le niveau d’agressivité de ces nouvelles tentatives est défini par les paramètres Restart= et RestartSec=.
Vérifiez l’unité avant de lui faire confiance :
sudo systemctl daemon-reload
systemd-analyze verify /etc/systemd/system/inventory-api.service
systemctl list-dependencies --after inventory-api.service
sudo systemctl start inventory-api.service
systemctl show inventory-api.service -p ConditionResult -p ActiveState -p ResultUne unité saine affiche ConditionResult=yes avec ActiveState=active, et Result=success confirme qu’aucune erreur ne s’est produite lors de la dernière exécution. ConditionResult=no avec ActiveState=inactive signifie que l’unité a été ignorée. La ligne du journal qui indique la condition vous précise quel test a échoué.
FAQ
L’option Requires= attend-elle le démarrage de l’autre unité ?
Non. Requires= et After= sont deux paramètres distincts. Requires= ajoute l’autre unité à la même transaction, puis systemd démarre les deux tâches en parallèle. Pour attendre, ajoutez After= en indiquant la même unité. Il existe une deuxième raison de l’ajouter : une dépendance Requires= qui échoue n’empêche votre unité de démarrer que si After= est également défini, car sans ordre de démarrage, votre unité est déjà démarrée lorsque l’autre échoue.
Faut-il ordonner le démarrage après network.target ou network-online.target ?
Au démarrage, network.target signifie seulement que le logiciel de gestion du réseau a été démarré. Il ne garantit donc ni la présence des adresses ni celle des routes. Utilisez network-online.target lorsque votre service a besoin d’une adresse fonctionnelle au démarrage, et indiquez à la fois Wants=network-online.target et After=network-online.target, car la target ne fait pas partie de la transaction de démarrage par défaut et After= seul attend une unité qui n’a été planifiée par personne. Si le service échoue uniquement parce qu’il se lie à une adresse IP précise, net.ipv4.ip_nonlocal_bind=1 avec Restart=on-failure est moins coûteux qu’un retard du démarrage.
Pourquoi mon unité signale-t-elle un succès alors qu’elle ne s’exécute jamais ?
Un test Condition...= échoué ignore l’unité et signale la tâche de démarrage comme réussie. Rien n’est donc jamais marqué comme échoué. Exécutez systemctl show <unit> -p ConditionResult, puis ConditionResult=no le confirme. Lisez ensuite journalctl -b -u <unit> pour trouver la ligne Condition check resulted in <description> being skipped. Avec systemd 250 et les versions ultérieures, systemctl status <unit> indique également la directive exacte qui n’a pas été satisfaite.
Quelle est la différence entre Condition et Assert ?
Ces deux paramètres exécutent les mêmes tests. Un Condition échoué ignore silencieusement l’unité et la tâche reste réussie. Un Assert échoué fait échouer l’unité, journalise Assertion failed for <description>. et la laisse dans l’état failed (Result: assert). Utilisez Condition lorsque « cette unité ne s’applique pas à cette machine », ce qui couvre presque tous les cas réels. Utilisez Assert uniquement lorsqu’une condition préalable manquante doit être visible par la personne qui surveille les unités en échec.
Pourquoi ExecStartPre échoue-t-il avec status=203/EXEC ?
203/EXEC signifie que systemd n’a pas pu exécuter la commande. Les causes habituelles sont un chemin qui n’est pas absolu, un binaire absent de la machine, un fichier dépourvu du bit d’exécution ou un script dont la ligne #! indique un interpréteur manquant. Les autres petits codes de systemd proviennent d’une table fixe. status=2/INVALIDARGUMENT signifie donc simplement que la commande s’est terminée avec le code 2 et ne donne aucune information sur les arguments. N’oubliez pas que ExecStartPre= n’est pas exécuté par un shell. Les pipes et les jokers nécessitent donc /bin/sh -c '...'.