Systemd en profondeur#

Hide code cell source

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np
import seaborn as sns
import pandas as pd

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

Philosophie systemd#

Systemd est né en 2010 sous la plume de Lennart Poettering (Red Hat) comme réponse aux limitations de SysV init et Upstart. Sa philosophie centrale est la gestion déclarative : au lieu d’écrire des scripts shell complexes qui exécutent des actions dans un ordre précis, on déclare des unités avec leurs dépendances, et systemd se charge de satisfaire le graphe de dépendances de la façon la plus efficace possible — en parallèle quand c’est possible.

Principes fondamentaux#

Parallélisation maximale. SysV init démarre les services séquentiellement, en attendant que chaque script /etc/init.d/ se termine avant de passer au suivant. Systemd calcule le graphe de dépendances et démarre tous les services sans dépendances mutuelles simultanément. Sur un serveur moderne avec de nombreux services, le gain de temps au démarrage est considérable.

Activation à la demande. Systemd peut démarrer un service uniquement quand on y accède pour la première fois (socket activation, path activation, D-Bus activation). Le socket réseau est créé immédiatement ; les connexions entrantes attendent que le service soit prêt. Un service rarement utilisé ne consomme aucune ressource tant qu’il n’est pas sollicité.

Supervision continue. Systemd surveille en permanence les services qu’il a démarrés. Si un service plante, systemd peut le redémarrer automatiquement selon une politique configurable (Restart=on-failure, Restart=always…). Plus besoin de scripts de supervision externes comme monit ou supervisord pour les cas simples.

Journalisation centralisée. Tous les services envoient leurs logs à journald, qui les stocke dans un format binaire indexé. Plus de configuration syslog séparée pour chaque service ; un seul outil (journalctl) pour tout consulter, filtrer et exporter.

Pourquoi systemd est controversé

Systemd a suscité de vifs débats dans la communauté Linux. Les critiques pointent sa complexité, sa violation de la philosophie Unix (un seul programme qui fait beaucoup de choses), et la difficulté de le remplacer une fois installé. Les distributions qui maintiennent SysV init ou runit (Void Linux, Devuan, Alpine Linux) existent et ont leur public. Pour l’administration système professionnelle, systemd est cependant incontournable sur Debian, Ubuntu, RHEL, Fedora et leurs dérivés.

Unités et dépendances#

L’atome de base de systemd est l”unité (unit). Chaque unité est décrite par un fichier texte dont l’extension indique le type. Les unités sont reliées entre elles par des relations de dépendance :

  • Requires= : dépendance forte. Si l’unité requise échoue, cette unité s’arrête aussi.

  • Wants= : dépendance souple. Si l’unité voulue échoue, cette unité continue.

  • After= : ordre de démarrage. Cette unité démarre après celle listée.

  • Before= : ordre inverse.

  • Conflicts= : ne peut pas fonctionner en même temps que l’unité listée.

  • BindsTo= : dépendance forte bidirectionnelle.

Types d’unités#

Systemd reconnaît neuf types d’unités :

Emplacements des fichiers d’unité

Les fichiers d’unité installés par les paquets sont dans /usr/lib/systemd/system/. Les fichiers créés par l’administrateur vont dans /etc/systemd/system/, qui a la priorité. N’éditez jamais les fichiers dans /usr/lib/ : ils seraient écrasés à la prochaine mise à jour du paquet.

# Tableau des types d'unités systemd

import matplotlib.pyplot as plt
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

fig, ax = plt.subplots(figsize=(12, 5.5))
ax.axis("off")

data = [
    [".service", "Service", "Processus daemon : nginx, sshd, postgresql...", "La plus courante"],
    [".socket",  "Socket",  "Activation à la demande via socket réseau ou Unix", "Activation lazy"],
    [".target",  "Target",  "Groupe d'unités — équivalent du runlevel SysV", "multi-user.target"],
    [".timer",   "Timer",   "Déclenchement planifié — remplace cron", "Flexible, journalisé"],
    [".mount",   "Mount",   "Point de montage (remplace /etc/fstab)", "Intégré au cycle de vie"],
    [".device",  "Device",  "Périphérique noyau exposé par udev", "Automatique via udev"],
    [".path",    "Path",    "Activation sur changement de fichier/répertoire", "Remplace inotifywait"],
    [".scope",   "Scope",   "Groupe de processus externes (lancés hors systemd)", "Sessions PAM"],
    [".slice",   "Slice",   "Hiérarchie cgroup pour la gestion des ressources", "cpu.slice, user.slice"],
]

table = ax.table(
    cellText=data,
    colLabels=["Extension", "Type", "Rôle", "Note"],
    loc="center",
    cellLoc="left",
)
table.auto_set_font_size(False)
table.set_fontsize(9)
table.scale(1, 1.65)

palette = sns.color_palette("muted", 3)
for j in range(4):
    table[0, j].set_facecolor("#4878CF")
    table[0, j].set_text_props(color="white", fontweight="bold")

for i in range(1, len(data) + 1):
    bg = "#f0f4fb" if i % 2 == 0 else "#ffffff"
    for j in range(4):
        table[i, j].set_facecolor(bg)

