Aller au contenu
Planifier des tâches : cron et minuteurs

Planifier des tâches : cron et minuteurs

200 Compagnon ⏱ 1 h 20 linuxdebianubuntusystemd

À la fin, vous saurez

  • Lire et écrire une ligne de crontab, y compris dans /etc/cron.d, en évitant les pièges du jour du mois, du signe % et de l'environnement minimal
  • Inventorier toutes les tâches planifiées d'un serveur hérité, crontabs et minuteurs confondus
  • Écrire une paire .timer et .service, valider son calendrier avec systemd-analyze calendar et vérifier son déclenchement avec systemctl list-timers
  • Choisir entre cron et un minuteur systemd selon des critères explicites : journal, rattrapage, ressources, isolation, portabilité
  • Empêcher deux exécutions simultanées d'une même tâche et rendre une tâche idempotente
  • Être prévenu de l'échec d'une tâche, et de son absence d'exécution, par OnFailure= et un signal de vie
  • Prévoir le comportement d'une tâche planifiée lors des changements d'heure

Prérequis

Testé avec anacron 2.3-39ubuntu2 (Ubuntu), 2.3-43 (Debian) cron 3.0pl1-184ubuntu2 (Ubuntu), 3.0pl1-197 (Debian) debian 13 debianutils 5.17 (Ubuntu), 5.23.2 (Debian) systemd 255 (Ubuntu), 257 (Debian) ubuntu 24.04 util-linux 2.39.3 (Ubuntu), 2.41.5 (Debian) , vérifié le 7 octobre 2026

Pourquoi

Chaque nuit, la mairie reçoit un fichier CSV des signalements de la veille, produit sur sig-outils. Chaque nuit aussi, une purge supprime les pièces jointes (photos de nids-de-poule, de lampadaires éteints) arrivées au bout de leur durée de conservation, telle qu'elle figure au registre des traitements au titre du RGPD. Ces tâches sont invisibles tant qu'elles marchent ; quand l'une s'arrête, on l'apprend par la mairie, dix jours plus tard, ou par un audit qui trouve des photos conservées deux ans au lieu d'un.

En reprenant sig-outils, vous découvrez l'état laissé par Camille : deux lignes dans sa crontab personnelle, l'une qui renvoie toute sortie vers /dev/null, un MAILTO vers une adresse qui n'existera plus après son départ, et aucun moyen de savoir si l'export de la nuit dernière a réussi. Trois défauts classiques : la tâche est cachée, muette et non surveillée.

Cette leçon apprend à planifier une tâche avec les deux outils présents sur toute machine Debian ou Ubuntu, cron et les minuteurs de systemd, puis surtout à rendre une tâche planifiée fiable : qu'elle ne tourne pas deux fois en parallèle, qu'elle puisse être relancée sans dégât, qu'elle laisse une trace lisible, et que son échec comme son silence vous parviennent.

Les concepts

Planifier, c'est déclencher un processus sans terminal

Une tâche planifiée est un processus comme un autre (leçon 10 de Premiers pas), avec trois particularités qui expliquent presque tous ses pièges :

  • personne ne la regarde : pas de terminal, donc ce qu'elle écrit sur sa sortie standard et sa sortie d'erreur doit être recueilli par quelqu'un, sinon c'est perdu ;
  • son environnement n'est pas le vôtre : elle ne lit pas votre .bashrc, son PATH est réduit, sa locale et son répertoire courant diffèrent (leçon 7 de Premiers pas) ;
  • elle dépend de l'heure : donc de l'horloge, du fuseau horaire et des changements d'heure (leçon 9).

Un planificateur (scheduler en anglais) est le démon qui se réveille au bon moment et lance le processus. Sur un serveur Linux actuel, deux planificateurs cohabitent : cron, l'outil historique d'Unix, et le gestionnaire systemd lui-même, par ses unités .timer.

cron et la crontab

cron lit des tables appelées crontabs (de cron table). Chaque ligne associe un moment, décrit par cinq champs, à une commande :

┌───────────── minute        (0-59)
│ ┌─────────── heure         (0-23)
│ │ ┌───────── jour du mois  (1-31)
│ │ │ ┌─────── mois          (1-12, ou jan, feb...)
│ │ │ │ ┌───── jour semaine  (0-7, 0 et 7 = dimanche, ou sun, mon...)
│ │ │ │ │
30 2 * * *  /opt/signalements/bin/exporter

Chaque champ accepte une valeur (5), une liste (1,15), un intervalle (1-5), l'étoile qui signifie « toutes les valeurs », et un pas après un intervalle ou une étoile (*/15 : toutes les quinze minutes ; 0-23/6 : 0 h, 6 h, 12 h, 18 h). La page crontab(5) de Debian ajoute des raccourcis : @reboot (au démarrage du démon cron), @hourly, @daily (synonyme @midnight), @weekly, @monthly, @yearly.

cron se réveille chaque minute, compare l'heure courante à toutes les lignes, et lance celles qui correspondent. Il n'a pas de mémoire : une minute où la machine était éteinte est une minute perdue.

Où vivent les crontabs

Sur Debian et Ubuntu, quatre emplacements, à connaître tous lors d'un état des lieux :

EmplacementFormatQui l'écritRemarque
/var/spool/cron/crontabs/<compte>5 champs + commandecrontab -e du comptejamais édité à la main ; le répertoire n'est accessible qu'au groupe crontab
/etc/crontab5 champs + utilisateur + commandel'administrateurlance cron.hourly, cron.daily, etc.
/etc/cron.d/<fichier>5 champs + utilisateur + commandepaquets et administrateurun fichier par usage, se déploie bien par automatisation
/etc/cron.{hourly,daily,weekly,monthly}/scripts exécutablespaquets et administrateurlancés par run-parts, sans horaire précis

