Files
varlog/e1e8a0c1-6971-4357-9aaa-7e7a748922f3/index.md
T
2026-05-15 10:37:48 +02:00

13 KiB

Quand systemd remplace cron : pourquoi (et comment) migrer ses tâches planifiées

Cron tourne sur Linux depuis 1975. Il a fait son temps pour beaucoup d'usages : voici ce que les timers systemd apportent, et comment basculer sans tout casser.

Pourquoi cron reste partout

cron est l'un des plus anciens outils Unix encore en service. Son principe tient en deux idées : un démon qui se réveille toutes les minutes, et un fichier texte — la crontab — où chaque ligne décrit une commande et son moment d'exécution avec cinq champs (minute, heure, jour du mois, mois, jour de la semaine).

# m h dom mon dow  command
0 2 * * *  /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

Cinquante ans plus tard, ça marche. C'est installé partout, c'est documenté à mort, ça tient sur une ligne, et n'importe quel administrateur sait lire 0 2 * * *. Pour beaucoup de besoins simples — « lancer ce script tous les jours à 2h du matin » — cron reste le bon choix.

Le problème est que les besoins ont rarement été aussi simples depuis longtemps.

Les limites de cron qu'on finit toujours par rencontrer

À chaque administration de serveur sérieuse, on retombe sur les mêmes frustrations.

La machine était éteinte au moment du job. Cron saute purement et simplement l'occurrence ratée. Si le portable de l'utilisateur dormait à 2h, la sauvegarde quotidienne n'aura pas lieu — point. Le job s'exécutera de nouveau le lendemain à 2h, sans rattrapage, sans alerte.

Les logs sont dispersés ou perdus. Par défaut, la sortie standard du job est envoyée par mail à l'utilisateur (si MAILTO est défini et qu'un MTA tourne) ou simplement perdue. Le démon lui-même logue dans syslog quand il démarre un job, mais pas son contenu. Diagnostiquer pourquoi un job a échoué la semaine dernière relève souvent de l'archéologie.

Pas de dépendances. Un job qui doit attendre que le réseau soit monté, qu'un point de montage soit présent, qu'un autre service ait fini son démarrage : cron ne sait pas exprimer ça. La parade habituelle — un sleep 60 ou un @reboot suivi d'une boucle d'attente — fonctionne mais reste un bricolage.

Pas de recouvrement entre exécutions. Si un job de 5 minutes en prend 7 ce jour-là, cron lance la prochaine occurrence pile au moment où la précédente tourne encore. Deux instances simultanées d'un script de synchronisation, c'est rarement ce qu'on veut.

Pas de jitter, pas de randomisation. Quand cinquante VMs lancent leur cron.daily toutes en même temps à 6h25 (l'heure d'anacron par défaut sur Debian), le pic de charge sur l'hyperviseur est garanti. Cron n'offre aucune primitive pour étaler les exécutions.

Pas de visibilité globale. Pour répondre à « quels jobs vont tourner aujourd'hui sur cette machine ? », il faut lire la crontab système, les crontabs utilisateur (/var/spool/cron/), le contenu de /etc/cron.d/, /etc/cron.daily/, /etc/cron.hourly/, etc. Aucune commande ne donne la vue consolidée.

Pas d'isolation, pas de quota. Le job s'exécute avec les privilèges et les ressources du shell qui l'a lancé. Aucune façon native de limiter à 50 % de CPU, à 1 Go de RAM, ou de couper si ça dépasse 10 minutes.

Aucun de ces points ne rend cron inutilisable. Mais accumulés sur une dizaine de jobs critiques, ils transforment l'administration en travail de surveillance permanente.

Ce qu'apporte un timer systemd

Sur toute distribution Linux moderne basée sur systemd (la quasi-totalité, hors BSD, Alpine, Gentoo et quelques cas particuliers), une alternative native existe : les timers. Le principe est différent dès le départ.

Un timer systemd, c'est deux fichiers au lieu d'une ligne :

  • Un fichier .service qui décrit ce qu'il faut faire — exactement comme on décrit un service classique, en mode Type=oneshot pour un job ponctuel
  • Un fichier .timer qui décrit quand le faire — ce sont les règles de déclenchement

Cette séparation entre le « quoi » et le « quand » est plus verbeuse au départ, mais elle débloque tout le reste.