ax.set_title("Types d'unités systemd", fontsize=12, fontweight="bold", pad=15)
plt.show()
_images/97f2e24fa7762ca80f10d5058307db0017e409c75747949809cf3b0cecd8d18a.png

Anatomie d’un fichier .service#

Un fichier .service est un fichier INI structuré en trois sections obligatoires : [Unit], [Service], et [Install].

Section [Unit]#

[Unit]
Description=Serveur web Nginx
Documentation=man:nginx(8) https://nginx.org/en/docs/
After=network.target network-online.target
Wants=network-online.target

Directive

Rôle

Description=

Description courte affichée par systemctl status

Documentation=

URLs ou pages man de référence

After=

Démarre après ces unités (ordre seul, pas dépendance)

Before=

Démarre avant ces unités

Requires=

Dépendance forte (arrêt en cascade si requise s’arrête)

Wants=

Dépendance souple (pas d’arrêt en cascade)

Conflicts=

Ne peut coexister avec ces unités

ConditionPathExists=

Démarre uniquement si le chemin existe

AssertFileNotEmpty=

Vérifie qu’un fichier n’est pas vide

Section [Service]#

[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s QUIT $MAINPID
TimeoutStopSec=5
KillMode=mixed
User=www-data
Group=www-data
Environment=NGINX_CONF=/etc/nginx/nginx.conf
Restart=on-failure
RestartSec=5s

Type de service (Type=) :

Valeur

Comportement

simple

Le processus lancé par ExecStart est le service (défaut)

exec

Comme simple, mais attend que execve() soit effectué

forking

Le processus père fork et se termine ; le fils devient le démon

oneshot

S’exécute une fois, puis est considéré comme arrêté

notify

Le service envoie une notification systemd quand il est prêt

dbus

Prêt quand le nom D-Bus est acquis

idle

Comme simple, mais attend que tous les jobs soient terminés

Directives de supervision :

Directive

Valeurs

Rôle

Restart=

no, on-success, on-failure, on-abnormal, always

Quand redémarrer

RestartSec=

durée

Délai avant redémarrage

StartLimitIntervalSec=

durée

Fenêtre de comptage des redémarrages

StartLimitBurst=

nombre

Nombre max de redémarrages dans la fenêtre

Sécurité et isolation :

# Directives de durcissement dans [Service]
NoNewPrivileges=yes          # interdit les élévations de privilèges
ProtectSystem=strict         # monte /usr et /boot en read-only
ProtectHome=yes              # cache /home, /root, /run/user
PrivateTmp=yes               # /tmp privé et isolé
PrivateDevices=yes           # accès limité à /dev
ReadWritePaths=/var/lib/myapp

Durcissement des services systemd

Les directives de sécurité de systemd (ProtectSystem=, PrivateTmp=, NoNewPrivileges=) constituent une première ligne de défense en cas de compromission d’un service. Un service Nginx correctement configuré ne devrait jamais pouvoir lire /home/ ni écrire dans /usr/. Utilisez systemd-analyze security <service> pour obtenir un score de sécurité et des recommandations.

Section [Install]#

[Install]
WantedBy=multi-user.target

Directive

Rôle

WantedBy=

Quand systemctl enable est utilisé, crée un lien symbolique dans <target>.wants/

RequiredBy=

Idem mais crée un lien dans <target>.requires/

Alias=

Noms alternatifs pour l’unité

Also=

Unités à activer/désactiver en même temps

Gestion des services avec systemctl#

enable vs start

systemctl enable crée un lien symbolique pour le démarrage automatique mais ne démarre pas le service immédiatement. systemctl start démarre le service maintenant mais ne l’active pas au prochain démarrage. Dans la pratique, utilisez presque toujours systemctl enable --now pour faire les deux en une commande.

Commandes essentielles#

# État d'un service
systemctl status nginx

# Démarrer / arrêter / redémarrer
systemctl start nginx
systemctl stop nginx
systemctl restart nginx

# Recharger la configuration sans redémarrer le processus
systemctl reload nginx

# Redémarrer uniquement si en cours d'exécution
systemctl try-restart nginx

# Activer au démarrage (crée le lien symbolique dans .wants/)
systemctl enable nginx

# Activer ET démarrer immédiatement
systemctl enable --now nginx

# Désactiver
systemctl disable nginx

# Masquer (empêche même le démarrage manuel)
systemctl mask nginx

# Démasquer
systemctl unmask nginx

Recharger la configuration systemd#

daemon-reload est obligatoire

Chaque fois que vous créez ou modifiez un fichier .service, vous devez exécuter systemctl daemon-reload avant que systemd prenne en compte les changements. Sans cette commande, systemd continue d’utiliser la version en cache du fichier. Cette étape est fréquemment oubliée et cause des comportements inattendus.

# Après toute modification de fichier .service
systemctl daemon-reload

# Lister toutes les unités (actives)
systemctl list-units

# Lister toutes les unités (y compris inactives)
systemctl list-units --all

# Lister uniquement les services
systemctl list-units --type=service

# Lister les unités en échec
systemctl list-units --state=failed

# Dépendances d'une unité
systemctl list-dependencies nginx

Diagnostiquer un service en échec

Quand systemctl list-units --state=failed retourne des unités, la procédure de diagnostic est : systemctl status <service> pour voir le code de sortie et les derniers logs, puis journalctl -u <service> -n 50 pour consulter les 50 dernières lignes de logs. Le code de retour du processus (Main process exited, code=exited, status=1/FAILURE) et le message d’erreur donnent généralement la cause exacte.

# Simulation d'une sortie de systemctl list-units --all
# Données représentatives d'un serveur Debian

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

units_data = {
    "unit": [
        "nginx.service", "sshd.service", "postgresql.service",
        "cron.service", "rsyslog.service", "NetworkManager.service",
        "bluetooth.service", "cups.service", "avahi-daemon.service",
        "snapd.service", "apt-daily.service", "fwupd.service",
        "ufw.service", "fail2ban.service", "redis.service",
        "memcached.service", "docker.service", "containerd.service",
        "thermald.service", "tlp.service",
    ],
    "load": ["loaded"] * 20,
    "active": [
        "active", "active", "active", "active", "active",
        "active", "inactive", "inactive", "active", "active",
        "inactive", "active", "active", "active", "active",
        "inactive", "active", "active", "active", "active",
    ],
    "sub": [
        "running", "running", "running", "running", "running",
        "running", "dead", "dead", "running", "running",
        "dead", "running", "exited", "running", "running",
        "dead", "running", "running", "running", "running",
    ],
    "description": [
        "A high performance web server", "OpenBSD Secure Shell server",
        "PostgreSQL Database", "Regular background program processing daemon",
        "System Logging Service", "Network Manager",
        "Bluetooth service", "CUPS Scheduler",
        "Avahi mDNS/DNS-SD Stack", "Snap Daemon",
        "Daily apt download activities", "Firmware update daemon",
        "Uncomplicated firewall", "Fail2Ban Service",
        "Advanced key-value store", "memcached daemon",
        "Docker Application Container Engine", "containerd container runtime",
        "Thermal Daemon Service", "TLP system startup/shutdown",
    ],
}

df_units = pd.DataFrame(units_data)

print(f"Total d'unités : {len(df_units)}")
print(f"\n=== Répartition par état ===")
print(df_units.groupby("active")["unit"].count().to_string())
print(f"\n=== Répartition par sous-état ===")
print(df_units.groupby("sub")["unit"].count().to_string())
print(f"\n=== Services inactifs ===")
inactifs = df_units[df_units.active == "inactive"]
for _, row in inactifs.iterrows():
    print(f"  {row['unit']:35s} {row['description']}")
Total d'unités : 20

=== Répartition par état ===
active
active      16
inactive     4

=== Répartition par sous-état ===
sub
dead        4
exited      1
running    15

=== Services inactifs ===
  bluetooth.service                   Bluetooth service
  cups.service                        CUPS Scheduler
  apt-daily.service                   Daily apt download activities
  memcached.service                   memcached daemon
# Visualisation de l'état des unités

import matplotlib.pyplot as plt
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

fig, axes = plt.subplots(1, 2, figsize=(12, 4.5))
palette = sns.color_palette("muted", 5)

# Répartition active/inactive
active_counts = df_units["active"].value_counts()
colors_active = {"active": palette[0], "inactive": palette[2]}
axes[0].bar(active_counts.index,
            active_counts.values,
            color=[colors_active.get(k, palette[3]) for k in active_counts.index],
            edgecolor="none")
axes[0].set_title("Services par état (active/inactive)")
axes[0].set_ylabel("Nombre d'unités")
axes[0].spines[["top", "right"]].set_visible(False)
for i, (_, val) in enumerate(active_counts.items()):
    axes[0].text(i, val + 0.1, str(val), ha="center", va="bottom", fontsize=11, fontweight="bold")

# Répartition par sous-état
sub_counts = df_units["sub"].value_counts()
colors_sub = {
    "running": palette[0], "exited": palette[1],
    "dead": palette[2], "failed": palette[3],
}
axes[1].barh(sub_counts.index,
             sub_counts.values,
             color=[colors_sub.get(k, palette[4]) for k in sub_counts.index],
             edgecolor="none")
axes[1].set_title("Services par sous-état (sub-state)")
axes[1].set_xlabel("Nombre d'unités")
axes[1].spines[["top", "right"]].set_visible(False)
for i, (label, val) in enumerate(sub_counts.items()):
    axes[1].text(val + 0.1, i, str(val), va="center", fontsize=10)

plt.suptitle("systemctl list-units --all — état des services", fontsize=12, fontweight="bold")
plt.show()
_images/c25f1ebc795ffe63a83221d6e2990a821ffb556e0c6a82300e1966574b1aceeb.png

Targets#

Concept de target#

Une target est une unité de type .target dont le seul rôle est de regrouper d’autres unités. Elle représente un état du système. Quand systemd active multi-user.target, il active récursivement toutes les unités dont multi-user.target dépend (via Wants=, Requires=).

# Voir la target par défaut
systemctl get-default
# graphical.target

# Changer la target par défaut
systemctl set-default multi-user.target

# Passer à une autre target sans redémarrer (équivalent changement de runlevel)
systemctl isolate rescue.target

# Lister toutes les targets
systemctl list-units --type=target --all

Targets importantes#

# Tableau des targets systemd

import matplotlib.pyplot as plt
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

fig, ax = plt.subplots(figsize=(12, 4.5))
ax.axis("off")

data_targets = [
    ["poweroff.target",    "0", "Arrêt complet du système",                 "shutdown -h now"],
    ["rescue.target",      "1", "Shell root mono-utilisateur, maintenance",  "systemctl isolate rescue.target"],
    ["multi-user.target",  "3", "Multi-utilisateurs, réseau, sans GUI",      "Servers par défaut"],
    ["graphical.target",   "5", "Multi-utilisateurs avec interface graphique","Desktops par défaut"],
    ["reboot.target",      "6", "Redémarrage du système",                    "shutdown -r now"],
    ["emergency.target",   "—", "Shell d'urgence avant montage de /",       "kernel: systemd.unit=emergency"],
    ["sleep.target",       "—", "Suspension du système",                    "systemctl suspend"],
    ["hibernate.target",   "—", "Hibernation sur disque",                   "systemctl hibernate"],
]

table = ax.table(
    cellText=data_targets,
    colLabels=["Target", "SysV équiv.", "Description", "Exemple d'utilisation"],
    loc="center",
    cellLoc="left",
)
table.auto_set_font_size(False)
table.set_fontsize(9)
table.scale(1, 1.7)

for j in range(4):
    table[0, j].set_facecolor("#4878CF")
    table[0, j].set_text_props(color="white", fontweight="bold")

for i in range(1, len(data_targets) + 1):
    bg = "#f0f4fb" if i % 2 == 0 else "#ffffff"
    for j in range(4):
        table[i, j].set_facecolor(bg)

ax.set_title("Targets systemd et équivalents SysV", fontsize=12, fontweight="bold", pad=15)
plt.show()
_images/9a97023d6d6e5f38420e2d4ecdb1a380d6dbbe6fe8726d35705b7fce2a9aaa30.png

Graphe de dépendances des targets#

# Graphe de dépendances des targets systemd (simplifié)

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

palette = sns.color_palette("muted", 5)

# Nœuds : (nom, x, y, couleur)
nodes = {
    "graphical.target":       (0.5,  0.9, palette[0]),
    "multi-user.target":      (0.5,  0.7, palette[1]),
    "basic.target":           (0.5,  0.5, palette[2]),
    "sysinit.target":         (0.25, 0.3, palette[3]),
    "network.target":         (0.75, 0.5, palette[4]),
    "network-online.target":  (0.75, 0.3, palette[4]),
    "local-fs.target":        (0.25, 0.1, palette[3]),
    "swap.target":            (0.5,  0.1, palette[3]),
}

# Arêtes (de → vers : "A dépend de B")
edges = [
    ("graphical.target",      "multi-user.target"),
    ("multi-user.target",     "basic.target"),
    ("multi-user.target",     "network.target"),
    ("basic.target",          "sysinit.target"),
    ("basic.target",          "local-fs.target"),
    ("network.target",        "network-online.target"),
    ("sysinit.target",        "local-fs.target"),
    ("sysinit.target",        "swap.target"),
]

fig, ax = plt.subplots(figsize=(9, 7))
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.axis("off")

for src, dst in edges:
    x0, y0 = nodes[src][:2]
    x1, y1 = nodes[dst][:2]
    ax.annotate("", xy=(x1, y1 + 0.045), xytext=(x0, y0 - 0.045),
                arrowprops=dict(arrowstyle="->", color="#888888",
                                lw=1.3, connectionstyle="arc3,rad=0.0"))

for name, (x, y, color) in nodes.items():
    ax.add_patch(mpatches.FancyBboxPatch((x - 0.14, y - 0.04), 0.28, 0.08,
                                    boxstyle="round,pad=0.01",
                                    facecolor=color, edgecolor="white", linewidth=1.5,
                                    zorder=3))
    ax.text(x, y, name, ha="center", va="center", fontsize=8.5,
            color="white", fontweight="bold", zorder=4)

legend_items = [
    mpatches.Patch(color=palette[0], label="Target graphique"),
    mpatches.Patch(color=palette[1], label="Target multi-utilisateur"),
    mpatches.Patch(color=palette[2], label="Target basique"),
    mpatches.Patch(color=palette[3], label="Targets de système de fichiers/init"),
    mpatches.Patch(color=palette[4], label="Targets réseau"),
]
ax.legend(handles=legend_items, loc="lower right", fontsize=8.5)
ax.set_title("Graphe de dépendances des targets systemd (simplifié)", fontsize=11)
plt.show()
_images/12e0cb4018dfcbd2d7190006b0d8fa3020ad1c43bb7c8dcccc98ee31aed44d81.png

Systemd timers#

Timers vs cron#

Les timers systemd remplacent avantageusement cron pour les tâches planifiées sur un système utilisant systemd :

  • Journalisation : les sorties des timers sont capturées par journald et consultables avec journalctl.

  • Dépendances : un timer peut dépendre d’une connexion réseau, d’un montage NFS, etc.

  • Précision : résolution à la milliseconde, contrairement à la minute de cron.

  • Rattrapage : directive Persistent=yes pour rattraper les exécutions manquées si la machine était éteinte.

  • Randomisation : RandomizedDelaySec= pour répartir la charge sur une flotte de serveurs.

# Comparaison cron vs systemd timer

import matplotlib.pyplot as plt
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

fig, ax = plt.subplots(figsize=(12, 5.5))
ax.axis("off")

data_cmp = [
    ["Syntaxe",          "5 2 * * 1-5   # lun-ven à 2h05", "OnCalendar=Mon..Fri 02:05:00"],
    ["Journalisation",   "Sortie perdue si pas de MAILTO", "journald — consultable avec journalctl"],
    ["Dépendances",      "Aucune", "After=network-online.target, etc."],
    ["Rattrapage",       "Non (exécution manquée perdue)", "Persistent=yes — rattrape les exécutions"],
    ["Randomisation",    "Non", "RandomizedDelaySec=300"],
    ["Monitoring",       "Aucun intégré", "systemctl list-timers, systemctl status"],
    ["Environnement",    "Minimal (pas de profil user)", "Configurable via Environment=, EnvironmentFile="],
    ["Gestion multi-user","Crontab par utilisateur", "Unités système ou utilisateur"],
    ["Précision",        "Minute (minimum)", "Secondes (OnBootSec=30s)"],
    ["Retry en cas d'échec", "Non", "Restart=on-failure dans le .service associé"],
]

table = ax.table(
    cellText=data_cmp,
    colLabels=["Critère", "Cron", "Systemd Timer"],
    loc="center",
    cellLoc="left",
)
table.auto_set_font_size(False)
table.set_fontsize(8.5)
table.scale(1, 1.62)

palette = sns.color_palette("muted", 2)
for j in range(3):
    table[0, j].set_facecolor("#4878CF")
    table[0, j].set_text_props(color="white", fontweight="bold")

for i in range(1, len(data_cmp) + 1):
    table[i, 0].set_facecolor("#e8ecf4")
    table[i, 0].set_text_props(fontweight="bold", fontsize=8)
    table[i, 1].set_facecolor("#fff4f4")
    table[i, 2].set_facecolor("#f4fff4")

ax.set_title("Cron vs Systemd Timer — comparaison", fontsize=12, fontweight="bold", pad=15)
plt.show()
_images/f40c772a047361847112d6503f9ca3ebffe291bb43f9d814329b716c1c1d85d8.png

Anatomie d’un timer systemd#

Un timer systemd se compose toujours de deux fichiers : un .timer et un .service de même nom (le timer active le service associé) :

# /etc/systemd/system/backup-db.timer
[Unit]
Description=Timer de sauvegarde base de données
Requires=backup-db.service

[Timer]
OnCalendar=*-*-* 03:00:00
RandomizedDelaySec=300
Persistent=yes

[Install]
WantedBy=timers.target
# /etc/systemd/system/backup-db.service
[Unit]
Description=Sauvegarde base de données
After=postgresql.service

[Service]
Type=oneshot
User=postgres
ExecStart=/usr/local/bin/backup-db.sh
StandardOutput=journal
StandardError=journal

Syntaxe OnCalendar#

# Exemples de valeurs OnCalendar
OnCalendar=daily               # tous les jours à 00:00:00
OnCalendar=weekly              # chaque lundi à 00:00:00
OnCalendar=monthly             # premier jour du mois à 00:00:00
OnCalendar=hourly              # chaque heure au début
OnCalendar=Mon..Fri 08:00:00   # du lundi au vendredi à 8h
OnCalendar=*-*-* 02:30:00      # tous les jours à 2h30
OnCalendar=2025-01-01 00:00:00 # une seule fois
OnCalendar=*:0/15              # toutes les 15 minutes

# OnBootSec — délai depuis le démarrage
OnBootSec=30s
OnBootSec=5min

# Vérifier l'interprétation d'une expression
systemd-analyze calendar "Mon..Fri 08:00:00"

# Lister tous les timers actifs
systemctl list-timers
# Activer un timer
systemctl enable --now backup-db.timer

# Voir le prochain déclenchement
systemctl list-timers backup-db.timer
# NEXT                        LEFT       LAST                        PASSED  UNIT
# Tue 2025-03-25 03:00:00 CET 14h left   Mon 2025-03-24 03:04:12 CET 9h ago  backup-db.timer

Journald et journalctl#

Architecture de journald#

systemd-journald collecte les logs de toutes les sources :

  • Les messages des services systemd (stdout/stderr)

  • Les messages des noyau (dmesg)

  • Les messages syslog (via le socket /dev/log)

  • Les messages via l’API native (sd_journal_print())

Les logs sont stockés dans un format binaire dans /run/log/journal/ (volatile, perdu au redémarrage) ou /var/log/journal/ (persistant si configuré).

Activer la persistance du journal

Par défaut sur certaines distributions, les logs journald sont volatiles. Pour les rendre persistants entre les redémarrages, créez le répertoire /var/log/journal/ et redémarrez journald :

mkdir -p /var/log/journal
systemd-tmpfiles --create --prefix /var/log/journal
systemctl restart systemd-journald

Configurez la taille maximale dans /etc/systemd/journald.conf : SystemMaxUse=500M.


```{admonition} Rotation et rétention des logs journald
:class: note
Journald gère automatiquement la rotation des logs. Les paramètres clés dans `/etc/systemd/journald.conf` : `SystemMaxUse=` (taille disque maximale), `SystemKeepFree=` (espace disque minimal à conserver), `MaxRetentionSec=` (durée maximale de rétention), `MaxFileSec=` (durée d'un fichier journal avant rotation). Pour purger manuellement : `journalctl --vacuum-size=500M` ou `journalctl --vacuum-time=2weeks`.

Utilisation de journalctl#

# Voir tous les logs (depuis le démarrage en cours)
journalctl

# Logs d'une unité spécifique
journalctl -u nginx.service
journalctl -u nginx -u postgresql    # plusieurs unités

# Suivre les logs en temps réel
journalctl -u nginx -f

# Filtrer par priorité (0=emerg → 7=debug)
journalctl -p err                    # erreurs et plus grave
journalctl -p warning..err           # entre warning et err

# Filtrer par période
journalctl --since "2025-03-01"
journalctl --since "2025-03-01 08:00" --until "2025-03-01 18:00"
journalctl --since "1 hour ago"

# Logs du démarrage en cours / précédents
journalctl -b                        # démarrage courant
journalctl -b -1                     # démarrage précédent
journalctl --list-boots              # liste des démarrages

# Format de sortie
journalctl -o json                   # JSON
journalctl -o json-pretty            # JSON indenté
journalctl -o cat                    # messages seuls (sans métadonnées)
journalctl -o verbose                # tous les champs

# Filtrer par champ
journalctl _SYSTEMD_UNIT=nginx.service _PRIORITY=3

# Logs du noyau
journalctl -k

# Utilisation du disque
journalctl --disk-usage
# Parse simulé de journalctl -o json — statistiques de logs par priorité

import json
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

# Données simulées représentatives d'un serveur de production
# (24h de logs pour un serveur avec nginx, postgresql, sshd, cron)
np.random.seed(42)

priority_labels = {
    0: "emerg",
    1: "alert",
    2: "crit",
    3: "err",
    4: "warning",
    5: "notice",
    6: "info",
    7: "debug",
}

# Distribution réaliste des priorités
priority_counts = {
    0: 0,
    1: 0,
    2: 3,
    3: 47,
    4: 312,
    5: 1843,
    6: 28654,
    7: 4127,
}

# Simuler les données par service
services = ["nginx.service", "postgresql.service", "sshd.service",
            "cron.service", "NetworkManager.service", "kernel"]
service_data = []

for service in services:
    for prio, label in priority_labels.items():
        if prio <= 3:
            count = np.random.randint(0, 10)
        elif prio == 4:
            count = np.random.randint(5, 60)
        elif prio == 5:
            count = np.random.randint(50, 500)
        elif prio == 6:
            count = np.random.randint(500, 8000)
        else:
            count = np.random.randint(100, 2000)
        service_data.append({"service": service, "priority": prio,
                              "label": label, "count": count})

df_logs = pd.DataFrame(service_data)

# Statistiques globales
totaux = pd.DataFrame(list(priority_counts.items()),
                      columns=["priority", "total"])
totaux["label"] = totaux["priority"].map(priority_labels)
totaux_nonzero = totaux[totaux.total > 0]

print("=== Distribution des logs par priorité (24h) ===")
for _, row in totaux.iterrows():
    bar = "█" * min(int(row["total"] / 1000), 30)
    print(f"  {row['priority']} {row['label']:10s} : {row['total']:6d}  {bar}")
=== Distribution des logs par priorité (24h) ===
  0 emerg      :      0  
  1 alert      :      0  
  2 crit       :      3  
  3 err        :     47  
  4 warning    :    312  
  5 notice     :   1843  █
  6 info       :  28654  ████████████████████████████
  7 debug      :   4127  ████
import matplotlib.pyplot as plt
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

fig, axes = plt.subplots(1, 2, figsize=(13, 5))
palette = sns.color_palette("muted", len(priority_counts))

# Barplot par priorité globale
totaux_nonzero = totaux[totaux.total > 0].copy()
colors_prio = {
    "crit":    "#D65F5F",
    "err":     "#E09F3E",
    "warning": "#B47CC7",
    "notice":  "#6ACC65",
    "info":    "#4878CF",
    "debug":   "#C4AD66",
}
bar_colors = [colors_prio.get(row["label"], "#999999")
              for _, row in totaux_nonzero.iterrows()]

axes[0].bar(totaux_nonzero["label"], totaux_nonzero["total"],
            color=bar_colors, edgecolor="none")
axes[0].set_yscale("log")
axes[0].set_title("Volume de logs par priorité (échelle log)")
axes[0].set_ylabel("Nombre de messages (log)")
axes[0].set_xlabel("Priorité (syslog)")
axes[0].spines[["top", "right"]].set_visible(False)
for i, (_, row) in enumerate(totaux_nonzero.iterrows()):
    axes[0].text(i, row["total"] * 1.3, f"{int(row['total']):,}",
                ha="center", va="bottom", fontsize=8)

# Heatmap par service et priorité
pivot = df_logs.pivot_table(values="count", index="service",
                            columns="label", aggfunc="sum", fill_value=0)
col_order = ["debug", "info", "notice", "warning", "err", "crit"]
col_order_present = [c for c in col_order if c in pivot.columns]
pivot = pivot[col_order_present]

sns.heatmap(pivot, ax=axes[1], cmap="Blues", fmt="d", annot=True,
            linewidths=0.5, cbar_kws={"label": "Messages"}, annot_kws={"fontsize": 7})
axes[1].set_title("Logs par service et priorité")
axes[1].set_xlabel("Priorité")
axes[1].set_ylabel("")
axes[1].tick_params(axis="x", rotation=30)
axes[1].tick_params(axis="y", rotation=0)

plt.suptitle("journalctl — analyse des logs sur 24h", fontsize=12, fontweight="bold")
plt.show()
_images/af330bc4990afa0f918a7d6dc2a448e59c2980ba497c778507a3016eca0c7a3b.png

Override de services#

Drop-in files#

Systemd permet de surcharger la configuration d’un service sans modifier le fichier d’origine (ce qui serait écrasé par les mises à jour). La méthode recommandée est d’utiliser des drop-in files : des fichiers .conf placés dans un répertoire spécifique.

Cas d’usage typiques des drop-ins

Les drop-in files sont utilisés pour : augmenter LimitNOFILE= pour les services qui ouvrent beaucoup de fichiers (nginx, PostgreSQL), ajouter des variables d’environnement sans modifier le fichier du paquet, ajouter une dépendance (After=, Requires=) à un service tiers, ou modifier la politique de redémarrage (Restart=, RestartSec=).

# Créer un drop-in avec systemctl edit (crée le répertoire et ouvre l'éditeur)
systemctl edit nginx.service
# Crée /etc/systemd/system/nginx.service.d/override.conf

# Pour remplacer complètement le fichier (pas un drop-in)
systemctl edit --full nginx.service
# Crée /etc/systemd/system/nginx.service

Structure d’un drop-in#

# /etc/systemd/system/nginx.service.d/override.conf

[Service]
# Augmenter la limite de descripteurs de fichiers
LimitNOFILE=65536

# Ajouter une variable d'environnement
Environment=NGINX_WORKER_PROCESSES=auto

# Remplacer ExecStart (vider d'abord, puis redéfinir)
ExecStart=
ExecStart=/usr/sbin/nginx -g 'daemon off;'

Vider une directive avant de la redéfinir

Certaines directives comme ExecStart= sont additives : si vous ajoutez simplement une ligne dans le drop-in, le service aura deux ExecStart. Pour remplacer une telle directive, il faut d’abord l’effacer avec une ligne vide (ExecStart=), puis la redéfinir. Cette règle s’applique à ExecStart, ExecStartPre, ExecStop, ExecStopPost.

# Afficher la configuration effective (unité + tous les drop-ins)
systemctl cat nginx.service

# Afficher les propriétés actuelles
systemctl show nginx.service

# Afficher les propriétés actuelles pour une seule
systemctl show nginx.service --property=Restart

# Appliquer les modifications
systemctl daemon-reload
systemctl restart nginx.service

Hiérarchie des fichiers d’unité#

Systemd cherche les fichiers d’unité dans cet ordre de priorité (du plus prioritaire au moins) :

/etc/systemd/system/          → configuration locale (prioritaire)
/run/systemd/system/          → configuration générée à l'exécution
/usr/lib/systemd/system/      → unités installées par les paquets

Unités utilisateur vs système

En plus des unités système, chaque utilisateur peut gérer ses propres unités dans ~/.config/systemd/user/. Ces unités tournent dans une instance systemd par session utilisateur et démarrent à la connexion. Utile pour les services personnels qui n’ont pas besoin de privilèges root. Gestion via systemctl --user start/enable/status <service>.

Écrire un service systemd complet#

Exemple : service de backup Python#

Voici un exemple complet d’un service systemd pour un script Python de sauvegarde, illustrant les bonnes pratiques.

Le script Python (/usr/local/bin/backup-databases.py) :

#!/usr/bin/env python3
"""Script de sauvegarde PostgreSQL avec journalisation systemd."""

import subprocess
import logging
import sys
import os
from datetime import datetime
from pathlib import Path

# Logging vers stdout/stderr → capturé par journald
logging.basicConfig(
    stream=sys.stdout,
    level=logging.INFO,
    format="%(levelname)s: %(message)s",
)
log = logging.getLogger(__name__)

BACKUP_DIR = Path(os.environ.get("BACKUP_DIR", "/var/backups/postgresql"))
RETENTION_DAYS = int(os.environ.get("RETENTION_DAYS", "7"))
DATABASES = os.environ.get("DATABASES", "").split(",")

def backup_database(dbname: str) -> Path:
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    outfile = BACKUP_DIR / f"{dbname}_{timestamp}.sql.gz"
    BACKUP_DIR.mkdir(parents=True, exist_ok=True)
    cmd = ["pg_dump", dbname, "--compress=9", f"--file={outfile}"]
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        raise RuntimeError(f"pg_dump échoué : {result.stderr}")
    return outfile

def cleanup_old_backups():
    import time
    cutoff = time.time() - RETENTION_DAYS * 86400
    for f in BACKUP_DIR.glob("*.sql.gz"):
        if f.stat().st_mtime < cutoff:
            f.unlink()
            log.info(f"Supprimé : {f.name}")

if __name__ == "__main__":
    log.info("Début de la sauvegarde")
    for db in DATABASES:
        db = db.strip()
        if not db:
            continue
        try:
            path = backup_database(db)
            size = path.stat().st_size / 1024 / 1024
            log.info(f"Sauvegardé : {db}{path.name} ({size:.1f} Mo)")
        except Exception as e:
            log.error(f"Échec sauvegarde {db} : {e}")
            sys.exit(1)
    cleanup_old_backups()
    log.info("Sauvegarde terminée avec succès")

L’unité service (/etc/systemd/system/backup-databases.service) :

[Unit]
Description=Sauvegarde des bases de données PostgreSQL
Documentation=https://wiki.interne/backup
After=postgresql.service network.target
Requires=postgresql.service

[Service]
Type=oneshot
User=postgres
Group=postgres
WorkingDirectory=/var/backups

# Configuration via variables d'environnement
Environment=BACKUP_DIR=/var/backups/postgresql
Environment=RETENTION_DAYS=7
Environment=DATABASES=app_production,app_staging,users

# Le script
ExecStart=/usr/local/bin/backup-databases.py

# Sécurité
NoNewPrivileges=yes
ProtectSystem=strict
ReadWritePaths=/var/backups/postgresql
PrivateTmp=yes

# En cas d'échec : réessayer une fois après 60s
Restart=on-failure
RestartSec=60s
StartLimitIntervalSec=300
StartLimitBurst=2

# Timeout
TimeoutStartSec=1800

# Logs
StandardOutput=journal
StandardError=journal
SyslogIdentifier=backup-databases

[Install]
WantedBy=multi-user.target

Le timer associé (/etc/systemd/system/backup-databases.timer) :

[Unit]
Description=Timer de sauvegarde quotidienne des bases de données
Requires=backup-databases.service

[Timer]
OnCalendar=*-*-* 03:00:00
RandomizedDelaySec=900
Persistent=yes

[Install]
WantedBy=timers.target
# Déploiement
systemctl daemon-reload
systemctl enable --now backup-databases.timer

# Tester manuellement sans attendre le timer
systemctl start backup-databases.service

# Vérifier les logs
journalctl -u backup-databases.service -f

# Vérifier le prochain déclenchement
systemctl list-timers backup-databases.timer

Oneshot vs simple pour les tâches de batch

Utilisez Type=oneshot pour les scripts qui s’exécutent et se terminent (scripts de backup, de nettoyage, de migration). Systemd considère le service comme « actif » pendant l’exécution et « inactif » (mais exited avec succès) après. Contrairement à Type=simple, systemd attend que le processus se termine avant de déclarer la tâche accomplie, ce qui est important pour les dépendances entre services.

Résumé#

Systemd unifie la gestion du cycle de vie de tous les processus système sous une interface cohérente, du démarrage à l’arrêt, avec journalisation centralisée et supervision automatique.

Systemd en production — erreurs fréquentes

Les erreurs les plus fréquentes en production : (1) oublier systemctl daemon-reload après une modification, (2) confondre enable et start, (3) écrire des chemins relatifs dans ExecStart= (toujours utiliser des chemins absolus), (4) oublier de vider ExecStart= avant de le redéfinir dans un drop-in, (5) configurer Restart=always sans StartLimitBurst= — un service qui plante en boucle peut saturer les ressources.

Points clés à retenir

  • Unités : atom de base. Chaque service, socket, timer, point de montage est une unité avec son fichier de configuration déclaratif.

  • Targets : remplacent les runlevels SysV. multi-user.target pour les serveurs, graphical.target pour les desktops.

  • daemon-reload : obligatoire après chaque modification de fichier d’unité.

  • Drop-in files : surcharger un service via systemctl edit sans modifier le fichier d’origine.

  • Timers : remplacent avantageusement cron — journalisés, avec dépendances, avec rattrapage (Persistent=yes).

  • journalctl : interface unique pour tous les logs. -u, -f, --since, -p sont les filtres les plus utilisés.

  • Sécurité : ProtectSystem=, PrivateTmp=, NoNewPrivileges= dans [Service] pour durcir les services.

Commandes de référence

systemctl status <svc>            # état détaillé
systemctl enable --now <svc>      # activer et démarrer
systemctl daemon-reload           # recharger après modif
systemctl edit <svc>              # créer un drop-in
systemctl cat <svc>               # voir la config effective
systemctl list-units --failed     # services en échec
systemctl list-timers             # timers actifs
journalctl -u <svc> -f            # logs en temps réel
journalctl -u <svc> --since today # logs depuis aujourd'hui
journalctl -p err                 # uniquement erreurs
systemd-analyze blame             # temps de démarrage par service
systemd-analyze security <svc>    # score de sécurité