Comment écrire et gérer efficacement des fichiers d'unité systemd personnalisés
Maîtrisez l'art de gérer vos services Linux avec ce guide complet sur les fichiers d'unité systemd personnalisés. Apprenez à créer, configurer et dépanner les fichiers `.service`, en utilisant des directives cruciales comme `ExecStart`, `WantedBy` et `Type`. Cet article fournit des instructions étape par étape et des exemples pratiques, vous permettant de standardiser le démarrage des applications, d'assurer un fonctionnement fiable et d'intégrer vos processus personnalisés de manière transparente dans votre environnement système Linux. Essentiel pour les développeurs et administrateurs souhaitant une gestion robuste des services.
Comment écrire et gérer efficacement des fichiers d'unité systemd personnalisés
Les fichiers d'unité systemd personnalisés transforment une commande fonctionnant dans votre terminal en un service que le système d'exploitation peut démarrer, arrêter, redémarrer, journaliser et superviser. La différence est importante. Une commande dans un shell hérite de votre environnement et se termine lorsque la session se ferme. Un service a un utilisateur explicite, un répertoire de travail, une politique de redémarrage, des dépendances, des limites de ressources et des journaux.
Voici la voie pratique que j'utilise pour les petites API internes, les workers, les scripts sidecar et les démons ponctuels : écrire l'unité correcte la plus simple, l'exécuter en tant qu'utilisateur dédié, laisser les journaux aller vers le journal, et n'ajouter des directives avancées que lorsqu'un besoin réel apparaît.
Comprendre les fichiers d'unité Systemd
Systemd gère diverses ressources système, appelées unités, qui sont définies par des fichiers de configuration. Ces unités incluent les services (.service), les points de montage (.mount), les périphériques (.device), les sockets (.socket), etc. Pour gérer les applications et les processus en arrière-plan, le type d'unité .service est le plus courant et le plus pertinent.
Les fichiers d'unité systemd sont des fichiers texte brut généralement stockés dans des répertoires spécifiques. Les emplacements principaux, par ordre de priorité, sont :
/etc/systemd/system/: C'est l'emplacement recommandé pour les fichiers d'unité personnalisés et les surcharges, car ils prennent le pas sur les valeurs par défaut du système et persistent lors des mises à jour système./run/systemd/system/: Utilisé pour les fichiers d'unité générés à l'exécution./usr/lib/systemd/system/: Contient les fichiers d'unité fournis par les paquets installés. Ne modifiez pas les fichiers dans ce répertoire directement.
En plaçant vos fichiers d'unité personnalisés dans /etc/systemd/system/, vous vous assurez qu'ils sont correctement reconnus et gérés par systemd.
Anatomie d'un fichier d'unité .service
Un fichier d'unité .service systemd est structuré en plusieurs sections, chacune désignée par [NomSection], contenant diverses directives (paires clé-valeur). Les trois sections principales pour une unité de service sont [Unit], [Service] et [Install].
Détaillons les directives les plus cruciales que vous utiliserez :
Section [Unit]
Cette section contient des options génériques sur l'unité, sa description et ses dépendances.
Description: Une chaîne lisible par l'homme décrivant le service. Elle apparaît dans la sortie desystemctl status.Description=Mon application Web Python personnaliséeDocumentation: Une URL pointant vers la documentation du service (optionnel).Documentation=https://example.com/docs/mon-appAfter: Spécifie que cette unité doit démarrer après les unités listées. Cela aide à gérer l'ordre de démarrage. Pour les applications Web, vous voudrez peut-être vous assurer que le réseau est opérationnel.After=network.targetRequires: Une dépendance forte. Si l'unité requise échoue à démarrer, cette unité ne démarrera pas. Si l'unité requise est arrêtée, cette unité peut également être arrêtée.Requires=docker.serviceWants: Une dépendance plus faible. Si l'unité souhaitée échoue ou n'est pas trouvée, cette unité tente toujours de démarrer. C'est généralement une meilleure valeur par défaut queRequires.Wants=syslog.target
Section [Service]
Cette section définit les paramètres d'exécution de votre service, y compris comment il démarre, s'arrête et se comporte.
Type: Définit le type de démarrage du processus. Critique pour la façon dont systemd surveille votre service.simple(par défaut) : La commandeExecStartest le processus principal du service. Systemd considère le service comme démarré immédiatement après l'invocation deExecStart. Il s'attend à ce que le processus s'exécute indéfiniment au premier plan.forking: La commandeExecStartcrée un processus fils et le parent se termine. Systemd considère le service comme démarré une fois que le processus parent se termine. Utilisez ceci si votre application se démonise elle-même.oneshot: La commandeExecStartest un processus unique qui se termine lorsqu'il a terminé. Utile pour les scripts qui effectuent une tâche et se terminent (par exemple, un script de sauvegarde).notify: Similaire àsimple, mais le service indique à systemd quand il est prêt. Cela nécessite le support de l'application pour les notifications systemd.idle: La commandeExecStartest exécutée uniquement lorsque tous les travaux sont terminés, retardant l'exécution jusqu'à ce que le système soit presque inactif.
Type=simpleExecStart: La commande à exécuter lorsque le service démarre. C'est la directive la plus importante de cette section. Utilisez toujours le chemin absolu vers votre exécutable ou script.ExecStart=/usr/bin/python3 /opt/mon_app/app.pyExecStop: La commande à exécuter lorsque le service est arrêté (optionnel). Si non spécifié, systemd envoieSIGTERMaux processus.ExecStop=/usr/bin/pkill -f 'mon_app/app.py'ExecReload: La commande à exécuter pour recharger la configuration du service (optionnel).ExecReload=/bin/kill -HUP $MAINPIDUser: Le compte utilisateur sous lequel les processus du service s'exécuteront. Essentiel pour la sécurité ; évitezrootsauf si absolument nécessaire.User=monappuserGroup: Le compte de groupe sous lequel les processus du service s'exécuteront.Group=monappgroupWorkingDirectory: Le répertoire de travail pour les commandes exécutées.WorkingDirectory=/opt/mon_appRestart: Définit quand le service doit être automatiquement redémarré.no(par défaut) : Ne jamais redémarrer.on-success: Redémarrer uniquement si le service se termine proprement.on-failure: Redémarrer uniquement si le service se termine avec un code de sortie non nul ou est tué par un signal.always: Toujours redémarrer le service, quel que soit le statut de sortie.
Restart=on-failureRestartSec: Combien de temps attendre avant de redémarrer le service (par exemple,5spour 5 secondes).RestartSec=5sEnvironment: Définit les variables d'environnement pour les commandes exécutées.Environment="APP_ENV=production" "DEBUG=false"EnvironmentFile: Lit les variables d'environnement à partir d'un fichier. Chaque ligne doit êtreKEY=VALUE.EnvironmentFile=/etc/default/mon_appLimitNOFILE: Définit le nombre maximum de descripteurs de fichiers ouverts autorisés pour le service (par exemple,100000). Important pour les applications à forte concurrence.LimitNOFILE=65536
Section [Install]
Cette section définit comment le service est activé pour démarrer automatiquement au démarrage.
WantedBy: Spécifie l'unité cible qui "veut" ce service. Lorsque l'unité cible est activée, ce service sera lié symboliquement dans son répertoire.wants, le faisant effectivement démarrer avec la cible.multi-user.target: La cible standard pour la plupart des services serveur, indiquant un système avec des connexions multi-utilisateurs non graphiques.graphical.target: Pour les services nécessitant un environnement graphique.
WantedBy=multi-user.targetRequiredBy: Similaire àWantedBy, mais une dépendance plus forte. Si la cible est activée, cette unité est également activée, et si cette unité échoue, la cible échouera également.
Pour la plupart des services personnalisés destinés à s'exécuter en arrière-plan sur un serveur, Type=simple et WantedBy=multi-user.target sont le bon point de départ. Si l'application se démonise déjà, désactivez ce comportement ou utilisez Type=forking avec précaution. Un processus au premier plan est plus facile à superviser pour systemd.
Étape par étape : Créer et gérer un service systemd personnalisé
Créons un exemple pratique : un simple serveur HTTP Python qui sert des fichiers à partir d'un répertoire spécifié. Nous le configurerons en tant que service systemd.
Étape 1 : Préparer votre application/script
Tout d'abord, créez le script d'application. Pour cet exemple, nous utiliserons un simple serveur HTTP Python. Créez un répertoire pour votre application, par exemple /opt/mon_app, et placez app.py à l'intérieur.
# /opt/mon_app/app.py
import http.server
import socketserver
import os
PORT = int(os.environ.get("PORT", 8000))
DIRECTORY = os.environ.get("DIRECTORY", os.getcwd())
class Handler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=DIRECTORY, **kwargs)
print(f"Sert le répertoire {DIRECTORY} sur le port {PORT}")
with socketserver.TCPServer(("", PORT), Handler) as httpd:
print("Serveur démarré.")
httpd.serve_forever()
Créez le répertoire et le fichier :
sudo mkdir -p /opt/mon_app
sudo nano /opt/mon_app/app.py
(Collez le code Python)
Assurez-vous que le script est exécutable (optionnel pour la commande python3, mais bonne pratique) :
sudo chmod +x /opt/mon_app/app.py
Envisagez de créer un utilisateur dédié pour votre service pour des raisons de sécurité :
sudo useradd --system --no-create-home monappuser
Définissez la propriété appropriée pour votre répertoire d'application :
sudo chown -R monappuser:monappuser /opt/mon_app
Étape 2 : Créer le fichier d'unité
Maintenant, créez le fichier d'unité systemd pour notre application Python. Nous le nommerons mon_app.service.
sudo nano /etc/systemd/system/mon_app.service
Collez le contenu suivant :
# /etc/systemd/system/mon_app.service
[Unit]
Description=Mon serveur HTTP Python personnalisé
Documentation=https://github.com/example/mon_app
After=network.target
[Service]
Type=simple
User=monappuser
Group=monappuser
WorkingDirectory=/opt/mon_app
Environment="PORT=8080" "DIRECTORY=/var/www/html"
ExecStart=/usr/bin/python3 /opt/mon_app/app.py
Restart=on-failure
RestartSec=10s
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
Remarque : Nous avons défini
StandardOutput=journaletStandardError=journalpour diriger la sortie du service vers le journal systemd, ce qui facilite la visualisation des journaux avecjournalctl.
Si votre application a besoin de secrets, évitez de les mettre directement dans le fichier d'unité. Utilisez un fichier d'environnement avec des permissions restrictives, un gestionnaire de secrets ou un support d'identification spécifique à la distribution. Les fichiers d'unité sont souvent lisibles par plus de personnes que vous ne le pensez.
Étape 3 : Placer le fichier d'unité
Comme indiqué, nous avons placé le fichier d'unité dans /etc/systemd/system/. C'est là que les fichiers d'unité personnalisés doivent résider.
Étape 4 : Recharger le démon Systemd
Après avoir créé ou modifié un fichier d'unité, systemd doit être informé des modifications. Cela se fait en rechargeant le démon systemd :
sudo systemctl daemon-reload
Étape 5 : Démarrer le service
Vous pouvez maintenant démarrer votre service :
sudo systemctl start mon_app.service
Étape 6 : Vérifier l'état du service et les journaux
Vérifiez que votre service fonctionne correctement :
systemctl status mon_app.service
Exemple de sortie (tronquée) :
● mon_app.service - Mon serveur HTTP Python personnalisé
Loaded: loaded (/etc/systemd/system/mon_app.service; disabled; vendor preset: enabled)
Active: active (running) since Tue 2023-10-26 10:30:00 UTC; 5s ago
Docs: https://github.com/example/mon_app
Main PID: 12345 (python3)
Tasks: 1 (limit: 1100)
Memory: 6.5M
CPU: 45ms
CGroup: /system.slice/mon_app.service
└─12345 /usr/bin/python3 /opt/mon_app/app.py
Oct 26 10:30:00 yourhostname python3[12345]: Sert le répertoire /var/www/html sur le port 8080
Oct 26 10:30:00 yourhostname python3[12345]: Serveur démarré.
Pour afficher les journaux du service, utilisez journalctl :
journalctl -u mon_app.service -f
Cette commande affiche les journaux pour mon_app.service et -f (follow) affichera les nouveaux journaux en temps réel.
Vous pouvez également tester le serveur depuis votre navigateur ou curl sur http://localhost:8080 (en supposant que /var/www/html existe et contient des fichiers).
Étape 7 : Activer le service pour le démarrage automatique
Pour que votre service démarre automatiquement à chaque démarrage du système, vous devez l'activer :
sudo systemctl enable mon_app.service
Cette commande crée un lien symbolique de /etc/systemd/system/multi-user.target.wants/mon_app.service vers /etc/systemd/system/mon_app.service.
Étape 8 : Arrêter et désactiver le service
Pour arrêter un service en cours d'exécution :
sudo systemctl stop mon_app.service
Pour empêcher un service de démarrer automatiquement au démarrage (tout en le laissant activé pour être démarré manuellement) :
sudo systemctl disable mon_app.service
Si vous souhaitez supprimer complètement le service, disable-le d'abord, puis stop-le, et enfin supprimez le fichier .service de /etc/systemd/system/ et exécutez sudo systemctl daemon-reload.
Étape 9 : Mettre à jour un service
Si vous modifiez votre script app.py ou le fichier d'unité mon_app.service, vous devrez mettre à jour systemd et redémarrer le service :
- Modifiez
/opt/mon_app/app.pyou/etc/systemd/system/mon_app.service. - Si vous avez modifié le fichier d'unité, exécutez
sudo systemctl daemon-reload. - Redémarrez le service :
sudo systemctl restart mon_app.service.
Modèles plus sûrs pour les services réels
Une unité qui fonctionne n'est pas toujours une unité que vous souhaitez maintenir pendant des années. Ces modèles évitent les erreurs courantes :
- Exécutez au premier plan. Laissez systemd superviser le processus principal. Évitez
nohup,screen,tmux,&en arrière-plan ou les modes démon de l'application dansExecStart. - Gardez
ExecStartdirect. Si vous avez besoin de fonctionnalités shell telles que des pipes ou l'expansion de variables, appelez/bin/sh -c '...'intentionnellement. Sinon, exécutez l'exécutable directement. - Utilisez un utilisateur dédié. Un service qui n'a besoin que de lire
/opt/mon_appet de se lier à un port non privilégié ne doit pas s'exécuter en tant queroot. - Rechargez après les modifications d'unité.
sudo systemctl daemon-reloadest nécessaire lorsque le fichier d'unité change. - Séparez les déploiements de code des modifications d'unité. Si seul le code Python a changé, redémarrez le service. Si l'unité a changé, rechargez d'abord systemd.
Dépannage d'une nouvelle unité
Si le service échoue, commencez par :
systemctl status mon_app.service
journalctl -u mon_app.service -n 100 --no-pager
systemctl cat mon_app.service
Les échecs courants sont généralement simples :
status=203/EXECsignifie souvent que le chemin de l'exécutable est erroné, que le fichier est manquant ou que le fichier n'est pas exécutable.Permission deniedsignifie généralement que l'utilisateur du service ne peut pas lire un fichier, entrer dans un répertoire, écrire des journaux ou lier le port demandé.address already in usesignifie qu'un autre processus possède le port. Vérifiez avecsudo ss -tulpen | grep ':8080'.- Un service qui démarre manuellement mais échoue sous systemd dépend souvent de variables d'environnement, d'un répertoire de travail différent ou de fichiers dans votre répertoire personnel.
Vous pouvez tester la commande en tant qu'utilisateur du service :
sudo -u monappuser /usr/bin/python3 /opt/mon_app/app.py
Ce n'est pas une reproduction parfaite de l'environnement de systemd, mais cela détecte les erreurs évidentes de l'application avant de vous lancer dans les détails du fichier d'unité.
Une variante plus adaptée à la production
Pour un service interne de longue durée, j'ajouterais généralement quelques garde-fous :
[Unit]
Description=Mon serveur HTTP Python personnalisé
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=monappuser
Group=monappuser
WorkingDirectory=/opt/mon_app
EnvironmentFile=-/etc/mon_app/mon_app.env
ExecStart=/usr/bin/python3 /opt/mon_app/app.py
Restart=on-failure
RestartSec=10s
TimeoutStopSec=30s
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ReadWritePaths=/opt/mon_app /var/log/mon_app
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
ProtectSystem=full rend une grande partie du système en lecture seule pour le service, alors ajoutez ReadWritePaths= uniquement pour les répertoires dont l'application a réellement besoin d'écrire. Testez le durcissement une directive à la fois. Les options de sécurité sont utiles, mais un service qui ne peut pas lire sa configuration ou écrire ses données échouera au démarrage.
Bonnes pratiques et dépannage
- Chemins absolus : Utilisez toujours des chemins absolus pour
ExecStart,WorkingDirectoryet tous les autres chemins de fichiers dans votre fichier d'unité. Les chemins relatifs peuvent entraîner un comportement inattendu. - Utilisateurs dédiés : Exécutez les services sous des comptes d'utilisateurs non privilégiés et dédiés (par exemple,
monappuser) pour améliorer la sécurité et limiter les dommages potentiels en cas de compromission. - Journalisation claire : Utilisez
StandardOutput=journaletStandardError=journalpour diriger la sortie du service vers le journal systemd. Utilisezjournalctl -u <nom_service>pour afficher les journaux. - Dépendances : Examinez attentivement
After,WantsetRequirespour vous assurer que votre service démarre dans le bon ordre par rapport à ses dépendances (par exemple, réseau, bases de données). - Test des modifications : Avant d'activer un service pour qu'il démarre au démarrage, testez-le minutieusement en le démarrant et en l'arrêtant manuellement. Vérifiez son état et ses journaux.
- Limites de ressources : Utilisez des directives comme
LimitNOFILE,LimitNPROCetMemoryMaxlorsque le service a des limites ou des modes de défaillance connus. - Variables d'environnement : Utilisez
Environment=ouEnvironmentFile=pour les valeurs de configuration qui peuvent changer ou varier entre les environnements, plutôt que de les coder en dur dans le fichier d'unité ou le script. - Gestion des erreurs dans les scripts : Assurez-vous que vos scripts d'application gèrent les erreurs avec élégance. Un code de sortie non nul déclenchera
Restart=on-failure.
Avertissement : Évitez de modifier les fichiers d'unité directement dans
/usr/lib/systemd/system/. Toute modification sera probablement écrasée par les mises à jour des paquets. Utilisez/etc/systemd/system/pour les unités personnalisées ou les surcharges.
Une bonne unité personnalisée est ennuyeuse de la meilleure façon : la commande est explicite, l'utilisateur est non privilégié, le comportement de redémarrage est intentionnel et les journaux sont faciles à trouver. Une fois que cela est solide, les minuteries systemd, l'activation par socket et les contrôles cgroup plus profonds sont les prochaines étapes naturelles.