Si vous avez déjà été réveillé parce que « la tâche nocturne ne s’est pas exécutée », vous connaissez déjà le problème : la planification est ennuyeuse jusqu’à ce qu’elle devienne la seule chose qui compte. Cron est simple, omniprésent et… étonnamment doué pour échouer en silence.
Sur Debian 13, les timers systemd sont l’option adulte : meilleure observabilité, gestion des dépendances sensée, rattrapage des exécutions manquées et modèle d’échec que l’on peut réellement automatiser. Ils ont aussi quelques arêtes vives qui vous entraîneront des ennuis si vous les utilisez mal.
La décision : quand rester avec cron et quand migrer
Soyons francs : cron suffit pour une quantité surprenante de choses. Si vous avez un hôte unique, quelques scripts, et que « ça a tourné à un moment donné » suffit comme critère de succès, cron fait encore bien le boulot.
Mais les systèmes de production ne tombent pas en panne sur le « chemin heureux ». Ils échouent pendant les redémarrages, les déploiements, les incidents DNS, les disques pleins, les montages NFS bloqués et les changements d’environnement accidentels. C’est là que la simplicité de cron devient une charge opérationnelle.
Utiliser cron quand
- Vous avez besoin d’un calendrier et rien d’autre.
- Vos hôtes sont stables, dépendances minimales, et vous surveillez activement les résultats.
- Vous maintenez des workloads legacy et une migration ajouterait du risque pour peu de bénéfice.
- Vous exécutez dans un conteneur où systemd n’est pas PID 1 et vous ne voulez pas lancer ce combat.
Utiliser les timers systemd quand
- Vous tenez aux exécutions manquées après un redémarrage ou une indisponibilité (Persistent=true est une ligne qui sauve des carrières).
- Vous voulez des logs dans le journal, avec des métadonnées cohérentes et un filtrage simple.
- Vous avez besoin d’ordonnancement de dépendances (after network-online, après des montages, après un service de base de données).
- Vous avez besoin de contrôle des ressources : plafonds CPU/IO, timeouts et isolation.
- Vous voulez une surveillance qui n’est pas « avons-nous reçu un mail de cron ? »
- Vous voulez empêcher les exécutions qui se chevauchent sans empiler des fichiers de verrou fragiles.
Conseil d’opinion : sur Debian 13, les nouvelles tâches planifiées devraient par défaut utiliser les timers systemd sauf raison claire de ne pas le faire. Cron devient l’exception, pas la règle.
Une citation qui reste pertinente en exploitation : « paraphrased idea » — l’idée de John Ousterhout selon laquelle « la complexité est l’ennemie de la fiabilité ». Les timers ne sont pas une complexité inutile ; ils déplacent la complexité au bon endroit (le gestionnaire de services) au lieu de la disperser dans des scripts.
Faits intéressants et un peu d’histoire (parce que ça compte)
Comprendre comment nous en sommes arrivés là vous aide à prédire les modes de défaillance. La planification a toujours été moins à propos de « exécuter à 2h » et plus à propos de « exécuter quand tout brûle et quand même rester correct ».
- Cron date de la fin des années 1970, conçu à l’origine pour Unix pour exécuter des tâches périodiques avec un minimum de surcharge. Il suppose que l’hôte est up et que l’horloge est correcte.
- Le cron classique utilise des crontabs par utilisateur et un crontab système global ; cette séparation est pratique, mais elle fragmente aussi la propriété et l’auditabilité.
- Anacron a été introduit pour traiter les tâches manquées sur des machines qui ne sont pas toujours allumées (comme les portables). Les timers systemd reprennent effectivement cette idée de rattrapage après une indisponibilité.
- Les timers systemd viennent du modèle « units partout » : une exécution planifiée n’est qu’un déclencheur pour une unité de service. C’est pour cela que vous obtenez de l’ordonnancement de dépendances et des logs cohérents.
- L’écosystème cron de Debian s’est historiquement appuyé sur l’email pour l’alerte. Mais les environnements modernes n’ont souvent pas de MTA local configuré, donc les échecs deviennent silencieux.
- systemd s’intègre aux cgroups. Cela signifie que vous pouvez brider les tâches gourmandes ou protéger le reste du système — chose que cron n’a jamais tenté de faire.
- Les timers systemd supportent la randomisation pour éviter la « foule tonitruante » de milliers d’hôtes frappant la même API à minuit.
- La synchronisation temporelle est devenue critique dès que les systèmes distribués se sont généralisés. Cron et timers souffrent tous deux d’horloges incorrectes, mais systemd fournit des preuves et des outils d’ordonnancement plus clairs.
- Les logs sont passés des fichiers aux journaux dans de nombreux déploiements Debian. Les timers en tirent parti directement car stdout/stderr sont capturés sans bricolage.
Blague n°1 : Cron est comme ce collègue qui « a totalement envoyé le mail » — vous ne saurez que ça n’a pas marché que lorsque quelqu’un se plaindra.
Modèle mental : ce que fait cron vs ce que fait systemd
Cron : un ordonnanceur minimal
Cron lit les entrées de crontab, se réveille une fois par minute et vérifie si une tâche doit s’exécuter. Il lance des commandes avec un environnement minimal, sous une identité utilisateur, avec la sortie éventuellement envoyée par mail. C’est à peu près tout.
Ses forces sont aussi ses faiblesses :
- Force : simplicité prévisible. Faiblesse : vous devez implémenter vous-même tout le reste (verrouillage, nouvelles tentatives, timeouts, dépendances, logging, alerting).
- Force : largement compris. Faiblesse : « compris » signifie souvent « supposé », et les suppositions pourrissent.
Timers systemd : un déclencheur plus un contrat de service
Une unité timer planifie l’activation d’une unité de service. L’unité de service définit comment la tâche s’exécute : utilisateur, environnement, répertoire de travail, timeouts, contrôles de ressources et ce qui compte comme succès/échec. Cette séparation est la magie.
Le résultat est moins de connaissance tribale. Un fichier d’unité systemd se décrit lui-même d’une manière qu’un extrait de shell aléatoire dans une crontab ne fait pas.
Différences opérationnelles courantes qui comptent
- Observabilité : les timers écrivent par défaut dans journald ; cron n’écrit souvent nulle part à moins que vous ne redirigiez.
- Exécutions manquées : les timers peuvent rattraper ; cron ne le fait pas sauf si vous ajoutez anacron ou une logique personnalisée.
- Dépendances : les timers peuvent s’exécuter après les montages/le réseau ; cron ne peut qu’espérer.
- Chevauchements : systemd peut contrôler la concurrence ; cron déclenchera une ruée à moins que vous n’implémentiez un verrou.
- Sémantique des échecs : systemd enregistre les codes de sortie, le comportement de redémarrage et les limites de débit ; cron se contente souvent d’hausser les épaules.
Une chose encore : les timers ne sont pas « plus fiables » par magie. Ils sont plus fiables parce qu’ils rendent la tâche en exécution explicite, avec des comportements explicites. La fiabilité consiste surtout à rendre l’implicite douloureusement explicite.
Tâches pratiques (commandes, sorties, décisions)
Voici les commandes que j’exécute réellement lors de la migration ou du débogage des tâches planifiées. Chacune indique ce que vous regardez et la décision qu’elle entraîne.
Tâche 1 : Identifier les implémentations de cron et les jobs système pilotés par cron
cr0x@server:~$ dpkg -l | egrep 'cron|anacron|systemd'
ii cron 3.0pl1-... amd64 process scheduling daemon
ii systemd 257-... amd64 system and service manager
Ce que cela signifie : Vous avez le cron classique installé. Si vous voyez aussi des paquets anacron, vous avez peut-être déjà un comportement partiel de « rattrapage » pour certaines tâches quotidiennes/hebdomadaires.
Décision : N’uninstallez pas cron tout de suite. D’abord inventorieez les jobs et comprenez ce qui en dépend.
Tâche 2 : Inventaire des entrées cron système (celles que tout le monde oublie)
cr0x@server:~$ ls -la /etc/cron.d /etc/cron.daily /etc/cron.hourly /etc/cron.weekly /etc/cron.monthly
/etc/cron.d:
total 20
-rw-r--r-- 1 root root 201 Jan 10 10:12 sysstat
-rw-r--r-- 1 root root 349 Jan 10 10:12 logrotate
...
/etc/cron.daily:
total 16
-rwxr-xr-x 1 root root 539 Jan 10 10:12 apt-compat
-rwxr-xr-x 1 root root 377 Jan 10 10:12 man-db
Ce que cela signifie : Les paquets Debian déposent souvent des jobs ici. Certains ont déjà des équivalents systemd sur les versions récentes ; d’autres s’appuient encore sur cron.
Décision : Pour les jobs fournis par le système, préférez les choix de distribution sauf si vous avez de bonnes raisons. Migrer des scripts fournis par un vendor rapporte rarement.
Tâche 3 : Inventaire des crontabs utilisateurs (là où vivent les trucs bizarres)
cr0x@server:~$ sudo ls -1 /var/spool/cron/crontabs
postgres
www-data
backup
Ce que cela signifie : Ce sont des crontabs par utilisateur. Elles contiennent souvent des tâches critiques pour le business que personne n’a documentées.
Décision : Traitez chaque crontab comme du code de production. Exportez et révisez ligne par ligne avant de changer quoi que ce soit.
Tâche 4 : Dumper une crontab spécifique et chercher les pièges d’environnement
cr0x@server:~$ sudo crontab -u backup -l
MAILTO=ops-alerts
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
15 2 * * * /opt/jobs/backup-nightly.sh
Ce que cela signifie : L’environnement de cron est explicite ici (bien), mais MAILTO suppose que la livraison d’emails fonctionne (souvent faux sur des flottes modernes).
Décision : Lors de la migration, intégrez l’environnement dans l’unité systemd et remplacez le « mail comme surveillance » par une alerte explicite.
Tâche 5 : Vérifier l’inventaire des timers (ce que systemd planifie déjà)
cr0x@server:~$ systemctl list-timers --all
NEXT LEFT LAST PASSED UNIT ACTIVATES
Mon 2025-12-29 02:15:00 UTC 3h 12min left Sun 2025-12-28 02:15:02 UTC 21h ago backup-nightly.timer backup-nightly.service
Mon 2025-12-29 00:00:00 UTC 1h - - - logrotate.timer logrotate.service
Ce que cela signifie : Les timers vous montrent NEXT/LAST. Cela répond immédiatement à la question « est-ce que ça a tourné ? » sans greper un fichier.
Décision : Si une tâche compte, elle doit être visible ici (ou dans un orchestrateur), pas cachée dans une crontab personnelle.
Tâche 6 : Inspecter l’horaire d’un timer et s’il rattrape
cr0x@server:~$ systemctl cat backup-nightly.timer
# /etc/systemd/system/backup-nightly.timer
[Unit]
Description=Nightly backup
[Timer]
OnCalendar=*-*-* 02:15:00
Persistent=true
RandomizedDelaySec=10m
[Install]
WantedBy=timers.target
Ce que cela signifie : Persistent=true indique que systemd l’exécutera dès que possible après un boot si l’horaire a été manqué. RandomizedDelaySec répartit la charge.
Décision : Pour les sauvegardes, rotation de logs et tâches périodiques de nettoyage, Persistent=true est généralement correct. Pour « réveiller quelqu’un à 02:15 précisément », ce n’est pas adapté.
Tâche 7 : Inspecter l’unité de service associée (c’est là que vit la fiabilité)
cr0x@server:~$ systemctl cat backup-nightly.service
# /etc/systemd/system/backup-nightly.service
[Unit]
Description=Nightly backup job
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
User=backup
Group=backup
WorkingDirectory=/opt/jobs
ExecStart=/opt/jobs/backup-nightly.sh
TimeoutStartSec=3h
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
Ce que cela signifie : C’est explicite : identité, répertoire de travail, timeout. Les tâches cron oublient souvent tout cela et comptent sur la chance.
Décision : Si votre script dépend de montages, du réseau ou de répertoires spécifiques, déclarez-le ici plutôt que de l’encoder dans une logique de script fragile.
Tâche 8 : Valider le parsing de l’horaire (attraper l’énergie « 31 février » avant la prod)
cr0x@server:~$ systemd-analyze calendar "*-*-* 02:15:00"
Original form: *-*-* 02:15:00
Normalized form: *-*-* 02:15:00
Next elapse: Mon 2025-12-29 02:15:00 UTC
(in UTC): Mon 2025-12-29 02:15:00 UTC
From now: 3h 12min left
Ce que cela signifie : systemd vous dit ce qu’il pense que vous avez voulu dire, et quand il déclenchera la prochaine fois. C’est votre première ligne de défense contre les malentendus d’horaire.
Décision : Si la forme normalisée diffère de votre attente, arrêtez-vous et corrigez maintenant. Ne faites pas de « wait and see ».
Tâche 9 : Vérifier si un timer a effectivement déclenché et ce qui s’est passé
cr0x@server:~$ journalctl -u backup-nightly.service -n 20 --no-pager
Dec 28 02:15:02 server systemd[1]: Starting backup-nightly.service - Nightly backup job...
Dec 28 02:15:03 server backup-nightly.sh[1142]: snapshot created: tank/backups@2025-12-28
Dec 28 03:01:29 server backup-nightly.sh[1142]: upload complete
Dec 28 03:01:29 server systemd[1]: backup-nightly.service: Deactivated successfully.
Dec 28 03:01:29 server systemd[1]: Finished backup-nightly.service - Nightly backup job.
Ce que cela signifie : Vous obtenez des lignes de début/fin plus la sortie du script, liées à un nom d’unité. C’est bien plus propre que « où est passée cette redirection ? »
Décision : Si la sortie est trop verbeuse, corrigez le logging du script. Ne sacrifiez pas la visibilité en redirigeant tout vers /dev/null.
Tâche 10 : Prouver si la dernière exécution a échoué (et comment)
cr0x@server:~$ systemctl status backup-nightly.service --no-pager
● backup-nightly.service - Nightly backup job
Loaded: loaded (/etc/systemd/system/backup-nightly.service; static)
Active: inactive (dead) since Sun 2025-12-28 03:01:29 UTC; 21h ago
Duration: 46min 26.113s
Process: 1142 ExecStart=/opt/jobs/backup-nightly.sh (code=exited, status=0/SUCCESS)
Ce que cela signifie : Vous avez le code de sortie, la durée d’exécution et la commande exacte invoquée.
Décision : Si le statut n’est pas 0/SUCCESS, ne devinez pas. Extrait le code de sortie et traitez-le : retry, alerter ou échouer rapidement.
Tâche 11 : Détecter les exécutions qui se chevauchent (le tueur silencieux des sauvegardes et ETL)
cr0x@server:~$ systemctl show backup-nightly.service -p ExecMainStartTimestamp -p ExecMainExitTimestamp -p ActiveEnterTimestamp -p ActiveExitTimestamp
ExecMainStartTimestamp=Sun 2025-12-28 02:15:03 UTC
ExecMainExitTimestamp=Sun 2025-12-28 03:01:29 UTC
ActiveEnterTimestamp=Sun 2025-12-28 02:15:02 UTC
ActiveExitTimestamp=Sun 2025-12-28 03:01:29 UTC
Ce que cela signifie : Vous pouvez extraire des timestamps structurés pour voir si la durée du job approche l’intervalle de planification.
Décision : Si le temps d’exécution se rapproche régulièrement de l’intervalle, ajoutez une protection de concurrence et envisagez d’élargir l’horaire.
Tâche 12 : Vérifier les problèmes de synchronisation horaire qui faussent les plannings
cr0x@server:~$ timedatectl
Local time: Mon 2025-12-29 00:03:11 UTC
Universal time: Mon 2025-12-29 00:03:11 UTC
RTC time: Mon 2025-12-29 00:03:11
Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: yes
NTP service: active
RTC in local TZ: no
Ce que cela signifie : Si la synchronisation de l’horloge est mauvaise, cron et timers deviennent chaotiques. « Il a tourné à 2h » cesse d’avoir du sens.
Décision : Corrigez l’heure d’abord. Déboguer la planification sur un hôte avec une horloge dérivante, c’est comme déboguer un stockage sur un serveur avec des câbles SATA desserrés.
Tâche 13 : Vérifier une dépendance de montage (courant pour sauvegardes, rapports, ingest)
cr0x@server:~$ systemctl status mnt-backups.mount --no-pager
● mnt-backups.mount - /mnt/backups
Loaded: loaded (/proc/self/mountinfo; generated)
Active: active (mounted) since Sun 2025-12-28 00:00:41 UTC; 1 day 0h ago
Where: /mnt/backups
What: /dev/mapper/vg0-backups
Ce que cela signifie : Votre mount cible existe et est actif. S’il ne l’est pas, votre job pourrait écrire par erreur sur la racine.
Décision : Ajoutez un ordre explicite de montage avec RequiresMountsFor=/mnt/backups dans l’unité de service.
Tâche 14 : Confirmer l’environnement que voit réellement votre job sous systemd
cr0x@server:~$ systemctl show backup-nightly.service -p Environment -p User -p Group -p WorkingDirectory
Environment=
User=backup
Group=backup
WorkingDirectory=/opt/jobs
Ce que cela signifie : Aucun variable d’environnement implicite n’est définie. Si votre script a besoin de AWS_REGION ou d’un ajustement de PATH, vous devez le déclarer.
Décision : Placez l’environnement requis dans un EnvironmentFile= aux permissions strictes, ou utilisez des chemins complets dans les scripts. Je préfère les chemins explicites pour les utilitaires critiques.
Tâche 15 : Limitation de débit et rafales de démarrage (pourquoi vos retries « n’ont rien fait »)
cr0x@server:~$ systemctl show backup-nightly.service -p StartLimitIntervalUSec -p StartLimitBurst
StartLimitIntervalUSec=10s
StartLimitBurst=5
Ce que cela signifie : systemd arrêtera les tentatives après une rafale d’échecs dans l’intervalle limite. Cela évite que le flapping se transforme en auto-DOS.
Décision : Si vous utilisez des retries, définissez des limites consciemment. Sinon votre plan « restart on failure » peut cesser de fonctionner en silence après cinq échecs rapides.
Comment construire un timer + service qui ne vous fera pas honte
Une bonne migration consiste surtout à décider quelle est votre tâche : identité, dépendances, timeouts, concurrence et sortie. Cron ne vous forçait jamais à décider tout cela. systemd le fera, et c’est l’objet.
Commencez par l’unité de service
Écrivez d’abord le service. Le timer doit être la partie ennuyeuse.
- Utilisez
Type=oneshotpour les scripts qui s’exécutent et se terminent. Ne faites pas semblant que c’est un daemon. - Définissez
WorkingDirectory=si votre script attend des chemins relatifs. Mieux : évitez les chemins relatifs, mais la réalité est sale. - Utilisez des chemins complets dans
ExecStart=et dans les scripts pour les outils critiques. Compter surPATHest la manière d’obtenir « marche en shell, échoue à 2h ». - Définissez des timeouts. Si la tâche peut se bloquer, elle se bloquera. Donnez à systemd la permission de la tuer.
- Déclarez les dépendances comme les montages et le réseau en ligne. Si la tâche a besoin d’un montage, dites-le. Si elle a besoin du DNS, dites-le.
Puis écrivez l’unité timer
Les timers sont compacts, mais ils ont deux champs qui définissent votre posture de fiabilité :
Persistent=true: rattrape les exécutions manquées après une indisponibilité.RandomizedDelaySec=: évite les foules. Idéal pour les flottes, dangereux pour les tâches « à l’heure exacte ».
Exemple concret : migrer une sauvegarde nocturne
Voici une base robuste. Pas « parfaite ». Mais suffisante pour mettre en production sans créer un nouveau hobby d’astreinte.
cr0x@server:~$ sudo tee /etc/systemd/system/backup-nightly.service > /dev/null <<'EOF'
[Unit]
Description=Nightly backup job
Wants=network-online.target
After=network-online.target
RequiresMountsFor=/mnt/backups
[Service]
Type=oneshot
User=backup
Group=backup
WorkingDirectory=/opt/jobs
ExecStart=/opt/jobs/backup-nightly.sh
TimeoutStartSec=3h
KillMode=control-group
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
# Basic hardening without breaking scripts:
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/mnt/backups /opt/jobs /var/log
EOF
cr0x@server:~$ sudo tee /etc/systemd/system/backup-nightly.timer > /dev/null <<'EOF'
[Unit]
Description=Nightly backup timer
[Timer]
OnCalendar=*-*-* 02:15:00
Persistent=true
RandomizedDelaySec=10m
AccuracySec=1m
Unit=backup-nightly.service
[Install]
WantedBy=timers.target
EOF
cr0x@server:~$ sudo systemctl daemon-reload
cr0x@server:~$ sudo systemctl enable --now backup-nightly.timer
Created symlink '/etc/systemd/system/timers.target.wants/backup-nightly.timer' → '/etc/systemd/system/backup-nightly.timer'.
Pourquoi ces choix :
RequiresMountsFor=évite le classique « la sauvegarde a écrit dans /mnt/backups qui n’était pas monté, donc en fait elle a rempli / ».KillMode=control-groups’assure que les processus enfants n’échappent pas si la tâche expire.- L’hardening de base réduit le rayon d’impact. Si votre script a besoin de plus d’accès, autorisez-le explicitement. Laissez le fichier d’unité être le contrat.
AccuracySecempêche systemd d’essayer trop fort de déclencher à une seconde précise (ce dont vous n’avez généralement pas besoin).
Jitter et « pourquoi ça a tourné à 02:23 ? »
Si vous activez RandomizedDelaySec, la tâche s’exécutera dans cette fenêtre. Ce n’est pas un bug. C’est une fonction de sécurité pour la flotte. Si la finance veut le rapport à 02:15:00 pile, ne le randomisez pas. Si la tâche touche un stockage partagé, randomisez et dormez tranquille.
Blague n°2 : RandomizedDelaySec est la version polie de « arrêtez tous de toucher le stockage à minuit ».
Fonctionnalités de fiabilité à utiliser réellement
systemd vous donne beaucoup de boutons. En utiliser trop crée un fichier d’unité que personne ne veut toucher. Utilisez les boutons qui apportent de la fiabilité par ligne de config.
1) Rattraper les exécutions manquées : Persistent=true
Si un serveur est down à 02:15, cron hausse les épaules. Les timers peuvent s’exécuter au boot. Pour les sauvegardes, la rotation des logs, le nettoyage périodique et les tâches « finalement consistantes », les timers persistants sont un gain simple.
Ne l’utilisez pas lorsque votre tâche doit s’exécuter uniquement à un instant précis du temps civil (par ex. actions coordonnées sur un marché). Dans ces cas, traitez l’horaire comme un contrat et gérez explicitement les indisponibilités.
2) Éviter la foule tonitruante : RandomizedDelaySec
Si vous avez plus que quelques hôtes, ne programmez pas tout au sommet de l’heure. Votre DNS, votre stockage objet, votre base de données et votre stockage partagé vont tous le remarquer en même temps.
La randomisation est une assurance pas chère. Vous échangez l’exactitude contre la stabilité système globale.
3) Timeouts : TimeoutStartSec et compagnons
Chaque tâche qui parle au réseau peut se bloquer. Chaque tâche qui touche le stockage peut se bloquer. Si votre script ne peut pas se bloquer, félicitations : vous n’avez jamais vu NFS un mauvais jour.
Choisissez un timeout basé sur votre SLO. Si la tâche prend normalement 10 minutes, un timeout de 3 heures n’est pas « sûr », c’est « vous ne remarquerez pas qu’elle est cassée ». Utilisez des budgets réalistes.
4) Contrôle de concurrence : une exécution à la fois
Par défaut cron « déclenche et oublie », ce qui devient « déclenche deux fois et regrette ». Avec systemd, vous pouvez concevoir l’absence de chevauchement.
Un pattern pratique :
- Faites en sorte que le service refuse de démarrer si une autre instance est active.
- Ou placez un verrou dans le script avec
flock, mais considérez ce verrou comme faisant partie du contrat (et loggez quand vous passez).
systemd ne fournit pas un flag « pas de chevauchement » en une ligne comme les gens l’espèrent, mais il rend les chevauchements visibles et contrôlables : vous pouvez voir les unités actives, définir des timeouts et faire respecter un chemin d’exécution unique.
5) Dépendances : les montages et le réseau ne sont pas « probablement là »
Les pires échecs ne sont pas « la tâche a échoué ». Les pires échecs sont « la tâche a réussi à faire la mauvaise chose ». Si une sauvegarde s’exécute sans le montage de sauvegarde, elle peut remplir la racine et renvoyer 0.
Utilisez RequiresMountsFor= pour tout chemin qui doit être monté. Utilisez After=network-online.target uniquement si vous avez vraiment besoin du réseau ; sinon vous ralentissez le boot et compliquez l’ordonnancement.
6) Environnement : rendez-le explicite ou éliminez-le
L’environnement de cron est notoirement minimal. Celui de systemd l’est aussi, mais différemment. Compter sur l’initialisation du shell interactif est la façon d’obtenir « ça marche quand je le lance ». Cette phrase devrait déclencher une petite alarme interne.
Si vous avez besoin de secrets ou de configuration, utilisez un EnvironmentFile= lisible uniquement par l’utilisateur du service, ou chargez-les depuis un fichier root-owned avec des permissions strictes. Ne mettez pas de secrets dans des fichiers d’unité.
7) Contrôle des ressources : empêcher les jobs de manger l’hôte
Ici systemd bat cron de loin. Avec cgroups, vous pouvez plafonner ou prioriser les jobs. Une tâche de compression lourde ne doit pas priver les services de production.
Les contrôles utiles incluent Nice, IOSchedulingClass et (quand vous êtes prêts) les contrôles CPU/IO. Commencez petit : priorité et timeouts couvrent déjà la plupart des incidents de jobs incontrôlés.
8) Logging : arrêtez de jeter la sortie dans le vide
Journald facilite la capture de la sortie de façon cohérente. Gardez stdout/stderr. Faites logger vos scripts de manière structurée si possible (même de simples lignes « clé=valeur » sont précieuses en incident).
Puis surveillez cela. Une tâche planifiée sans surveillance est une surprise planifiée.
Playbook de diagnostic rapide
Ceci est l’ordre dans lequel je vérifie les choses quand une tâche pilotée par timer « ne s’est pas exécutée » ou « a tourné mais rien ne s’est passé ». L’objectif est d’identifier le goulot d’étranglement en minutes, pas dans un fil Slack qui dure jusqu’à midi.
Premier point : systemd a-t-il pensé qu’il a déclenché ?
- Vérifiez
systemctl list-timers --allpour LAST et NEXT. - Vérifiez
systemctl status yourjob.timerpour l’état d’activation. - Décision : si LAST est manquant ou ancien, c’est un problème de planification/activation. Si LAST est récent, c’est l’exécution.
Second point : le service a-t-il démarré et quel code de sortie a-t-il renvoyé ?
- Exécutez
systemctl status yourjob.serviceet capturez le code de sortie. - Décision : un code non nul signifie échec au niveau tâche ; zéro signifie « ça a tourné » et votre problème est probablement « ça a tourné mais n’a rien fait d’utile ».
Troisième point : que disent les logs juste autour de l’exécution ?
- Utilisez
journalctl -u yourjob.serviceavec des filtres temporels ou-n. - Décision : si les logs sont vides, vous pouvez être sur la mauvaise unité, le mauvais timer ou le mauvais hôte. Si les logs montrent un blocage, vérifiez les dépendances ou les timeouts.
Quatrième point : vérifier les prérequis (montages, réseau, DNS, identifiants)
- Vérifiez les montages avec
systemctl status mnt-*.mountoufindmnt. - Vérifiez la synchronisation horaire avec
timedatectl. - Décision : si les prérequis ne sont pas stables, corrigez-les ; ne les masquer pas par des retries.
Cinquième point : rechercher chevauchement et backpressure
- Vérifiez la durée de la tâche et la fréquence. Si la tâche prend plus que son intervalle, vous aurez des chevauchements ou un backlog perpétuel.
- Décision : si chevauchement existe, appliquez une politique à exécution unique et ajustez l’horaire ou optimisez en toute sécurité.
Erreurs courantes : symptômes → cause racine → correction
Cette section existe parce que ces erreurs reviennent encore et encore, surtout lors des migrations cron→timer. Si vous voyez le symptôme, ne débattez pas. Allez à la cause racine.
1) Symptom : « Le timer est activé, mais la tâche ne tourne jamais »
Cause racine : L’unité timer est activée mais l’horaire est malformé, ou elle n’est pas installée correctement dans timers.target.
Correction : Validez l’horaire et confirmez l’activation.
cr0x@server:~$ systemd-analyze calendar "Mon..Fri *-*-* 02:15:00"
Original form: Mon..Fri *-*-* 02:15:00
Normalized form: Mon..Fri *-*-* 02:15:00
Next elapse: Mon 2025-12-29 02:15:00 UTC
From now: 3h 12min left
cr0x@server:~$ systemctl is-enabled backup-nightly.timer
enabled
2) Symptom : « Ça marche manuellement, mais échoue depuis le timer »
Cause racine : Différences d’environnement : PATH manquant, répertoire de travail manquant, identifiants manquants, utilisateur différent.
Correction : Faites que l’unité définisse le contrat : User=, WorkingDirectory=, chemins complets, EnvironmentFile=.
3) Symptom : « La tâche a tourné, exit 0, mais n’a rien produit / n’a rien fait »
Cause racine : Le script vérifie un état et sort silencieusement, ou a écrit la sortie ailleurs, ou la tâche a été exécutée sur la mauvaise instance (mauvais chemin de config).
Correction : Ajoutez du logging explicite. Assurez-vous aussi que le fichier d’unité pointe vers le bon script et la bonne config. Vérifiez WorkingDirectory et les chemins absolus.
4) Symptom : « Ça a tourné après un reboot à une heure bizarre »
Cause racine : Persistent=true a provoqué un rattrapage, éventuellement combiné avec RandomizedDelaySec.
Correction : Décidez si le rattrapage est souhaité. Sinon, retirez Persistent=true. Si oui, communiquez ce comportement aux parties prenantes et surveillez-le.
5) Symptom : « Deux instances ont tourné en même temps et ont corrompu l’état »
Cause racine : Le service autorise les démarres concurrents, l’intervalle timer est plus court que le pire runtime, ou une exécution manuelle a chevauché l’exécution planifiée.
Correction : Ajoutez un verrou (niveau script avec flock), augmentez l’intervalle et définissez un timeout réaliste. Pensez aussi à faire passer les opérations manuelles par systemctl start pour qu’elles soient visibles.
6) Symptom : « Ça a échoué une fois et ensuite n’a plus jamais tourné »
Cause racine : La limitation de débit de démarrage a été atteinte à cause d’échecs rapides, ou le timer est correct mais le service échoue immédiatement et est supprimé.
Correction : Inspectez le statut et les logs ; ajustez les limites de démarrage si vous faites des retries volontaires, et corrigez la cause fondamentale.
7) Symptom : « Les sauvegardes ont rempli le système racine »
Cause racine : Le montage de destination de sauvegarde manquait ; le script a écrit dans un répertoire existant sur la racine.
Correction : Utilisez RequiresMountsFor= et écrivez les sauvegardes dans un chemin qui n’existe que lorsqu’il est monté (ou vérifiez l’identité du montage dans le script).
8) Symptom : « Cron m’envoyait des mails en cas d’erreurs ; maintenant on ne voit rien »
Cause racine : Le comportement mail de cron servait d’alerte accidentelle. systemd logge dans le journal, mais personne ne surveille.
Correction : Mettez en place de la surveillance : remontez l’état des unités, alertez sur les échecs, alertez sur les runs manquants, et éventuellement poussez les logs du journal dans votre pipeline de logs.
Trois mini-histoires du monde de l’entreprise
Mini-histoire 1 : L’incident causé par une mauvaise hypothèse
Une équipe a migré une exportation de base de données de cron vers un timer systemd. L’ancienne entrée cron s’exécutait en tant que postgres. Le nouveau service s’exécutait en root parce que « root peut tout faire ». Cette hypothèse a tenu exactement une semaine.
Le script d’export écrivait dans un répertoire appartenant à postgres et utilisait un socket local en s’attendant aux valeurs par défaut de ~postgres. En root, il se connectait parfois — mais créait des fichiers de sortie appartenant à root avec des permissions restrictives. Le job en aval, toujours exécuté en postgres, a commencé à échouer à lire les exports. Le premier signe n’a pas été un échec net ; c’était un rapport obsolète et un cadre qui demandait pourquoi les chiffres n’avaient pas bougé.
L’astreinte a d’abord vérifié la base de données (évidemment). Puis le stockage (aussi évident). Des heures plus tard, quelqu’un a remarqué que le répertoire d’export contenait des fichiers avec des propriétaires mélangés et des horodatages incohérents avec le pipeline de reporting.
La correction n’a pas été héroïque : définir User=postgres, verrouiller WorkingDirectory, et arrêter d’utiliser le comportement implicite du répertoire home. Ils ont aussi ajouté un ReadWritePaths= au niveau unité pour rendre impossible l’écriture ailleurs. Le postmortem a résumé : « Nous avons supposé que root était plus sûr. En fait, il était juste moins visible. »
Mini-histoire 2 : L’optimisation qui a mal tourné
Une entreprise avait une flotte d’hôtes Debian exécutant la compaction des logs et l’envoi toutes les cinq minutes. Quelqu’un a décidé « d’optimiser le temps de boot » en supprimant les dépendances network-online.target des services planifiés. Boot plus rapide, moins de contraintes d’ordonnancement. Sur le papier, cela semblait propre.
En pratique, le timer a déclenché peu après le boot, avant que le DNS ne soit cohérent et avant qu’une interface réseau overlay soit stable. Le job n’échouait pas toujours bruyamment. Parfois il mettait les données en file locale et sortait 0. Parfois il écrivait vers une destination de secours. Parfois il restait bloqué sur une connexion socket jusqu’au timeout par défaut (autrement dit « un moment »).
Après une semaine, les alertes stockage ont commencé : les vagues de redémarrages planifiés corrélaient avec des arriérés de logs, qui corrélaient avec un retard d’ingestion faisant mentir les dashboards. L’« optimisation » n’était pas que le job était cassé, mais que le système se comportait différemment sous perturbation de boot.
La correction ennuyeuse a été de remettre les dépendances — mais seulement les bonnes. Ils ont remplacé le large « attendre le réseau » par une dépendance de montage et une petite vérification préliminaire qui validait la résolution de nom pour l’endpoint spécifique. Ils ont aussi défini un TimeoutStartSec raisonnable et loggé explicitement les échecs. Le boot a été légèrement plus lent. Les opérations se sont beaucoup calmées.
Mini-histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise
Une équipe finance exécutait des rapports de réconciliation mensuels. Tout le monde détestait y toucher car le script était ancien et les règles métier complexes. Ce qu’ils ont bien fait n’était pas brillant : ils ont traité la tâche planifiée comme un service avec un SLO.
Ils ont migré vers un timer systemd et ajouté deux contrôles : (1) alerter si le service échoue, et (2) alerter si le timer n’a pas tourné avec succès dans la fenêtre attendue. Ils ont aussi figé l’environnement via un EnvironmentFile et utilisé des chemins complets pour chaque outil invoqué.
Un mois, une mise à jour de paquet a changé le comportement d’un utilitaire de parsing (même nom, format de sortie par défaut différent). Le script a continué à tourner, mais a produit une sortie incompatible avec la validation en aval. Le job est sorti non-zéro à cause d’un contrôle strict, et l’unité est passée en failed.
Parce que l’équipe avait une « surveillance ennuyeuse », l’échec a été détecté en quelques minutes pendant les heures ouvrables. Ils ont rollbacké le paquet sur cet hôte, ajusté le parsing pour être explicite, et relancé le service manuellement via systemd afin que la nouvelle exécution soit loggée et traçable. Pas de panique nocturne, pas de chiffres bricolés, juste un échec propre et une correction propre.
Listes de contrôle / plan étape par étape
Voici un plan de migration pratique qui minimise les surprises. Utilisez-le que vous migriez une tâche ou cent.
Étape 1 : Inventorier et classer les tâches
- Lister les répertoires cron système et les crontabs utilisateurs.
- Classifier chaque tâche : critique, importante, nice-to-have.
- Décider du comportement souhaité après indisponibilité : rattraper ou sauter ?
Étape 2 : Définir le contrat d’exécution pour chaque tâche
- Quel utilisateur doit l’exécuter ?
- Quels répertoires doivent exister et être inscriptibles ?
- Quels montages doivent être présents ?
- Quelles conditions réseau sont requises ?
- Quel est le pire runtime acceptable ?
- Le chevauchement est-il autorisé ?
Étape 3 : Construire l’unité de service systemd en premier
- Commencez par
Type=oneshot. - Ajoutez
User=,Group=,WorkingDirectory=. - Ajoutez un
TimeoutStartSec=réaliste. - Ajoutez des dépendances de montage et réseau seulement si nécessaire.
- Ajoutez un hardening minimal :
NoNewPrivileges,PrivateTmp, et chemins d’écriture limités.
Étape 4 : Ajouter l’unité timer
- Validez l’horaire avec
systemd-analyze calendar. - Utilisez
Persistent=truelà où les exécutions manquées doivent être rattrapées. - Utilisez
RandomizedDelaySecsur les flottes pour réduire les pics de charge.
Étape 5 : Tester sérieusement
- Démarrez manuellement le service :
systemctl start yourjob.service. - Vérifiez le statut et les logs.
- Simulez des modes d’échec : montage manquant, DNS en panne, problème de permissions.
Étape 6 : Faire fonctionner en parallèle brièvement (avec prudence)
- Si c’est sûr, laissez cron désactivé mais conservé ; ou exécutez la tâche systemd en « dry run » pendant que cron reste primaire.
- Ne lancez jamais deux jobs stateful en parallèle sauf si vous avez un verrou explicite et êtes certain que les effets secondaires sont sûrs.
Étape 7 : Basculer et surveiller
- Désactivez l’entrée cron une fois le timer activé et vérifié.
- Alertez sur les échecs et sur l’absence de runs réussis.
- Revue des logs après les premières exécutions réelles.
Étape 8 : Réduire le risque dans le temps
- Améliorez les scripts pour qu’ils soient idempotents.
- Ajoutez du logging structuré.
- Rendez les dépendances explicites et éliminez les dépendances accidentelles.
FAQ
1) Les timers systemd sont-ils toujours « meilleurs » que cron ?
Non. Ils sont meilleurs quand vous avez besoin de fonctionnalités de fiabilité et d’observabilité. Cron convient pour des tâches simples et peu impactantes. Le gain n’est pas la « modernité », c’est la clarté opérationnelle.
2) Quel est l’équivalent systemd de « exécuter toutes les 5 minutes » ?
Utilisez OnUnitActiveSec=5min (monotone) ou une expression OnCalendar comme *:0/5 selon ce que vous entendez par « toutes les 5 minutes ». Les timers monotones s’exécutent par rapport à la dernière activation ; les timers calendaires s’alignent sur l’horloge murale.
3) Comment empêcher les runs manqués après un reboot ?
Utilisez Persistent=true dans le timer. Assurez-vous ensuite que votre job est sûr à exécuter après une indisponibilité (idempotent ou aware de l’état).
4) Pourquoi mon job s’exécute-t-il avec un PATH différent de mon shell ?
Parce que les tâches planifiées ne doivent pas hériter de votre configuration shell interactive. Corrigez cela en utilisant des chemins complets dans les scripts, ou en définissant explicitement Environment=PATH=... (ou un EnvironmentFile) dans l’unité de service.
5) Dois-je mettre la logique dans le timer ou dans le script ?
Mettez la planification dans le timer et la sémantique d’exécution dans le service. Mettez la logique métier dans le script/app. Ne ré-implémentez pas la planification et les retries en shell si systemd peut le faire avec un comportement plus clair.
6) Qu’est-ce qui remplace « cron m’envoyait la sortie par mail » ?
Deux choses : (1) les logs journald pour stdout/stderr, et (2) la surveillance qui alerte sur les unités en échec et sur les runs manquants. L’email n’est pas de la surveillance ; c’est un piège nostalgique.
7) Puis-je exécuter des timers pour des utilisateurs non-root ?
Oui. Vous pouvez utiliser des timers système exécutant des services avec un User= spécifique, ou des unités systemd au niveau utilisateur (lingering peut être requis). Pour les serveurs, les unités système avec User= explicite sont généralement plus faciles à gérer centralement.
8) Comment gérer proprement les dépendances sur les montages et le réseau ?
Utilisez RequiresMountsFor= pour les chemins de fichiers et After=network-online.target/Wants=network-online.target seulement lorsque la tâche nécessite vraiment un réseau stable. Évitez les dépendances trop larges qui ralentissent le boot et masquent la vraie readiness.
9) Qu’en est-il des tâches qui ne doivent jamais se chevaucher ?
Supposez que le chevauchement arrivera à moins que vous ne l’empêchiez. Utilisez des verrous (souvent flock) et assurez-vous que votre monitoring distingue « sauté à cause d’un verrou » et « exécuté avec succès ». Ajustez aussi intervalles et timeouts pour que le comportement soit prévisible.
10) Est-il sûr de désactiver cron complètement sur Debian 13 ?
Parfois. Mais de nombreux paquets livrent encore des tâches de maintenance basées sur cron. Désinstaller cron globalement peut créer des cassures subtiles. Préférez migrer vos propres jobs d’abord, puis décidez si cron doit rester installé pour la maintenance des paquets.
Conclusion : prochaines étapes rentables
Si vous exploitez des systèmes Debian 13 en production et que vous tenez à ne pas manquer de travaux planifiés, les timers systemd sont le choix par défaut vers lequel tendre. Ils n’éliminent pas les échecs. Ils rendent les échecs lisibles, testables et surveillables.
Étapes pratiques :
- Inventoriez les jobs cron et marquez les critiques.
- Migrez une tâche critique vers un timer + service avec utilisateur explicite, répertoire de travail, timeout et dépendances de montage.
- Activez
Persistent=truelà où le rattrapage est désiré, et ajoutez du jitter là où la charge de flotte compte. - Branchez la surveillance : alertez sur les échecs de service et sur « aucune exécution réussie dans la fenêtre ».
- Ce n’est qu’après cela que vous supprimez les entrées cron. Conservez les anciennes définitions en contrôle de version, pas à la poubelle.
Le meilleur système de planification est celui qui transforme « a-t-il tourné ? » en une question de deux minutes avec une réponse ennuyeuse. systemd vous y aide — si vous traitez les fichiers d’unité comme des contrats opérationnels, pas comme un nouveau lieu pour cacher des scripts shell.