12k
All articles

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.

OpenReplay Team
OpenReplay Team
Remplacer vos tâches cron par des timers systemd

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é .timer dont la section [Timer] détermine quand le .service correspondant 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 via WantedBy=timers.target.
  • systemd-analyze calendar vérifie qu’une expression OnCalendar= est correctement analysée et affiche sa prochaine échéance.
  • Persistent=true exé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 MAILTO de cron. Pour les alertes en cas d’échec, utilisez OnFailure=.

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=true rattrape l’exécution manquée (voir la section Persistent).
  • Aucun moyen simple de vérifier le résultat de la nuit dernière. systemctl status affiche 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 :

PlanificationcronOnCalendar
Tous les jours à 02:3030 2 * * **-*-* 02:30:00
En semaine à 22:3030 22 * * 1-5Mon..Fri 22:30
Le dimanche à 03:000 3 * * 0Sun *-*-* 03:00:00
Le 1er du mois, à minuit0 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.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.