La différence de format est le premier piège : les fichiers système (/etc/crontab, /etc/cron.d/*) ont un sixième champ, le compte sous lequel lancer la commande ; les crontabs utilisateur ne l'ont pas, puisque le compte est celui du propriétaire.

Les répertoires cron.daily et run-parts

Le fichier /etc/crontab livré par Debian et Ubuntu contient quatre lignes de ce genre :

25 6    * * *   root    test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.daily; }

Chaque jour à 6 h 25, run-parts exécute, dans l'ordre alphabétique, les fichiers de /etc/cron.daily/. La page run-parts(8) pose une règle stricte : sans option particulière, seuls les fichiers dont le nom est composé uniquement de lettres ASCII, de chiffres, de soulignés et de tirets sont lancés. Un fichier export.sh ou purge.py est ignoré sans aucun message. Debian l'a voulu ainsi pour ne jamais exécuter les copies laissées par le gestionnaire de paquets (logrotate.dpkg-old, logrotate.dpkg-dist). La même règle s'applique aux fichiers de /etc/cron.d/ : pas de point dans le nom.

Le test -x /usr/sbin/anacron || de la ligne s'explique par le paragraphe suivant.

anacron, pour les machines qui dorment

anacron ne raisonne pas en heures mais en périodes : « au moins une fois par jour, par semaine, par mois ». Il note dans /var/spool/anacron/ la date de la dernière exécution de chaque groupe et, s'il constate un retard (la machine était éteinte à 6 h 25), il rattrape. Quand il est installé, cron(8) précise que cron lui laisse les tâches quotidiennes, hebdomadaires et mensuelles, d'où le test -x. Conçu pour les portables, il n'est en général pas installé sur un serveur (dpkg -s anacron) ; les minuteurs systemd offrent le même rattrapage avec Persistent=true.

L'environnement d'une tâche cron

C'est la source de la moitié des « ça marche à la main mais pas dans cron ». D'après crontab(5) et le code source du paquet Debian :

  • SHELL vaut /bin/sh (/usr/bin/sh sur Debian 13, le même fichier depuis la fusion de /usr), donc dash sur Debian et Ubuntu, pas Bash : pas de [[ ]], pas de tableaux, pas d'expansion {1..5} ;
  • HOME et LOGNAME viennent de /etc/passwd ;
  • PATH vaut /usr/bin:/bin (constante _PATH_DEFPATH du code), sauf si la crontab le redéfinit. La page le dit explicitement : les variables chargées par PAM depuis /etc/environment ne remplacent pas ce réglage. Une commande dans /usr/sbin ou /usr/local/bin n'est donc pas trouvée ;
  • le répertoire courant est HOME ; la locale est celle de /etc/default/locale, rarement celle de votre session.

On peut définir des variables en tête de crontab (PATH=..., MAILTO=...), mais sans substitution : PATH=$HOME/bin:$PATH n'est pas développé.

Ce que devient la sortie

cron récupère tout ce que la commande écrit et, s'il y a quelque chose, l'envoie par courrier au propriétaire de la crontab, ou à l'adresse de MAILTO (MAILTO="" désactive l'envoi). Cela suppose un agent de transfert de courrier (MTA, Mail Transfer Agent, le programme qui achemine le courrier, comme Postfix ou Exim) installé et configuré. Sur une instance de cloud, il n'y en a presque jamais : le correctif Debian Dont-fail-on-missing-MTA fait alors écrire dans le journal :

CRON[48211]: (CRON) info (No MTA installed, discarding output)

La sortie de la tâche, et donc son message d'erreur, est jetée. Il faut remplacer le courrier par une journalisation explicite, ce que les minuteurs systemd font nativement.

Les minuteurs systemd

Un minuteur systemd (timer) est une unité de type .timer dont le seul rôle est de démarrer une autre unité au bon moment, par défaut le .service du même nom. On sépare ainsi deux questions :

  • quand ? : le fichier signalements-export.timer, avec sa section [Timer] ;
  • quoi et comment ? : le fichier signalements-export.service, un service ordinaire de type oneshot, qui bénéficie de tout ce que systemd sait faire pour un service : compte dédié, fichier d'environnement, journal, limites de ressources, isolation, dépendances.

La page systemd.timer(5) distingue deux familles de déclencheurs :

  • les minuteurs calendaires, OnCalendar=, qui suivent l'heure murale : « tous les jours à 5 h » ;
  • les minuteurs monotones, relatifs à un événement : OnBootSec= (après le démarrage de la machine), OnStartupSec= (après le démarrage du gestionnaire systemd), OnActiveSec= (après l'activation du minuteur), OnUnitActiveSec= (après la dernière activation du service), OnUnitInactiveSec= (après la dernière fin du service). Ils ne dépendent pas de l'horloge murale et ne sont donc pas affectés par un changement d'heure.

On peut combiner plusieurs déclencheurs dans un même minuteur. OnBootSec=10min plus OnUnitActiveSec=1h signifie « dix minutes après le démarrage, puis une heure après chaque exécution ».

La syntaxe OnCalendar

systemd.time(7) définit une expression calendaire sous la forme JourSemaine Année-Mois-Jour Heure:Minute:Seconde Fuseau, chaque partie étant facultative sauf l'heure ou la date :

BesoincronOnCalendar
tous les jours à 2 h 3030 2 * * **-*-* 02:30:00 (ou 02:30)
du lundi au vendredi à 5 h0 5 * * 1-5Mon..Fri *-*-* 05:00
toutes les 15 minutes*/15 * * * **:0/15
le 1er du mois à minuit0 0 1 * *monthly ou *-*-01 00:00
le dernier jour du mois à 23 himpossible directement*-*~01 23:00
à 5 h, heure de Paris, sur un serveur en UTCimpossible (un seul fuseau par démon)*-*-* 05:00 Europe/Paris

Les intervalles s'écrivent avec .., les répétitions avec /, le tilde désigne les derniers jours du mois (~01 : le dernier, ~03 : l'antépénultième), et un nom de fuseau de la base IANA peut terminer l'expression. Les mots-clés minutely, hourly, daily, weekly (lundi 0 h), monthly, yearly, quarterly et semiannually sont des abréviations.

Une différence de sens avec cron : chez systemd, un jour de la semaine et une date se combinent par ET. Fri *-*-13 signifie « les vendredis 13 ». Chez cron, on va le voir dans les pièges, c'est l'inverse.

Les réglages qui comptent

  • Persistent=true : systemd enregistre sur disque la date du dernier déclenchement. Si la machine était éteinte (ou le minuteur arrêté) au moment prévu, le service est lancé dès la réactivation du minuteur. Ne s'applique qu'à OnCalendar=. Faux par défaut.
  • RandomizedDelaySec= : ajoute un délai aléatoire, tiré uniformément entre 0 et la valeur donnée, à chaque échéance. Sert à étaler une même tâche sur un parc pour ne pas réveiller cinquante machines à minuit pile contre le même serveur. 0 par défaut. FixedRandomDelay=true rend ce délai stable d'une échéance à l'autre pour une machine donnée.
  • AccuracySec= : la précision demandée. Par défaut une minute : systemd s'autorise à déclencher n'importe quand dans la minute qui suit l'échéance, pour regrouper les réveils et économiser l'énergie. Pour une tâche à la seconde près, AccuracySec=1s.
  • Unit= : le service à déclencher, si son nom diffère de celui du minuteur.

Les minuteurs calendaires reçoivent automatiquement une dépendance sur time-set.target et time-sync.target : ils attendent que l'horloge soit réglée, ce qui évite qu'une machine dont l'horloge matérielle dérive déclenche une tâche à une heure absurde au démarrage.

En pratique

Les commandes de cette section n'ont pas été exécutées pour rédiger la leçon : les sorties présentées sont reconstruites d'après les pages de manuel et le code source de cron et de systemd, et introduites comme telles. On suppose, conformément à la leçon 9, que sig-outils est réglé en UTC.

