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 de systemctl status.
    Description=Mon application Web Python personnalisée
    
  • Documentation : Une URL pointant vers la documentation du service (optionnel).
    Documentation=https://example.com/docs/mon-app
    
  • After : 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.target
    
  • Requires : 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.service
    
  • Wants : 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 que Requires.
    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 commande ExecStart est le processus principal du service. Systemd considère le service comme démarré immédiatement après l'invocation de ExecStart. Il s'attend à ce que le processus s'exécute indéfiniment au premier plan.
    • forking : La commande ExecStart cré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 commande ExecStart est 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 commande ExecStart est 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=simple
    
  • ExecStart : 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.py
    
  • ExecStop : La commande à exécuter lorsque le service est arrêté (optionnel). Si non spécifié, systemd envoie SIGTERM aux 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 $MAINPID
    
  • User : Le compte utilisateur sous lequel les processus du service s'exécuteront. Essentiel pour la sécurité ; évitez root sauf si absolument nécessaire.

    User=monappuser
    
  • Group : Le compte de groupe sous lequel les processus du service s'exécuteront.

    Group=monappgroup
    
  • WorkingDirectory : Le répertoire de travail pour les commandes exécutées.

    WorkingDirectory=/opt/mon_app
    
  • Restart : 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-failure
    
  • RestartSec : Combien de temps attendre avant de redémarrer le service (par exemple, 5s pour 5 secondes).

    RestartSec=5s
    
  • Environment : 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 être KEY=VALUE.

    EnvironmentFile=/etc/default/mon_app
    
  • LimitNOFILE : 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.target
    
  • RequiredBy : 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=journal et StandardError=journal pour diriger la sortie du service vers le journal systemd, ce qui facilite la visualisation des journaux avec journalctl.

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 :

  1. Modifiez /opt/mon_app/app.py ou /etc/systemd/system/mon_app.service.
  2. Si vous avez modifié le fichier d'unité, exécutez sudo systemctl daemon-reload.
  3. 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 dans ExecStart.
  • Gardez ExecStart direct. 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_app et de se lier à un port non privilégié ne doit pas s'exécuter en tant que root.
  • Rechargez après les modifications d'unité. sudo systemctl daemon-reload est 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/EXEC signifie 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 denied signifie 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 use signifie qu'un autre processus possède le port. Vérifiez avec sudo 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, WorkingDirectory et 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=journal et StandardError=journal pour diriger la sortie du service vers le journal systemd. Utilisez journalctl -u <nom_service> pour afficher les journaux.
  • Dépendances : Examinez attentivement After, Wants et Requires pour 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, LimitNPROC et MemoryMax lorsque le service a des limites ou des modes de défaillance connus.
  • Variables d'environnement : Utilisez Environment= ou EnvironmentFile= 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.