Remplacer vos tâches cron par des timers systemd
Remplacez les tâches cron par des minuteurs systemd : créez les unités service et timer, validez OnCalendar, activez le rattrapage et consultez journaux et erreurs.
Pour remplacer une tâche cron par un timer systemd, vous écrivez deux fichiers d’unité : un .service qui exécute le script et un .timer qui indique quand l’exécuter. Vous activez ensuite le timer avec systemctl enable --now.
Si vous avez déjà ouvert /var/log/backup.log à 9 h du matin sans pouvoir dire si l’exécution de la nuit avait réussi, vous savez déjà pourquoi on abandonne cron. Ce guide convertit une véritable ligne de crontab en timer :
30 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
Chaque section remplace une partie de cette ligne. La planification devient OnCalendar=, la redirection devient le journal, et la question « est-ce que ça a tourné ? » trouve sa réponse dans systemctl status. Pour en savoir plus sur cron lui-même, consultez comment automatiser des tâches cron. Les commandes fonctionnent sur toute distribution actuelle basée sur systemd.
Points clés
- Un timer systemd est une unité
.timerdont la section[Timer]détermine quand le.servicecorrespondant s’exécute. Le service définit ce qui s’exécute, et c’est le timer que l’on active. - Le service déclenché par un timer n’a pas besoin de section
[Install]. Le timer se rattache lui-même au démarrage viaWantedBy=timers.target. systemd-analyze calendarvérifie qu’une expressionOnCalendar=est correctement analysée et affiche sa prochaine échéance.Persistent=trueexécute une tâche manquée pendant que la machine était éteinte dès que le timer est de nouveau activé.- Les timers systemd n’ont pas d’équivalent intégré au
MAILTOde cron. Pour les alertes en cas d’échec, utilisezOnFailure=.
Ce que cron ne sait pas faire
Cron lance des commandes selon une planification, et pas grand-chose de plus. Les lacunes ci-dessous sont les raisons habituelles de migrer une tâche :
- Aucune journalisation hormis la sortie envoyée par e-mail. Soit vous redirigez manuellement la sortie vers un fichier, soit vous comptez sur
MAILTO. Les tâches lancées par un timer écrivent automatiquement dans le journal (voir Consulter les logs). - Aucune dépendance entre les tâches. Les tâches lancées par un timer sont des unités ordinaires : elles peuvent donc dépendre d’autres unités systemd.
- Rien ne s’exécute si la machine était éteinte à l’heure prévue.
Persistent=truerattrape l’exécution manquée (voir la section Persistent). - Aucun moyen simple de vérifier le résultat de la nuit dernière.
systemctl statusaffiche la dernière exécution et son code de sortie.
Créer un timer systemd de bout en bout
Un timer fonctionnel se compose de deux fichiers dans /etc/systemd/system/, plus une commande : sudo systemctl enable --now backup.timer. Commencez par le script.
#!/usr/bin/env bash
# /usr/local/bin/backup.sh
set -euo pipefail
SRC=/srv/app/data
DEST=/var/backups/app
tar -czf "$DEST/app-$(date +%F).tar.gz" -C "$SRC" .
find "$DEST" -name 'app-*.tar.gz' -mtime +14 -delete
set -euo pipefail force le script à se terminer avec un code non nul dès qu’une commande échoue, et c’est ce code de sortie que systemd enregistre. Rendez le script exécutable et créez le répertoire de destination. Si les bits de permission ne vous sont pas familiers, le guide des permissions de fichiers Linux les explique.
sudo chmod +x /usr/local/bin/backup.sh
sudo install -d -o deploy -m 750 /var/backups/app
Ensuite, le service, /etc/systemd/system/backup.service :
[Unit]
Description=Nightly tarball of /srv/app/data
[Service]
Type=oneshot
User=deploy
ExecStart=/usr/local/bin/backup.sh
Type=oneshot dans systemd.service(5) convient aux scripts qui s’exécutent jusqu’à leur terme. systemd ne considère l’unité comme démarrée qu’une fois le script terminé. Sauf si vous ajoutez RemainAfterExit=, l’unité repasse alors directement à l’état inactif et n’apparaît jamais comme « active ». Remplacez deploy par un compte non-root existant capable de lire SRC et d’écrire dans DEST. Sans User=, la tâche s’exécute en tant que root. Le service n’a ni section [Install] ni référence au timer. La page de l’Arch Wiki consacrée aux timers explique pourquoi : c’est le timer que l’on active, et c’est lui qui active le service portant le même nom.
Passons maintenant au timer, /etc/systemd/system/backup.timer :
[Unit]
Description=Run backup.service daily at 02:30
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
[Install]
WantedBy=timers.target
timers.target est une cible spéciale que systemd utilise au démarrage pour lancer tous les timers censés être actifs. Rechargez systemd, exécutez la tâche une fois manuellement, puis planifiez-la :
sudo systemctl daemon-reload
sudo systemctl start backup.service # test run now; returns when the script exits
systemctl status backup.service # check the result
sudo systemctl enable --now backup.timer
systemctl list-timers backup.timer
Comme chaque tâche planifiée est un service ordinaire, vous pouvez l’exécuter à la demande avec systemctl start backup.service et la déboguer sans attendre 02:30. Seul, enable se contente de créer les liens symboliques. L’option --now de systemctl(1) démarre également le timer. list-timers accepte un motif d’unité, et --all inclut les timers inactifs. Entre deux exécutions, le service apparaît comme inactif. C’est normal.
OnCalendar face à la ligne cron à cinq champs
Exécutez systemd-analyze calendar sur toute expression OnCalendar= avant de l’intégrer à un fichier de timer. La commande confirme que l’expression est correctement analysée et affiche sa prochaine échéance. La grammaire décrite dans systemd.time(7) est DayOfWeek Year-Month-Day Hour:Minute:Second. Chaque champ de date ou d’heure peut valoir * pour tout correspondre, une liste de valeurs séparées par des virgules, ou une plage notée avec ... Vous pouvez omettre complètement le jour de la semaine. Si vous le conservez, utilisez les noms de jours anglais comme Mon ou Monday, et non *.
Le tableau ci-dessous fait correspondre des planifications cron courantes à des expressions OnCalendar=. L’aide-mémoire cron pour Linux couvre la partie cron de ce tableau :
| Planification | cron | OnCalendar |
|---|---|---|
| Tous les jours à 02:30 | 30 2 * * * | *-*-* 02:30:00 |
| En semaine à 22:30 | 30 22 * * 1-5 | Mon..Fri 22:30 |
| Le dimanche à 03:00 | 0 3 * * 0 | Sun *-*-* 03:00:00 |
| Le 1er du mois, à minuit | 0 0 1 * * | *-*-01 00:00:00 |
| Toutes les 15 minutes | */15 * * * * | *:00/15 |
La ligne cron 30 22 * * 1-5 devient OnCalendar=Mon..Fri 22:30, car les numéros de jour 1 à 5 de cron correspondent au lundi à vendredi. Le suffixe /15 signifie « répéter toutes les 15 unités ». Validez une expression avec systemd-analyze calendar :
systemd-analyze calendar "Mon..Fri 22:30" --iterations=3
Exemple de sortie (les dates sont des valeurs fictives) :
Original form: Mon..Fri 22:30
Normalized form: Mon..Fri *-*-* 22:30:00
Next elapse: Mon YYYY-MM-DD 22:30:00 CEST
(in UTC): Mon YYYY-MM-DD 20:30:00 UTC
From now: 5h 12min left
Iteration #2: Tue YYYY-MM-DD 22:30:00 CEST
Iteration #3: Wed YYYY-MM-DD 22:30:00 CEST
La ligne normalisée montre la forme abrégée développée selon la grammaire complète. La ligne (in UTC) n’apparaît que si l’heure locale n’est pas UTC. Les versions antérieures à systemd 253 libellent les exécutions supplémentaires Iter. #2 au lieu de Iteration #2. --iterations= nécessite systemd 242 ou ultérieur.
Une réserve concernant la précision : le paramètre AccuracySec d’un timer vaut une minute par défaut, si bien qu’une tâche prévue à 02:30:00 n’est pas garantie de démarrer à cette seconde exacte. Si vous avez besoin de la précision la plus fine qu’offre systemd, ajoutez AccuracySec=1us à la section [Timer].
Persistent=true : la fonctionnalité pensée pour les portables
Avec Persistent=true, un timer qui a manqué son exécution prévue parce que la machine était éteinte démarre son service dès qu’il est de nouveau activé, c’est-à-dire généralement au démarrage suivant. Cron seul n’a pas d’équivalent. Si le portable est éteint à 02:30, cron saute la tâche. L’outil distinct anacron sert à rattraper les tâches manquées (sur les systèmes de la famille RHEL, il est fourni avec cronie), mais il ne planifie qu’à la journée près, et non à une heure précise. systemd.timer(5) enregistre la date du dernier déclenchement de chaque timer persistant. Persistent= n’a aucun effet sur les timers dépourvus de OnCalendar=, et l’exécution de rattrapage attend malgré tout l’éventuel délai défini par RandomizedDelaySec=.
Les timers monotones, comme OnBootSec= et OnUnitActiveSec=, se comportent différemment. Ils cessent de décompter pendant la mise en veille du système, sauf si WakeSystem= est défini. Utilisez OnCalendar= avec Persistent=true pour les tâches liées à une heure de la journée.
Consulter les logs et l’état de la dernière exécution
Pour savoir si la tâche de la nuit dernière a réussi, exécutez systemctl status backup.service. La commande indique quand le service s’est exécuté pour la dernière fois, s’il a réussi ou échoué, ainsi que le code de sortie du processus. journalctl(1) affiche tout ce que le script a écrit en sortie :
systemctl status backup.service
journalctl -u backup.service --since yesterday
journalctl -u backup.service --since "-2h"
--since accepte yesterday, today, des horodatages absolus et des décalages relatifs précédés de -. Notez que yesterday désigne minuit au début de la journée d’hier : la sortie inclut donc aussi les entrées d’aujourd’hui.
Il est utile de provoquer volontairement, une fois, l’échec d’une tâche lancée par un timer systemd. Modifiez le script pour qu’il contienne SRC=/srv/app/missing, puis exécutez-le :
sudo systemctl start backup.service
systemctl status backup.service
journalctl -u backup.service -n 20
tar échoue, set -e fait sortir le script avec un code non nul, et l’unité passe à l’état failed, ce code de sortie figurant dans son statut. Le journal contient le message d’erreur de tar lui-même. Rétablissez SRC une fois l’essai terminé.
Quand cron reste-t-il pertinent ?
Cron reste pertinent, et plus simple, pour une tâche isolée sans dépendances. Une tâche tient sur une seule ligne de crontab, cron est disponible quasiment partout, et une unique tâche nocturne sans dépendances y fonctionne parfaitement. Les timers vous coûtent deux fichiers et quelques commandes systemctl. Ils n’ont pas non plus d’équivalent intégré à MAILTO. Pour être notifié en cas d’échec d’une tâche, faites pointer la directive OnFailure= du service vers une unité chargée d’envoyer l’alerte.
Conclusion
Passer de cron aux timers systemd vous apporte trois choses que cron seul ne peut offrir : une tâche que l’on peut exécuter et inspecter à la demande, une sortie qui arrive dans le journal sans redirection, et des exécutions de rattrapage après une interruption. Choisissez la tâche de votre crontab qui vous inquiète le plus souvent. Écrivez son service et son timer, validez la planification avec systemd-analyze calendar, testez-la avec systemctl start, puis supprimez la ligne de crontab dès que systemctl status signale une exécution planifiée réussie.
FAQ
Puis-je exécuter un timer systemd en tant qu'utilisateur ordinaire, sans root ?
Oui. Placez les fichiers .service et .timer dans ~/.config/systemd/user/ et gérez-les avec systemctl --user, par exemple systemctl --user enable --now backup.timer. Par défaut, l'instance systemd de l'utilisateur s'arrête à la fermeture de sa dernière session, et les timers cessent alors de se déclencher. Exécutez sudo loginctl enable-linger suivi du nom d'utilisateur pour lancer l'instance utilisateur au démarrage et la maintenir active sans session ouverte.
Quelle est la différence entre AccuracySec et RandomizedDelaySec ?
AccuracySec laisse à systemd une marge pour regrouper les événements des timers et réveiller le processeur moins souvent. Le timer peut se déclencher à tout moment entre l'heure prévue et cette heure augmentée d'AccuracySec, fenêtre fixée à 1 minute par défaut. RandomizedDelaySec agit à l'inverse du regroupement : il retarde chaque exécution d'une durée aléatoire, jusqu'à la valeur définie (0 par défaut), afin que les timers partageant la même planification s'étalent au lieu de se déclencher simultanément. Si vous définissez les deux, le délai aléatoire est appliqué avant que systemd n'aligne l'événement sur les autres.
Comment planifier une tâche ponctuelle avec systemd sans écrire de fichiers d'unité ?
Utilisez systemd-run avec une option de timer, par exemple sudo systemd-run --on-calendar='Mon 14:00' /usr/bin/cleanup.sh. Cette commande crée une paire timer/service transitoire. Seul le timer démarre immédiatement, et le service s'exécute à l'échéance du timer. Des options comme --on-active= et --on-boot= créent des timers transitoires monotones. Les unités transitoires ne survivent pas à un redémarrage : les tâches récurrentes ont donc toujours leur place dans des fichiers d'unité.
Le fichier de timer doit-il porter le même nom que le service qu'il déclenche ?
Non, mais c'est le comportement par défaut. Un timer active le service portant le même nom, au suffixe près : backup.timer démarre donc backup.service. Pour déclencher une unité portant un autre nom, définissez Unit= dans la section [Timer], par exemple Unit=nightly-backup.service. La valeur ne peut pas être une autre unité .timer. Cela dit, systemd.timer(5) recommande de donner le même nom de base au timer et à l'unité qu'il démarre.