Inventorier les tâches héritées

Avant d'écrire quoi que ce soit, trouvez tout ce qui est déjà planifié. La leçon 1 l'a fait en survol ; voici la méthode complète.

$ sudo ls -l /var/spool/cron/crontabs/
$ for u in $(sudo ls /var/spool/cron/crontabs/); do echo "== $u"; sudo crontab -l -u "$u"; done
$ cat /etc/crontab
$ ls -l /etc/cron.d/ /etc/cron.hourly/ /etc/cron.daily/ /etc/cron.weekly/ /etc/cron.monthly/
$ systemctl list-timers --all
$ ls /etc/systemd/system/*.timer 2>/dev/null
$ atq 2>/dev/null

crontab -l -u <compte> affiche la crontab d'un compte ; la boucle passe par le spool, seule liste des comptes qui en ont une. --all montre aussi les minuteurs inactifs, et /etc/systemd/system/*.timer isole ceux écrits par un humain, par opposition à ceux des paquets sous /usr/lib/systemd/system/. atq liste les tâches ponctuelles de at, s'il est installé.

Sur sig-outils, la crontab de camille contient :

MAILTO=camille@exemple-mairie.fr
0 2 * * * cd /opt/signalements && venv/bin/python -m signalements.export > /dev/null 2>&1
15 3 * * * cd /opt/signalements && venv/bin/python -m signalements.purge --jours 365 >> /var/log/purge.log

L'export jette tout vers /dev/null, donc le MAILTO ne reçoit jamais rien ; la purge écrit dans un fichier sans rotation, peut-être propriété de root (la redirection échouerait alors) ; rien ne dit si l'une ou l'autre a réussi. Surtout, les deux tâches tournent sous l'identité de Camille : comme l'a noté la leçon 1, verrouiller son compte les arrêtera. Il faut donc les migrer vers le compte de service signalements, propriétaire du code et de /srv/donnees/pieces-jointes. Le journal, lui, dit au moins qu'elles ont été lancées :

$ journalctl -u cron --since yesterday --grep 'CMD'

Sortie typique :

Oct 07 02:00:01 sig-outils CRON[51873]: (camille) CMD (cd /opt/signalements && venv/bin/python -m signalements.export > /dev/null 2>&1)
Oct 07 03:15:01 sig-outils CRON[52419]: (camille) CMD (cd /opt/signalements && venv/bin/python -m signalements.purge --jours 365 >> /var/log/purge.log)

cron journalise par défaut le démarrage des tâches (niveau 1 de l'option -L), pas leur fin ni leur code de sortie. Avec -L 15 dans EXTRA_OPTS de /etc/default/cron, il journalise aussi les fins et les échecs, ce qui vaut la peine sur une machine qui garde des tâches cron.

Écrire une crontab correcte

Si vous restiez sur cron, voici ce que devrait contenir la crontab du compte signalements (on suppose qu'il possède un répertoire /var/lib/signalements), éditée par sudo crontab -e -u signalements :

SHELL=/bin/bash
PATH=/opt/signalements/venv/bin:/usr/local/bin:/usr/bin:/bin
MAILTO=""

# Export CSV pour la mairie, chaque nuit à 2 h 00 UTC
0 2 * * *  cd /opt/signalements && flock -n /var/lib/signalements/export.lock venv/bin/python -m signalements.export 2>&1 | logger -t signalements-export

Ligne par ligne :

  • SHELL=/bin/bash : la commande sera interprétée par Bash plutôt que dash, utile si elle emploie des constructions propres à Bash.
  • PATH= : explicite, avec l'environnement virtuel en tête. Sans cela, un outil de /usr/local/bin ou de l'environnement virtuel ne serait pas trouvé.
  • MAILTO="" : pas de courrier, puisqu'il n'y a pas de MTA ; la sortie part dans le journal à la place.
  • flock -n /var/lib/signalements/export.lock : prend un verrou exclusif sur le fichier ; -n (non-blocking) fait échouer immédiatement, avec le code 1, si une exécution précédente le détient encore. On y revient dans « Sous le capot ».
  • 2>&1 | logger -t signalements-export : la sortie d'erreur rejoint la sortie standard, et le tout est envoyé au journal avec l'étiquette signalements-export ; journalctl -t signalements-export la retrouve. Le code de sortie de la ligne devient celui de logger : sans importance pour cron, qui l'ignore de toute façon, mais c'est une raison de plus de préférer un minuteur.

À l'enregistrement, crontab vérifie la syntaxe avant d'installer la table. Une faute de frappe donne :

crontab: installing new crontab
"/tmp/crontab.Xb3kQe/crontab":6: bad hour
errors in crontab file, can't install.
Do you want to retry the same edit? (y/n)

Ces messages sont ceux du code de crontab.c et des correctifs Debian ; le numéro désigne la ligne fautive.

Pour une tâche déployée (par Ansible, par exemple), préférez un fichier dans /etc/cron.d/, qui porte le champ utilisateur et se versionne :

# /etc/cron.d/signalements-taches  (pas de point dans le nom !)
SHELL=/bin/bash
PATH=/opt/signalements/venv/bin:/usr/bin:/bin
MAILTO=""
0 2 * * *   signalements  cd /opt/signalements && flock -n /var/lib/signalements/export.lock venv/bin/python -m signalements.export 2>&1 | logger -t signalements-export
15 3 * * *  signalements  cd /opt/signalements && flock -n /var/lib/signalements/purge.lock venv/bin/python -m signalements.purge --jours 365 2>&1 | logger -t signalements-purge

La page cron(8) exige que ce fichier appartienne à root et ne soit pas modifiable par le groupe ou les autres. Il n'a pas besoin d'être exécutable, et cron le relit de lui-même : il vérifie chaque minute la date de modification de ses répertoires, sans redémarrage.

Passer à un minuteur systemd

Sur sig-outils, la décision de l'équipe est de migrer les deux tâches vers des minuteurs. Commencez par le service, dans /etc/systemd/system/signalements-export.service :

[Unit]
Description=Export CSV nocturne de Signalements pour la mairie
Wants=network-online.target
After=network-online.target
OnFailure=notifier-echec@%n.service

[Service]
Type=oneshot
User=signalements
Group=signalements
WorkingDirectory=/opt/signalements
EnvironmentFile=/etc/signalements/env
ExecStart=/opt/signalements/venv/bin/python -m signalements.export --sortie /srv/donnees/exports
ExecStartPost=-/usr/bin/curl -fsS -m 10 --retry 3 -o /dev/null https://vigie.lyneko.example/ping/2b6f0c1e-signalements-export
TimeoutStartSec=45min
Nice=10
IOSchedulingClass=idle

Ce qui change par rapport au service web de la leçon 11 de Premiers pas :

  • Type=oneshot : le service consiste à lancer un programme et attendre qu'il se termine. D'après systemd.service(5), il reste dans l'état activating pendant tout son travail, puis passe directement à inactive (succès) ou failed (échec) ; il n'est jamais active. Pas de section [Install] : on n'active pas le service, c'est le minuteur qui le démarre.
  • User=signalements et EnvironmentFile=/etc/signalements/env : le compte de service et son fichier d'environnement en 0640, qui contient la chaîne de connexion à sig-db, à la place du .pgpass de Camille.
  • Wants= et After=network-online.target : l'export interroge sig-db ; avec Persistent=true, il peut partir juste après un démarrage, avant le réseau.
  • OnFailure=notifier-echec@%n.service : si le service finit en failed, systemd démarre l'instance notifier-echec@signalements-export.service.service de l'unité modèle décrite plus bas. %n est le spécificateur du nom complet de l'unité.
  • ExecStartPost=-... : n'est exécuté que si ExecStart= a réussi. Il envoie un signal de vie au service de surveillance (voir « En production »). Le tiret initial fait ignorer l'échec de curl : une panne du service de surveillance ne doit pas faire passer l'export lui-même en échec. -f fait échouer curl sur une réponse HTTP d'erreur, -sS le rend silencieux sauf erreur, -m 10 borne la durée à dix secondes, --retry 3 réessaie trois fois.
  • TimeoutStartSec=45min : pour un oneshot, la page systemd.service(5) précise que le délai de démarrage est désactivé par défaut. Un export bloqué sur une requête SQL resterait donc activating indéfiniment, et le minuteur, voyant le service toujours actif, ne le relancerait jamais. Ce délai borne la durée totale ; au-delà, systemd tue la tâche et la marque en échec. RuntimeMaxSec= ne convient pas : il est sans effet sur un oneshot.
  • Nice=10 et IOSchedulingClass=idle : la tâche cède le processeur et le disque aux autres processus de la machine (réception des journaux notamment). La leçon 11 ira plus loin en plaçant les tâches de fond dans une tranche commune, taches.slice, avec des limites de mémoire.

Puis le minuteur, /etc/systemd/system/signalements-export.timer :

[Unit]
Description=Déclenche l'export CSV nocturne de Signalements

[Timer]
OnCalendar=*-*-* 05:00:00 Europe/Paris
Persistent=true
AccuracySec=1min

[Install]
WantedBy=timers.target
  • OnCalendar=... Europe/Paris : la mairie veut son fichier « avant l'ouverture des services, à 5 h ». Le serveur est en UTC, mais l'expression porte son propre fuseau : l'export suivra l'heure de Paris été comme hiver, sans toucher à l'horloge du système.
  • Persistent=true : si sig-outils est arrêté à 5 h (maintenance, redémarrage de noyau de la leçon 4), l'export partira dès le retour de la machine.
  • WantedBy=timers.target : c'est le minuteur que l'on active, et il se rattache à la cible qui regroupe les minuteurs au démarrage.

Avant de charger, validez le calendrier :

$ systemd-analyze calendar --iterations=3 '*-*-* 05:00:00 Europe/Paris'

Sortie typique, le 7 octobre 2026 en fin de matinée (les libellés sont ceux de analyze-calendar.c) :

  Original form: *-*-* 05:00:00 Europe/Paris
Normalized form: *-*-* 05:00:00 Europe/Paris
    Next elapse: Thu 2026-10-08 03:00:00 UTC
       From now: 16h left
   Iteration #2: Fri 2026-10-09 03:00:00 UTC
       From now: 1 day 16h left
   Iteration #3: Sat 2026-10-10 03:00:00 UTC
       From now: 2 days 16h left

Les échéances sont affichées dans le fuseau du système : 5 h à Paris en heure d'été, c'est 3 h UTC. Après le 25 octobre, ce sera 4 h UTC. Une expression invalide est refusée avec le message Failed to parse calendar specification '<expression>': Invalid argument.

Chargez et activez le minuteur, puis testez le service à la main, sans attendre la nuit :

$ sudo systemd-analyze verify /etc/systemd/system/signalements-export.service /etc/systemd/system/signalements-export.timer
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now signalements-export.timer
$ sudo systemctl start signalements-export.service
$ systemctl status signalements-export.service
$ journalctl -u signalements-export.service -n 30 --no-pager

systemctl start signalements-export.service lance la tâche exactement comme le fera le minuteur, avec le même compte, le même environnement, la même isolation. Comme le service est oneshot, la commande attend la fin de l'export avant de rendre la main, et renvoie une erreur s'il échoue. Un avantage décisif sur cron.

Vérifiez la planification :

$ systemctl list-timers 'signalements-*'

Sortie typique :

NEXT                        LEFT LAST                        PASSED UNIT            ACTIVATES
Thu 2026-10-08 03:00:00 UTC  16h Wed 2026-10-07 10:41:12 UTC 2min ago signalements-export.timer signalements-export.service

1 timers listed.
Pass --all to see loaded but inactive timers, too.

NEXT et LEFT : la prochaine échéance ; LAST et PASSED : le dernier déclenchement (ici, le lancement manuel). Un NEXT vide signale un minuteur qui ne se déclenchera plus.

La purge, avec un délai aléatoire

La purge n'a pas d'heure imposée ; elle doit seulement tourner chaque nuit, après l'export, sans gêner. /etc/systemd/system/signalements-purge.timer :

[Unit]
Description=Purge quotidienne des pièces jointes expirées de Signalements

[Timer]
OnCalendar=*-*-* 03:30:00 UTC
RandomizedDelaySec=30min
Persistent=true

[Install]
WantedBy=timers.target

et signalements-purge.service, sur le même modèle que l'export, avec ExecStart=/opt/signalements/venv/bin/python -m signalements.purge --jours 365 et son propre signal de vie. RandomizedDelaySec=30min étale le déclenchement entre 3 h 30 et 4 h UTC, ce qui prend tout son sens quand la même unité tourne sur des dizaines de serveurs.

Une tâche ponctuelle : systemd-run

Le développeur demande de relancer une réindexation complète ce soir à 22 h, une seule fois. Pas besoin d'écrire de fichier :

$ sudo systemd-run --on-calendar='2026-10-07 22:00:00 Europe/Paris' \
    --unit=sig-reindex --uid=signalements --gid=signalements \
    --working-directory=/opt/signalements \
    --property=EnvironmentFile=/etc/signalements/env \
    /opt/signalements/venv/bin/python -m signalements.reindex

Sortie typique (messages de run.c) :

Running timer as unit: sig-reindex.timer
Will run service as unit: sig-reindex.service

--on-calendar= crée un minuteur transitoire ; --unit= le nomme (sinon systemd invente un nom run-...) ; --uid=, --gid= et --working-directory= règlent le service ; --property= (-p) transmet n'importe quelle directive du service. Les unités transitoires vivent sous /run : elles disparaissent au redémarrage. systemctl list-timers sig-reindex.timer montre l'échéance, journalctl -u sig-reindex.service le résultat. Pour un délai relatif, --on-active=2h.

L'outil historique est at (echo "commande" | at 22:00, atq, atrm), avec sa variante batch qui attend une charge faible ; il faut installer le paquet at, et ses tâches ont les défauts de cron.

Les minuteurs d'un utilisateur

Un compte peut avoir ses minuteurs dans ~/.config/systemd/user/ (systemctl --user), mais ils ne tournent en son absence qu'après sudo loginctl enable-linger <compte>. Pour Signalements, préférez des unités système avec User=, visibles de tous les administrateurs.

Retirer l'ancienne crontab

Une fois les minuteurs validés sur une nuit au moins, retirez les lignes de la crontab de camille, sans quoi les tâches tourneront deux fois :

$ sudo crontab -l -u camille > ~/crontab-camille-avant-migration.txt
$ sudo crontab -e -u camille

Gardez la copie dans la fiche de serveur et notez le changement dans le journal des changements (leçon 1). crontab -r supprime toute la table sans confirmation, sauf avec -i : prudence, la touche r est voisine de e.

Sous le capot

Comment cron lance une tâche

Le démon cron (cron -f -P sur Ubuntu 24.04, comme l'a montré la leçon 11 de Premiers pas) charge toutes les crontabs en mémoire, puis dort jusqu'à la minute suivante. À chaque réveil, il vérifie la date de modification du spool et de /etc/crontab, recharge ce qui a changé, puis parcourt les entrées. Pour chaque entrée à lancer :

cron (PID 812, root)
 └─ CRON (fils, nom en majuscules)  ouvre une session PAM, change d'identité
     └─ /bin/sh -c "<commande>"     stdin, stdout, stderr branchés sur des tubes
         └─ python -m signalements.export

Le fils CRON ouvre une session PAM (/etc/pam.d/cron : pam_env, pam_limits, pam_loginuid), prend l'identité du compte, construit l'environnement et exécute le shell, dont il lit la sortie sur un tube pour l'envoyer à /usr/sbin/sendmail, ou la jeter.

Le % est un caractère spécial de cette chaîne : d'après crontab(5), tout % non échappé est remplacé par un saut de ligne, et ce qui suit le premier % est envoyé sur l'entrée standard de la commande. date +%F devient date +. Échappez (\%) ou passez par un script.

Deux conséquences moins connues :

  • les tâches cron vivent dans le cgroup de cron.service. La pile PAM de cron sur Debian et Ubuntu n'appelle pas pam_systemd, donc aucune session systemd n'est créée : systemd-cgls montre l'export sous /system.slice/cron.service, mêlé à toutes les autres tâches. Impossible de lui donner une limite de mémoire propre, ou de savoir ce qu'il consomme. Le KillMode=process de l'unité cron.service sert précisément à ne pas tuer les tâches en cours quand on redémarre le démon ;
  • pam_limits s'applique aux tâches cron, contrairement aux services systemd. Une limite posée dans /etc/security/limits.conf agit donc sous cron mais pas sous un minuteur, piège que la leçon 11 détaille.

Comment systemd déclenche un minuteur

Pour un minuteur calendaire, systemd calcule la prochaine échéance en temps réel (CLOCK_REALTIME), arme un réveil du noyau, et, à l'échéance, met en file un job de démarrage pour le service. Pour un minuteur monotone, l'horloge utilisée ne recule jamais et n'est pas affectée par les réglages d'heure. Quand l'horloge système saute (réglage par NTP, changement de fuseau), le code de timer.c recalcule l'échéance (Time change, recalculating next elapse en mode débogage).

Avec Persistent=true, la date du dernier déclenchement est écrite dans /var/lib/systemd/timers/stamp-<nom>.timer (ou ~/.local/share/systemd/timers/ pour un minuteur utilisateur). Au démarrage du minuteur, systemd compare cette date au calendrier : si au moins une échéance est passée entre-temps, il déclenche aussitôt (sous réserve de RandomizedDelaySec=). Une seule exécution de rattrapage, même si dix échéances ont été manquées. systemctl clean --what=state signalements-export.timer efface ce fichier.

Pourquoi un service ne se chevauche pas lui-même

systemd.timer(5) le dit sans ambiguïté : si le service est encore actif quand le minuteur expire, il n'est pas relancé, simplement laissé tel quel. Un oneshot en cours de travail est en état activating ; un second systemctl start pendant ce temps se rattache au job existant et attend sa fin, au lieu de lancer une deuxième instance. Une même unité ne peut tout simplement pas exister deux fois. C'est ce qui rend flock inutile pour une tâche lancée uniquement par son minuteur, et c'est aussi pourquoi TimeoutStartSec= est indispensable : sans lui, une exécution bloquée empêche toutes les suivantes, en silence.

Comment flock verrouille

flock(1) appelle l'appel système flock(2) sur un fichier : le noyau associe un verrou à la description de fichier ouverte. Le verrou est consultatif (seuls les processus qui le demandent le respectent) et il est libéré automatiquement quand le dernier descripteur se ferme, y compris si le processus est tué par SIGKILL. Pas de fichier .pid périmé à nettoyer après un plantage : c'est toute la différence avec les verrous artisanaux du type « si le fichier existe, sortir ». Le fichier de verrou se place dans un répertoire qui appartient au compte de la tâche (ici /var/lib/signalements/) : sur Debian et Ubuntu, /run/lock est un tmpfs en 1777, ouvert à tous comme /var/tmp, et un répertoire ouvert à tous permettrait à n'importe quel compte de créer le fichier avant vous et de bloquer la tâche indéfiniment. D'après flock(1), l'échec d'acquisition avec -n renvoie le code 1 par défaut, modifiable avec -E ; -w <secondes> attend un temps borné au lieu d'échouer immédiatement.

Pièges courants

Le jour du mois et le jour de la semaine se combinent par OU. 0 2 1 * 1 ne signifie pas « le 1er du mois si c'est un lundi » mais « le 1er de chaque mois et chaque lundi ». crontab(5) le précise : quand les deux champs sont restreints, la commande tourne si l'un ou l'autre correspond. Subtilité supplémentaire documentée dans la même page : l'implémentation ne regarde que le premier caractère du champ ; 0 0 */2 * sun commence par une étoile, donc compte comme non restreint, et tourne les dimanches de date impaire seulement. Avec un minuteur, Mon *-*-01 veut bien dire « le 1er, si c'est un lundi ».