Une syntaxe d'horaire lisible

Là où cron oblige à mentaliser 0 2 * * 1-5, systemd écrit :

OnCalendar=Mon..Fri 02:00:00

Et la commande systemd-analyze calendar "Mon..Fri 02:00:00" valide l'expression en montrant la prochaine exécution prévue. Une erreur de jour-de-semaine ou un décalage horaire ne plante pas en silence : on le voit avant de déployer.

D'autres formes utiles que cron ne sait pas exprimer :

OnCalendar=*-*-* 02..04:00:00          # toutes les heures entre 2h et 4h
OnCalendar=*-*-01 03:00:00             # tous les 1er du mois à 3h
OnCalendar=*-*-* 09:00:00 Europe/Paris # à 9h heure de Paris, été comme hiver

Le support natif des fuseaux horaires est une avancée significative pour qui gère des serveurs distribués géographiquement — cron ignore tout du concept et tourne sur le fuseau du système.

Du temps relatif, pas seulement du temps absolu

Cron raisonne uniquement en horloge murale (« tel jour, à telle heure »). systemd ajoute le temps monotone, relatif à un événement :

OnBootSec=10min          # 10 minutes après le démarrage
OnUnitActiveSec=6h       # toutes les 6 heures après la dernière exécution
OnStartupSec=5min        # 5 minutes après le démarrage de systemd

OnUnitActiveSec=6h règle proprement le problème des exécutions qui se chevauchent : la prochaine instance se déclenche 6 heures après la fin de la précédente, pas 6 heures après son démarrage. Aucune équivalence simple en cron.

Le rattrapage des exécutions ratées

Une seule ligne change tout :

Persistent=true

Avec cette option, systemd mémorise la dernière exécution réussie. Si la machine était éteinte au moment prévu, le job se déclenche dès le démarrage suivant (après le RandomizedDelaySec éventuel, voir plus bas). Pour un portable, un poste de développement, ou n'importe quelle machine qui n'est pas en service 24/7, c'est une différence majeure de fiabilité.

Du jitter intégré

RandomizedDelaySec=15min

Le déclenchement se fait à un instant aléatoire dans la fenêtre [heure_prévue, heure_prévue + 15 min]. Quand cinquante machines lancent leur mise à jour quotidienne, le pic de charge se lisse au lieu de tomber au même instant. C'est la fonctionnalité que tous les administrateurs de flottes finissent par re-bricoler en cron avec un sleep $((RANDOM % 900)) peu élégant.

Le logging gratuit dans journald

Tout ce que le service écrit sur stdout et stderr est capturé automatiquement par journald. Une seule commande pour tout consulter :

journalctl -u backup.service           # toutes les exécutions historisées
journalctl -u backup.service -f        # en suivi temps réel
journalctl -u backup.service --since yesterday

Pas de configuration, pas de redirection à la main, pas de >> /var/log/backup.log 2>&1 à coller à chaque ligne de crontab. Et accessoirement, journald gère la rotation, la compression et la rétention.

Les dépendances déclaratives

Dans le fichier .service, on peut dire au planificateur qu'un job nécessite que le réseau soit prêt, qu'un point de montage soit présent, qu'un autre service ait démarré :

[Unit]
Wants=network-online.target
After=network-online.target
RequiresMountsFor=/mnt/backup

systemd attend que ces conditions soient remplies avant de déclencher le service. Le job ne tente plus de s'exécuter sur un montage absent ou avant que la résolution DNS soit fonctionnelle.

Le contrôle des ressources via cgroups

Puisque chaque exécution passe par un service, on bénéficie de tout l'arsenal cgroups de systemd :

[Service]
CPUQuota=50%
MemoryMax=1G
IOWeight=10

Un job de sauvegarde qui pourrait saturer le disque ne sortira pas de son enveloppe. Cron n'offre rien d'équivalent — au mieux on enrobe la commande dans nice et ionice, ce qui reste primitif.

La vue consolidée

systemctl list-timers --all

Une seule commande, toutes les exécutions planifiées du système, classées par prochaine échéance, avec date de dernière exécution. La question « qu'est-ce qui tourne automatiquement sur cette machine ? » trouve enfin une réponse en une ligne.

Un exemple complet, pas-à-pas

