Planifier des tâches : cron et minuteurs
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, sonPATHest 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/exporterChaque 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 :
| Emplacement | Format | Qui l'écrit | Remarque |
|---|---|---|---|
/var/spool/cron/crontabs/<compte> | 5 champs + commande | crontab -e du compte | jamais édité à la main ; le répertoire n'est accessible qu'au groupe crontab |
/etc/crontab | 5 champs + utilisateur + commande | l'administrateur | lance cron.hourly, cron.daily, etc. |
/etc/cron.d/<fichier> | 5 champs + utilisateur + commande | paquets et administrateur | un fichier par usage, se déploie bien par automatisation |
/etc/cron.{hourly,daily,weekly,monthly}/ | scripts exécutables | paquets et administrateur | lancé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 :
SHELLvaut/bin/sh(/usr/bin/shsur 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};HOMEetLOGNAMEviennent de/etc/passwd;PATHvaut/usr/bin:/bin(constante_PATH_DEFPATHdu code), sauf si la crontab le redéfinit. La page le dit explicitement : les variables chargées par PAM depuis/etc/environmentne remplacent pas ce réglage. Une commande dans/usr/sbinou/usr/local/binn'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 typeoneshot, 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 :
| Besoin | cron | OnCalendar |
|---|---|---|
| tous les jours à 2 h 30 | 30 2 * * * | *-*-* 02:30:00 (ou 02:30) |
| du lundi au vendredi à 5 h | 0 5 * * 1-5 | Mon..Fri *-*-* 05:00 |
| toutes les 15 minutes | */15 * * * * | *:0/15 |
| le 1er du mois à minuit | 0 0 1 * * | monthly ou *-*-01 00:00 |
| le dernier jour du mois à 23 h | impossible directement | *-*~01 23:00 |
| à 5 h, heure de Paris, sur un serveur en UTC | impossible (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=truerend 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.logL'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-exportLigne 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/binou 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'étiquettesignalements-export;journalctl -t signalements-exportla retrouve. Le code de sortie de la ligne devient celui delogger: 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-purgeLa 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=idleCe 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èssystemd.service(5), il reste dans l'étatactivatingpendant tout son travail, puis passe directement àinactive(succès) oufailed(échec) ; il n'est jamaisactive. Pas de section[Install]: on n'active pas le service, c'est le minuteur qui le démarre.User=signalementsetEnvironmentFile=/etc/signalements/env: le compte de service et son fichier d'environnement en0640, qui contient la chaîne de connexion àsig-db, à la place du.pgpassde Camille.Wants=etAfter=network-online.target: l'export interrogesig-db; avecPersistent=true, il peut partir juste après un démarrage, avant le réseau.OnFailure=notifier-echec@%n.service: si le service finit enfailed, systemd démarre l'instancenotifier-echec@signalements-export.service.servicede l'unité modèle décrite plus bas.%nest le spécificateur du nom complet de l'unité.ExecStartPost=-...: n'est exécuté que siExecStart=a réussi. Il envoie un signal de vie au service de surveillance (voir « En production »). Le tiret initial fait ignorer l'échec decurl: une panne du service de surveillance ne doit pas faire passer l'export lui-même en échec.-ffait échouercurlsur une réponse HTTP d'erreur,-sSle rend silencieux sauf erreur,-m 10borne la durée à dix secondes,--retry 3réessaie trois fois.TimeoutStartSec=45min: pour unoneshot, la pagesystemd.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 doncactivatingindé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 unoneshot.Nice=10etIOSchedulingClass=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.targetOnCalendar=... 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: sisig-outilsest 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 leftLes é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.targetet 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.exportLe 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 paspam_systemd, donc aucune session systemd n'est créée :systemd-cglsmontre 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. LeKillMode=processde l'unitécron.servicesert précisément à ne pas tuer les tâches en cours quand on redémarre le démon ; pam_limitss'applique aux tâches cron, contrairement aux services systemd. Une limite posée dans/etc/security/limits.confagit 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 comptesignalements, 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.denyexistent. Sur un serveur, une liste blanche réduite à root et aux comptes de service est une mesure simple. Un compte en/usr/sbin/nologinpeut quand même avoir une crontab :cronutiliseSHELL, 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; dansEnvironment=d'une unité, elle est lisible par tous viasystemctl cat. UtilisezEnvironmentFile=vers un fichier en0640, ou les credentials de systemd (cours systemd en profondeur). - Durcir l'unité de tâche. Un service
oneshotaccepte toutes les options d'isolation de systemd :NoNewPrivileges=yes,ProtectSystem=strictavecReadWritePaths=/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 avecsystemd-analyze security. - Les données manipulées. L'export contient des données personnelles :
/srv/donnees/exportsen0750, 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ère | cron | minuteur systemd |
|---|---|---|
| Sortie et erreurs | courrier, souvent perdu ; à rediriger soi-même | journal de l'unité, automatiquement, avec le code de sortie |
| Tester la tâche | reproduire l'environnement à la main | systemctl start : exactement les conditions réelles |
| Machine éteinte à l'heure prévue | tâche perdue (sauf anacron, par période) | Persistent=true, tâche par tâche |
| Chevauchement | à gérer (flock) | impossible par construction |
| Ressources et isolation | aucune, tout dans cron.service | cgroup propre, limites, durcissement |
| Dépendances | aucune | After=, Wants=, OnFailure= |
| Fuseau horaire | un seul, celui du démon | par expression (Europe/Paris) |
| Étalement sur un parc | à scripter (sleep $((RANDOM % 1800))) | RandomizedDelaySec= |
| Lisibilité | une ligne, connue de tous | deux fichiers, plus verbeux |
| Portabilité | tous les Unix, conteneurs minimalistes | machines 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 parsys.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.csvdans un fichier temporaire puis le renomme (mvest 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 45mdevant 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 %iet 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 danssig-db.flocket 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-cacheSolution
- 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èsnetwork-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
| Panne | Détectée par |
|---|---|
| la commande échoue (base injoignable, bogue) | code de sortie non nul, état failed, OnFailure= |
| la commande se bloque | TimeoutStartSec= la tue, état failed, OnFailure= |
| exécution précédente encore en cours | le minuteur ne relance pas ; si elle dure trop, TimeoutStartSec= |
| minuteur désactivé ou supprimé, unité cassée | absence de signal de vie |
| machine éteinte ou injoignable | absence 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 pourrun-partset/etc/cron.d;PATH=/usr/bin:/bin;%à échapper ; sortie envoyée par courrier, donc perdue sans MTA. - Minuteurs systemd : une paire
.timer(quand) et.serviceoneshot(quoi) ;OnCalendar=validé parsystemd-analyze calendar, avec fuseau possible ;Persistent=truepour rattraper,RandomizedDelaySec=pour étaler,AccuracySec=d'une minute par défaut ; on active le minuteur, on teste le service avecsystemctl start;systemctl list-timerspour l'état. systemd-run --on-calendar=pour une tâche ponctuelle ;loginctl enable-lingerpour les minuteurs d'un utilisateur absent.- Robustesse : codes de sortie honnêtes, idempotence, durée bornée (
TimeoutStartSec=, désactivé par défaut pour unoneshot), pas de chevauchement (natif sous systemd,flock -nsous cron). - Être prévenu :
OnFailure=vers une unité modèle qui exploiteMONITOR_*, 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)etcron(8)de Debian, dont les sections DEBIAN SPECIFIC et LIMITATIONS répondent à la plupart des comportements surprenants. - Les pages
systemd.timer(5)etsystemd.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-outilsle jour où il sature la mémoire de la machine.
Sources
- Debian, page de manuel crontab(5), paquet cron 3.0pl1-197
- Debian, page de manuel cron(8), sections DEBIAN SPECIFIC et changements d'heure
- Debian, page de manuel crontab(1), cron.allow et cron.deny
- Debian, code source et correctifs du paquet cron (pathnames.h, entry.c, crontab.c, debian/patches)
- Debian, page de manuel run-parts(8)
- util-linux, page de manuel flock(1)
- systemd, page de manuel systemd.timer(5)
- systemd, page de manuel systemd.time(7), section Calendar Events
- systemd, page de manuel systemd.service(5), Type=oneshot et TimeoutStartSec=
- systemd, page de manuel systemd.unit(5), OnFailure= et spécificateurs
- systemd, page de manuel systemd.exec(5), variables $PATH et $MONITOR_*
- systemd, pages de manuel systemd-run(1) et systemd-analyze(1)
- systemd, page de manuel loginctl(1), enable-linger
- systemd, code source v255 : src/core/timer.c, src/analyze/analyze-calendar.c, src/run/run.c
- Debian, bogue n° 1107573 : documenter les tâches cron remplacées par des minuteurs dans trixie
- MITRE ATT&CK, T1053 Scheduled Task/Job (sous-techniques Cron et Systemd Timers)
- Healthchecks.io, documentation de l'API de signalement (ping)
- Debian, paquets cron, util-linux, debianutils, anacron et systemd dans trixie
- Ubuntu, paquets cron, util-linux, debianutils, anacron et systemd dans noble