Un script dans /etc/cron.daily/ qui ne tourne jamais. Son nom contient un point (sauvegarde.sh), ou il n'est pas exécutable. run-parts l'ignore sans un mot. Diagnostic : run-parts --test /etc/cron.daily liste ce qui serait exécuté ; si votre script n'y figure pas, renommez-le (sauvegarde) et vérifiez chmod 755.

Un fichier de /etc/cron.d/ ignoré. Même règle de nom : /etc/cron.d/signalements-taches.cron n'est jamais lu, sans message. S'il appartient à un autre compte que root (un cp fait par un utilisateur, un déploiement mal configuré), cron journalise (*system*signalements-taches) WRONG FILE OWNER (/etc/cron.d/signalements-taches) et l'ignore. Le préfixe *system* est la façon dont le code de Debian nomme les crontabs système.

Le champ utilisateur oublié, ou ajouté à tort. Dans /etc/cron.d/, sans champ utilisateur, le chemin de la commande est pris pour un nom de compte ; dans une crontab utilisateur, un champ signalements en trop devient une commande introuvable.

command not found sous cron seulement. Le PATH de cron est /usr/bin:/bin : un outil de /usr/local/bin ou de l'environnement virtuel n'est pas trouvé. Chemin absolu ou PATH= en tête de crontab. Sous systemd, le PATH est fixe aussi (/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin, d'après systemd.exec(5)), mais systemctl start le révèle tout de suite.

Le % qui coupe la commande. tar czf /srv/sauvegardes/conf-$(date +%F).tgz /etc/signalements produit une archive nommée conf-.tgz ou un message d'erreur de syntaxe du shell. Échappez (\%F) ou passez par un script.

La dernière ligne sans saut de ligne. crontab refuse le fichier avec new crontab file is missing newline before EOF, can't install., et pour /etc/crontab ou /etc/cron.d, édités sans passer par crontab, cron considère la table comme au moins partiellement cassée et écrit un avertissement dans le journal.

Activer le service au lieu du minuteur. sudo systemctl enable signalements-export.service répond par un long message qui commence par The unit files have no installation config (WantedBy=, RequiredBy=, UpheldBy=, Also=, or Alias= settings in the [Install] section... : normal, le service n'a pas de section [Install]. C'est signalements-export.timer qu'il faut activer. Autre variante : systemctl start signalements-export.timer sans enable, et le minuteur ne revient pas après le redémarrage suivant.

Un minuteur sans déclencheur. Une directive mal orthographiée (OnCalender=) laisse la section [Timer] vide : Timer unit lacks value setting. Refusing. (texte de timer.c). systemd-analyze verify l'aurait signalé.

Un minuteur qui ne se déclenche plus, sans erreur. systemctl list-timers montre un LAST vieux de plusieurs jours, et le service est en activating depuis tout ce temps : une exécution bloquée, sans TimeoutStartSec=, empêche toutes les suivantes. systemctl status signalements-export.service le montre ; systemctl stop signalements-export.service débloque, et le délai l'évitera à l'avenir.

Persistent=true après une longue coupure. Une machine restaurée d'un vieil instantané relance aussitôt ses tâches. Pour une tâche qui ne doit pas tourner en retard, pas de Persistent=.

Le changement d'heure. Une tâche à 2 h 30, heure de Paris, et le dernier dimanche de mars, l'heure passe de 2 h à 3 h : 2 h 30 n'existe pas. En octobre, de 3 h on revient à 2 h : 2 h 30 existe deux fois. cron(8) de Debian documente son comportement : pour un saut de moins de trois heures en avant, les tâches de l'intervalle sauté sont lancées « peu après » le changement ; en arrière, elles ne sont pas relancées pendant l'heure répétée ; les tâches à étoile (* dans l'heure ou la minute) suivent simplement la nouvelle heure. Pour systemd, ce cas n'est pas documenté et a fait l'objet de rapports d'anomalie selon les versions. Parades, par ordre de préférence : serveur et horaires en UTC ; sinon, fuseau explicite hors de la plage 2 h à 3 h ; et vérification avec systemd-analyze calendar --base-time='2027-03-28 00:00 Europe/Paris' --iterations=2 '<expression>'. Le prochain changement, le 25 octobre 2026, est dans moins de trois semaines.

Sécurité

  • Une tâche planifiée est un point de persistance favori des attaquants. Une ligne ajoutée dans une crontab ou un minuteur dans /etc/systemd/system/ survit aux redémarrages et passe inaperçue. La matrice MITRE ATT&CK en fait deux techniques à part entière (T1053.003 pour cron, T1053.006 pour les minuteurs systemd). Inventoriez les tâches comme vous inventoriez les comptes, et faites surveiller /etc/cron*, /var/spool/cron/ et /etc/systemd/system/ par l'audit de la leçon 15.
  • Le script appelé compte autant que la ligne qui l'appelle. Une crontab de root qui lance un script de /opt/signalements/bin/, si ce fichier ou son répertoire est modifiable par le compte signalements, donne à quiconque contrôle ce compte une exécution en tant que root chaque nuit. Règle : une tâche tourne sous le compte le moins privilégié possible (moindre privilège), et ce que root exécute n'est modifiable que par root.
  • Qui a le droit de planifier. Sur Debian et Ubuntu, d'après crontab(1), tout compte peut avoir une crontab, sauf si /etc/cron.allow (liste blanche, prioritaire) ou /etc/cron.deny existent. Sur un serveur, une liste blanche réduite à root et aux comptes de service est une mesure simple. Un compte en /usr/sbin/nologin peut quand même avoir une crontab : cron utilise SHELL, pas le shell de connexion.
  • Les secrets. Une DATABASE_URL écrite en clair dans une crontab est lisible par root et par toute sauvegarde de /var/spool/cron ; dans Environment= d'une unité, elle est lisible par tous via systemctl cat. Utilisez EnvironmentFile= vers un fichier en 0640, ou les credentials de systemd (cours systemd en profondeur).
  • Durcir l'unité de tâche. Un service oneshot accepte toutes les options d'isolation de systemd : NoNewPrivileges=yes, ProtectSystem=strict avec ReadWritePaths=/srv/donnees/exports, PrivateTmp=yes, ProtectHome=yes. C'est un argument fort en faveur des minuteurs : cron n'offre rien d'équivalent. La leçon 14 applique ces options et les mesure avec systemd-analyze security.
  • Les données manipulées. L'export contient des données personnelles : /srv/donnees/exports en 0750, au compte de service, et purgé lui aussi. Une purge RGPD qui échoue en silence est une non-conformité qui s'accumule.

En production

Choisir entre cron et un minuteur

Critèrecronminuteur systemd
Sortie et erreurscourrier, souvent perdu ; à rediriger soi-mêmejournal de l'unité, automatiquement, avec le code de sortie
Tester la tâchereproduire l'environnement à la mainsystemctl start : exactement les conditions réelles
Machine éteinte à l'heure prévuetâche perdue (sauf anacron, par période)Persistent=true, tâche par tâche
Chevauchementà gérer (flock)impossible par construction
Ressources et isolationaucune, tout dans cron.servicecgroup propre, limites, durcissement
DépendancesaucuneAfter=, Wants=, OnFailure=
Fuseau horaireun seul, celui du démonpar expression (Europe/Paris)
Étalement sur un parcà scripter (sleep $((RANDOM % 1800)))RandomizedDelaySec=
Lisibilitéune ligne, connue de tousdeux fichiers, plus verbeux
Portabilitétous les Unix, conteneurs minimalistesmachines sous systemd uniquement

Pour un serveur Debian ou Ubuntu que vous administrez, le minuteur l'emporte sur presque tous les critères qui comptent en exploitation. Debian elle-même va dans ce sens : plusieurs paquets ont remplacé leur tâche cron par un minuteur pour trixie, et de nombreux scripts de /etc/cron.daily/ (celui de logrotate, par exemple) se terminent immédiatement s'ils détectent systemd, laissant le travail à leur minuteur. cron reste légitime pour des tâches personnelles d'utilisateur, pour un script qui doit tourner sur d'autres Unix, ou quand une équipe entière le maîtrise et que la tâche est triviale.

Rendre une tâche robuste

  • Codes de sortie honnêtes. Le planificateur ne sait que ce que le code de sortie lui dit. Un script Bash commence par set -euo pipefail (leçon 6 de Premiers pas) ; un script Python termine par sys.exit(1) en cas d'erreur. Une tâche qui affiche « erreur » et sort avec 0 est un succès pour systemd.
  • Idempotence. Relancer la tâche ne doit pas faire de dégât : l'export du 7 octobre écrit signalements-2026-10-07.csv dans un fichier temporaire puis le renomme (mv est atomique dans un même système de fichiers), si bien qu'une relance remplace le fichier au lieu de produire un doublon et que la mairie ne lit jamais un fichier à moitié écrit ; la purge supprime « ce qui a plus de 365 jours », jamais « les 500 plus anciens ». Une tâche idempotente se rattrape sans réfléchir : systemctl start signalements-export.service.
  • Durée bornée : TimeoutStartSec= pour un minuteur, timeout 45m devant la commande sous cron.
  • Une tâche, une unité. Ne regroupez pas export et purge dans un même script : l'échec de l'un masquerait l'autre, et l'on ne pourrait pas les relancer séparément.

Être prévenu de l'échec : OnFailure=

L'unité modèle /etc/systemd/system/notifier-echec@.service :

[Unit]
Description=Notification d'échec de %i

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/notifier-echec %i

et le script /usr/local/sbin/notifier-echec (appartenant à root, 0755) :

#!/bin/bash
set -euo pipefail
unite="$1"
extrait=$(journalctl -o cat --no-pager -n 20 _SYSTEMD_INVOCATION_ID="${MONITOR_INVOCATION_ID:-}" || true)
texte="Échec de ${unite} sur $(hostname) : ${MONITOR_SERVICE_RESULT:-?} (${MONITOR_EXIT_CODE:-?} ${MONITOR_EXIT_STATUS:-?})
${extrait}"
curl -fsS -m 10 --retry 3 -o /dev/null \
  --data-urlencode "text=${texte}" \
  "$(cat /etc/notifier-echec/webhook-url)"

Depuis systemd 251, systemd.exec(5) documente les variables transmises au service déclenché par OnFailure= : MONITOR_SERVICE_RESULT (par exemple exit-code ou timeout), MONITOR_EXIT_CODE (exited, killed), MONITOR_EXIT_STATUS (le code ou le signal), MONITOR_INVOCATION_ID et MONITOR_UNIT. L'identifiant d'invocation permet d'extraire du journal les lignes de cette exécution-là, et pas celles de la veille. La même page prévient que ces variables ne sont pas transmises quand plusieurs unités partagent un même gestionnaire non paramétré, d'où l'unité modèle @ instanciée avec %n. L'URL du webhook (messagerie d'équipe, outil d'astreinte) est un secret, rangé dans un fichier lisible par root seulement.

Être prévenu du silence : le signal de vie

OnFailure= ne couvre que les échecs visibles. Il reste muet si le minuteur a été désactivé par erreur, si la machine est éteinte, si le service est bloqué, ou si quelqu'un a supprimé l'unité. La parade est la surveillance par signal de vie (dead man's switch, littéralement « dispositif de l'homme mort », par analogie avec la pédale qui arrête un train si le conducteur la relâche) : la tâche signale chaque succès à un service extérieur, et c'est l'absence de signal au-delà d'un délai qui déclenche l'alerte. C'est le rôle du ExecStartPost= de signalements-export.service.

Plusieurs outils libres et auto-hébergeables le font : Healthchecks (ses points d'entrée /start, /fail et /<code de sortie> permettent aussi de mesurer la durée et de remonter l'échec), Uptime Kuma (moniteurs de type push). Hébergez-le hors de la machine surveillée, idéalement dans une autre zone. Une alternative sans nouvel outil : la tâche écrit l'horodatage de son dernier succès dans une métrique (fichier lu par le collecteur textfile de node_exporter, par exemple), et une alerte se déclenche si time() - dernier_succes dépasse 26 heures, dans Prometheus ou dans Cockpit. Gardez une marge (26 heures, pas 24) contre les fausses alertes.

À l'échelle d'un parc et au-delà

  • Les tâches sont du code : versionnées, déployées par l'automatisation (cours Ansible : les fondamentaux), documentées avec leur procédure de relance (runbook).
  • Une tâche « singleton » sur plusieurs machines (la purge ne doit tourner qu'une fois, même si l'on ajoute un second sig-outils) demande un verrou partagé, par exemple un verrou consultatif PostgreSQL (pg_try_advisory_lock) pris par le script dans sig-db. flock et l'unicité des unités systemd ne valent que pour une machine.
  • Sortir les tâches des machines. Dans un cluster Kubernetes, le CronJob reprend la syntaxe de cron, avec une politique de concurrence (concurrencyPolicy: Forbid) et un rattrapage borné ; chez Scaleway, les Serverless Jobs acceptent aussi une planification de type cron (leçon sur les Functions et Jobs). Les règles de cette leçon s'y appliquent à l'identique.

Exercices

1. Lire des lignes de crontab (niveau 100). Que font ces lignes, situées dans /etc/cron.d/sig-divers ?

*/10 8-18 * * 1-5  signalements  /opt/signalements/bin/verifier-file
0 4 1,15 * 0       root          /usr/local/sbin/rapport
@reboot            signalements  /opt/signalements/bin/rechauffer-cache
Solution
  • La première tourne toutes les dix minutes (0, 10, 20... 50) de 8 h à 18 h 50, du lundi au vendredi, sous le compte signalements.
  • La deuxième tourne à 4 h le 1er et le 15 de chaque mois, et chaque dimanche à 4 h : jour du mois et jour de la semaine sont tous deux restreints, donc combinés par OU. Si l'intention était « le 1er et le 15 seulement », il faut mettre * dans le dernier champ.
  • La troisième tourne au démarrage du démon cron, ce qui peut précéder la disponibilité du réseau ou de la base : à remplacer par un service ordonné après network-online.target.

Enfin, le nom du fichier, sig-divers, respecte la règle de run-parts : pas de point.

2. Traduire en minuteur (niveau 200). Écrivez la valeur OnCalendar= équivalente à chacune des lignes 1 et 2 ci-dessus (dans l'intention réelle « le 1er et le 15 » pour la seconde), et la commande qui vérifie vos expressions.

Solution
  • OnCalendar=Mon..Fri *-*-* 08..18:00/10 : du lundi au vendredi, heures 8 à 18, minutes 0, 10, 20... 50.
  • OnCalendar=*-*-01,15 04:00.

Vérification : systemd-analyze calendar --iterations=5 'Mon..Fri *-*-* 08..18:00/10' affiche la forme normalisée (Mon..Fri *-*-* 08..18:00/10:00) et les cinq prochaines échéances. Si l'on voulait réellement le comportement de cron (« le 1er, le 15, ou un dimanche »), il faudrait deux lignes OnCalendar= dans le même minuteur, *-*-01,15 04:00 et Sun *-*-* 04:00, puisque les déclencheurs multiples s'additionnent.

3. L'export qui marche à la main (niveau 200). Un collègue a ajouté dans sa crontab : 0 6 * * * pg_dump -Fc signalements > /srv/sauvegardes/sig-$(date +%F).dump. Le fichier n'apparaît jamais, alors que la commande fonctionne dans son terminal. Donnez au moins trois causes possibles, la commande pour trouver laquelle est en jeu, et une version corrigée.

Solution

Causes : le % non échappé coupe la commande à date + (la suite part sur l'entrée standard) ; pg_dump n'est peut-être pas dans /usr/bin:/bin ; les variables de connexion (PGHOST, PGPASSWORD ou ~/.pgpass) définies dans le .bashrc du collègue n'existent pas sous cron ; le compte n'a peut-être pas le droit d'écrire dans /srv/sauvegardes. Aucun message n'est visible parce qu'il n'y a pas de MTA.

Diagnostic : journalctl -u cron --since today montre la ligne CMD (...) tronquée au premier %, puis (CRON) info (No MTA installed, discarding output).

Correction : un script /usr/local/bin/sauver-sig (chemins absolus, set -euo pipefail, variables lues dans un fichier en 0600), lancé par un minuteur OnCalendar=*-*-* 06:00 avec un service oneshot, User= dédié et OnFailure=. Le résultat se lit alors dans journalctl -u sauver-sig.service. Et pour une vraie sauvegarde, la leçon 16 va beaucoup plus loin.

4. Concevoir la surveillance (niveau 300). La purge RGPD de sig-outils doit tourner chaque nuit. Listez les pannes possibles qui l'empêcheraient d'aboutir, indiquez pour chacune quel mécanisme de la leçon la détecte, et proposez la configuration complète (unités et alertes) qui garantit d'être prévenu dans les 26 heures, quelle que soit la panne.

Solution
PanneDétectée par
la commande échoue (base injoignable, bogue)code de sortie non nul, état failed, OnFailure=
la commande se bloqueTimeoutStartSec= la tue, état failed, OnFailure=
exécution précédente encore en coursle minuteur ne relance pas ; si elle dure trop, TimeoutStartSec=
minuteur désactivé ou supprimé, unité casséeabsence de signal de vie
machine éteinte ou injoignableabsence de signal de vie (et Persistent=true rattrape au retour)
la commande « réussit » sans rien purger (mauvais paramètre)seulement si le script vérifie son propre résultat et sort en erreur, ou publie le nombre d'éléments purgés en métrique
la notification d'échec elle-même échoue (webhook en panne)absence de signal de vie

Configuration : signalements-purge.service en oneshot avec User=signalements, TimeoutStartSec=1h, OnFailure=notifier-echec@%n.service, ExecStartPost=-curl ... vers le service de signal de vie ; signalements-purge.timer avec OnCalendar=*-*-* 03:30 UTC, RandomizedDelaySec=30min, Persistent=true, activé. Côté surveillance, un contrôle attendu toutes les 24 heures avec une tolérance de 2 heures, hébergé hors de sig-outils, qui alerte l'astreinte. Enfin, le script de purge publie le nombre de pièces jointes supprimées : une valeur nulle plusieurs jours de suite, alors que des signalements arrivent tous les jours, mérite aussi une alerte. Le signal de vie est le seul mécanisme qui couvre les pannes du dispositif de notification lui-même : c'est pour cela qu'il est indispensable.

Récapitulatif

  • cron : cinq champs, jour du mois et jour de la semaine combinés par OU ; crontabs utilisateur (crontab -e, sans champ utilisateur) et système (/etc/crontab, /etc/cron.d/, avec champ utilisateur) ; pas de point dans les noms pour run-parts et /etc/cron.d ; PATH=/usr/bin:/bin ; % à échapper ; sortie envoyée par courrier, donc perdue sans MTA.
  • Minuteurs systemd : une paire .timer (quand) et .service oneshot (quoi) ; OnCalendar= validé par systemd-analyze calendar, avec fuseau possible ; Persistent=true pour rattraper, RandomizedDelaySec= pour étaler, AccuracySec= d'une minute par défaut ; on active le minuteur, on teste le service avec systemctl start ; systemctl list-timers pour l'état.
  • systemd-run --on-calendar= pour une tâche ponctuelle ; loginctl enable-linger pour les minuteurs d'un utilisateur absent.
  • Robustesse : codes de sortie honnêtes, idempotence, durée bornée (TimeoutStartSec=, désactivé par défaut pour un oneshot), pas de chevauchement (natif sous systemd, flock -n sous cron).
  • Être prévenu : OnFailure= vers une unité modèle qui exploite MONITOR_*, et un signal de vie dont l'absence déclenche l'alerte.
  • Changement d'heure : serveurs en UTC, ou fuseau explicite hors de la plage 2 h à 3 h ; vérifier avec --base-time.

Pour aller plus loin

  • Les pages crontab(5) et cron(8) de Debian, dont les sections DEBIAN SPECIFIC et LIMITATIONS répondent à la plupart des comportements surprenants.
  • Les pages systemd.timer(5) et systemd.time(7), pour toutes les formes d'expressions calendaires et de durées.
  • La page flock(1), et son modèle de script qui se verrouille lui-même.
  • Le cours systemd en profondeur, pour les credentials, l'activation par chemin (.path, déclencher une tâche à l'arrivée d'un fichier) et le durcissement complet des unités.
  • La leçon suivante, Borner les ressources : limites et cgroups, qui reprend l'export de sig-outils le jour où il sature la mémoire de la machine.
+20 XP Carte du ciel →Mon cosmonaute →

Sources