Reprenons le job de sauvegarde initial — 0 2 * * * /usr/local/bin/backup.sh — et traduisons-le.

/etc/systemd/system/backup.service :

[Unit]
Description=Sauvegarde quotidienne
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
User=backup
Group=backup
ExecStart=/usr/local/bin/backup.sh
# Capture stdout/stderr dans journald (comportement par défaut, ici explicité)
StandardOutput=journal
StandardError=journal
# Garde-fous ressources
CPUQuota=50%
MemoryMax=1G

/etc/systemd/system/backup.timer :

[Unit]
Description=Déclenche la sauvegarde tous les jours à 2h

[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=15min

[Install]
WantedBy=timers.target

Activation :

sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer

Vérifications :

systemctl list-timers backup.timer
systemctl status backup.timer
systemd-analyze calendar "*-*-* 02:00:00"     # voir la prochaine échéance
journalctl -u backup.service                  # voir l'historique
sudo systemctl start backup.service           # déclencher manuellement pour tester

Comparé à la ligne de crontab originale, c'est plus verbeux. Mais on a, sans rien ajouter : le rattrapage en cas d'arrêt machine, du jitter pour éviter les pics, l'attente du réseau, des limites de ressources, du logging structuré, et une commande pour tout inspecter.

Quelques recettes utiles

Tous les jours à 3h sauf le dimanche :

OnCalendar=Mon..Sat 03:00:00

Toutes les 15 minutes pendant les heures de bureau :

OnCalendar=Mon..Fri 08..18:00/15:00

Le premier lundi de chaque mois à 5h : pas faisable en une seule expression, mais combinable avec une condition ExecStartPre qui vérifie la date et sort si ce n'est pas le bon jour. C'est l'une des rares zones où cron reste plus naturel (0 5 * * 1 + [ $(date +\%d) -le 7 ] dans le script).

Toutes les six heures à partir du dernier passage (jamais de chevauchement) :

[Timer]
OnBootSec=5min
OnUnitActiveSec=6h

Timer utilisateur, sans sudo : dans ~/.config/systemd/user/, puis :

systemctl --user daemon-reload
systemctl --user enable --now monjob.timer
loginctl enable-linger $USER   # pour que ça tourne sans session ouverte

Quand garder cron

Tout n'est pas à migrer. Cron reste le bon choix dans plusieurs cas :

  • Scripts portables vers BSD, macOS, ou des conteneurs minimaux. systemd n'existe pas dans Alpine Linux, sur les BSD, ni dans la plupart des images Docker légères.
  • Tâches utilisateur très simples sur un serveur partagé, où chaque utilisateur gère sa propre crontab sans privilèges admin.
  • Notification par mail intégrée : si MAILTO=admin@domain.tld suivi d'une sortie sur stderr couvre déjà le besoin de monitoring, repasser par journald + un exporter Prometheus est de la sur-ingénierie.
  • Un job de trente secondes à ajouter sur un serveur existant déjà couvert par cron. Mélanger les deux outils est sans risque — ils coexistent sans interférence — et créer deux fichiers pour un alias unique d'une ligne reste excessif.

La meilleure stratégie est rarement migratoire au pas de charge. Elle consiste à utiliser systemd pour toute nouvelle tâche planifiée, et à ne migrer les jobs cron existants que quand ils posent un problème concret : un job raté qu'il fallait rattraper, un log perdu qu'il fallait retrouver, un chevauchement qui a corrompu des données.

En résumé

Cron n'est pas obsolète, il est sous-dimensionné pour des besoins modernes. Les timers systemd ne remplacent pas la simplicité d'une ligne de crontab pour un job trivial, mais ils apportent à peu près tout ce qui manque dès qu'une tâche planifiée devient critique : rattrapage, logging, dépendances, isolation, observabilité.

Pour un DevOps qui construit aujourd'hui un nouveau service, le choix par défaut a basculé : commencer en systemd, et n'utiliser cron que par exception justifiée. La verbosité initiale des deux fichiers se rentabilise au premier incident de production qu'on diagnostique en journalctl -u nom.service au lieu de fouiller dans des logs disparates.

Et même sans migrer quoi que ce soit, la commande systemctl list-timers mérite d'entrer dans le réflexe de tout audit de machine Linux. C'est là que se cache la moitié des tâches planifiées qu'on croit avoir comprises.