Vous avez écrit le script. Il fonctionne sur votre portable. Vous le planifiez. Tout le monde se détend. Puis le rapport du lundi est vide, la sauvegarde n’a jamais eu lieu, et le « nettoyage quotidien » a discrètement supprimé le mauvais répertoire. Les tâches planifiées n’échouent pas bruyamment par défaut ; elles échouent poliment, à 03:07, puis retournent se coucher.
Si vous voulez du travail planifié en qui vous pouvez avoir confiance, traitez la « planification de tâches » comme de l’ingénierie de production : contrats, logs, verrous, temps, environnement, permissions et observabilité. C’est la version adulte du « ajoute juste cron ».
Ce que signifie réellement une planification fiable
La planification n’est pas « exécutez cette commande à 02:00. » La planification est un contrat : exécuter à peu près ce rythme, sous une identité connue, avec un environnement connu, produisant des artefacts connus, et émettant assez de preuves pour prouver ce qui s’est passé. Ou ce qui ne s’est pas passé.
Les quatre promesses que votre tâche planifiée doit tenir
- Elle s’exécute : L’ordonnanceur la déclenche, même après un redémarrage, même après des changements d’heure, même quand la machine est chargée.
- Elle s’exécute une seule fois : Pas de chevauchements, pas de doublons, pas de « deux minuits » parce que l’heure d’été est capricieuse.
- Elle s’exécute de la même manière : Même PATH, même répertoire de travail, même configuration, mêmes permissions, même locale, même umask.
- Elle laisse des preuves : Logs, codes de sortie, horodatages, métriques, et alertes quand elle manque.
Tout le reste est secondaire. Oui, même « s’exécute rapidement. » Rapide c’est bien. Correct, c’est le loyer.
Une opinion qui vous évitera des douleurs : traitez les scripts planifiés comme des services. Pas « une ligne bash ». Les services ont des unités, des limites de ressources, des logs, des retries et des responsables. Le job peut être un script, mais l’exploitation doit ressembler à celle d’un service.
Idée paraphrasée (si vous avez fait de l’ops, vous avez entendu le sentiment) : Les défaillances arrivent ; la fiabilité vient de la conception de systèmes qui les détectent et s’en remettent.
— idée paraphrasée attribuée à l’école SRE popularisée par Google.
Bref historique et faits (parce que le contexte évite les mauvaises décisions)
- cron est assez ancien pour avoir des opinions. Il date des premiers UNIX (fin des années 1970). C’est pour cela qu’il est simple, stable, et aussi qu’il suppose un monde sans prolifération de conteneurs.
- Vixie Cron est devenu le « cron par défaut » sur de nombreuses distributions Linux pendant des années, façonnant des comportements comme la gestion de l’environnement et l’attente d’un mail en cas de sortie.
- Windows Task Scheduler existe depuis les jours de Windows 9x/NT et a évolué en un moteur assez capable avec déclencheurs, conditions et « exécuter que l’utilisateur soit connecté ou non ». Ce n’est plus qu’une interface graphique.
- DST crée des heures « manquantes » et « dupliquées ». Dans de nombreux fuseaux, 02:30 peut ne jamais exister un jour par an, et exister deux fois un autre jour. Ce n’est pas un problème théorique ; c’est un générateur d’incidents.
- les timers systemd existent en partie parce que cron ne peut pas exprimer les contraintes modernes. (dépendances, sandboxing, journalisation par unité, et « exécuter les jobs manqués après un redémarrage »).
- anacron a été inventé pour les laptops et autres machines qui ne sont pas toujours allumées au moment planifié, car cron ne déclenche que lorsque la machine tourne.
- le « thundering herd » est un artefact d’ordonnancement : des flottes configurées pour « exécuter à minuit » peuvent piétiner des services partagés. La randomisation/jitter est une fonctionnalité de fiabilité.
- envoyer la sortie par mail était normal. Beaucoup de configurations cron dépendaient du mail local. Les systèmes modernes n’ont souvent pas de MTA installé, donc la sortie disparaît dans le néant.
Choisissez votre ordonnanceur comme vous choisissez un système de fichiers
Linux/Unix : cron vs systemd timers vs « autre chose »
Utilisez cron quand : vous avez besoin d’une compatibilité universelle, de déclencheurs périodiques très simples, et que vous pouvez fournir vous‑même les fonctionnalités de production manquantes (verrouillage, journalisation, contrôle d’environnement, monitoring).
Utilisez systemd timers quand : vous êtes sur une distro basée sur systemd et que vous voulez des primitives de fiabilité intégrées : journald, dépendances, contrôle des ressources, sandboxing et Persistent=true pour rattraper les jobs manqués après une indisponibilité.
Utilisez Kubernetes CronJobs quand : le job appartient au cluster et nécessite l’isolation des conteneurs, des requests/limits de ressources et des politiques de retry natives du cluster. Mais souvenez-vous : vous déplacez juste les modes de défaillance, vous ne les supprimez pas.
Utilisez un moteur de workflow quand : vous avez besoin de DAG, de retries par étape, de backfills et de pistes d’audit. Si votre « script » est devenu une petite entreprise, arrêtez de prétendre que c’est un passe-temps.
Windows : Task Scheduler est correct, si vous le traitez comme de la prod
Windows Task Scheduler peut être très fiable, mais il vous laissera absolument vous tirer une balle dans le pied avec les identifiants, les répertoires « start in » et les hypothèses de session utilisateur. Si vous planifiez PowerShell, faites-le sérieusement : chemins explicites, décisions explicites sur la execution policy, consignez tout et ne comptez pas sur des lecteurs mappés.
L’ordonnanceur n’est pas votre couche de fiabilité
Même le meilleur ordonnanceur ne peut pas réparer :
- un script qui n’est pas idempotent,
- un job qui peut se chevaucher,
- un job qui dépend de « quel PATH est aujourd’hui »,
- un job qui « réussit » tout en produisant silencieusement de la garbage.
Choisissez un ordonnanceur pour les déclencheurs et l’orchestration. Intégrez la fiabilité dans le job.
Blague 1/2 : Un job cron sans logs, c’est comme un sous‑marine sans sonar : c’est silencieux jusqu’au moment où vous cognez sur quelque chose de cher.
Patrons de conception pour des scripts qui survivent en production
1) Rendez l’environnement ennuyeux volontairement
Cron s’exécute avec un environnement minimal. Les unités systemd peuvent s’exécuter avec un environnement différent de votre shell interactif. Les tâches Windows s’exécutent avec un profil différent de votre session RDP. La solution est identique partout : déclarez ce dont vous avez besoin.
- Utilisez des chemins absolus pour les exécutables et les fichiers.
- Définissez PATH explicitement (et de façon minimale).
- Définissez la locale (LC_ALL=C) si le parsing de la sortie en dépend.
- Définissez umask explicitement si vous créez des fichiers que d’autres doivent lire.
- Définissez explicitement un répertoire de travail (ou ne comptez pas dessus).
2) Traitez le temps comme hostile
Fuseaux horaires, heure d’été, secondes intercalaires, dérive d’horloge et sauts NTP : le temps vous trahira. Si votre job dépend d’une date, décidez si « date » signifie UTC ou heure locale, et soyez explicite.
- Privilégiez l’UTC pour les horodatages et les noms de partition.
- Si un processus métier nécessite l’heure locale (« envoyer à 8h locale »), utilisez l’heure locale mais protégez‑vous les jours DST.
- Enregistrez à la fois « heure planifiée » et « heure de démarrage réelle » dans les logs.
3) Utilisez le verrouillage pour éviter les chevauchements
Les chevauchements sont la défaillance lente classique : tout va bien jusqu’à ce qu’une exécution prenne plus longtemps que d’habitude, la suivante démarre, et vous vous retrouvez avec deux processus qui se disputent les mêmes fichiers, lignes de base de données ou bande passante de stockage.
Sur Linux, utilisez flock. Sur Windows, utilisez un fichier de verrou avec une ouverture exclusive, ou des primitives OS (mutex) si vous êtes dans un vrai langage. Ne bricolez pas un verrou avec « vérifier puis créer » ; c’est du cosplay de conditions de concurrence.
4) Rendre les scripts idempotents, ou au moins répétables sans danger
Les ordonnanceurs réessayent. Les humains relancent. Les machines rebootent au milieu d’un job. Si relancer corrompt, vous n’avez pas d’automatisation — vous avez une machine à sous.
- Écrivez les sorties dans un chemin temporaire, puis renommez atomiquement en place.
- Utilisez des « fichiers marqueurs » avec précaution ; incluez version et timestamp.
- Lors d’interactions avec des bases, utilisez des transactions et des clés uniques pour la dé‑duplication.
- Si vous supprimez des données, mettez‑les en quarantaine avant suppression définitive.
5) Rendre le « succès » mesurable
Le code de sortie 0 est nécessaire mais insuffisant. Un job qui génère un fichier de sauvegarde vide peut toujours retourner 0. Définissez ce que succès signifie :
- La sortie attendue existe et n’est pas vide.
- Le checksum correspond (si applicable).
- Les comptes de lignes sont dans les bornes attendues.
- La sauvegarde est restaurable (tests de restauration périodiques).
6) Journalisez comme si vous étiez d’astreinte (parce que c’est le cas)
Chaque job planifié doit logger : heure de début, heure de fin, durée d’exécution, paramètres clés et un statut final clair. S’il touche au stockage, logguez les octets écrits/lus et les erreurs de couche stockage.
Renvoyez les logs quelque part où on peut les rechercher. Si vous dépendez de fichiers locaux, faites‑les pivoter. Si vous dépendez de journald, assurez‑vous que la rétention est sensée.
7) Définissez des timeouts et limites de ressources
Un job bloqué est pire qu’un job échoué car il bloque la prochaine exécution et affame la machine. Utilisez des timeouts pour les appels réseau et des plafonds de durée totale.
systemd est excellent ici : TimeoutStartSec=, limites CPU et mémoire, ordonnancement IO et sandboxing. Cron peut le faire aussi, mais vous écrirez plus de wrappers.
8) Ajoutez du jitter pour éviter les bousculades en flotte
Si mille serveurs exécutent « nettoyage quotidien » à minuit, félicitations : vous avez inventé un déni de service distribué contre votre propre stockage.
Ajoutez un délai aléatoire (borné), ou planifiez sur une fenêtre. Le jitter coûte peu et apporte de la fiabilité.
9) Surveillez l’absence, pas seulement la présence
Alerter sur « job échoué » c’est bien. Alerter sur « job n’a pas tourné » c’est mieux. Les exécutions manquées arrivent : timers désactivés, crontabs cassés, VM en pause, identifiants expirés.
Exposer un métrique heartbeat ou écrire un fichier timestamp que la supervision vérifie. Le silence n’est pas un succès.
Tâches pratiques (commandes, sorties, décisions)
Voici des vérifications opérationnelles réelles. Exécutez‑les lorsque vous construisez une planification, et réexécutez‑les quand ça casse à 02:13. Chaque item inclut : commande, ce que la sortie signifie, et la décision suivante.
Task 1: Confirmer que le démon cron tourne réellement
cr0x@server:~$ systemctl status cron
● cron.service - Regular background program processing daemon
Loaded: loaded (/usr/lib/systemd/system/cron.service; enabled; preset: enabled)
Active: active (running) since Tue 2026-02-03 00:12:10 UTC; 2 days ago
Docs: man:cron(8)
Main PID: 812 (cron)
Tasks: 1 (limit: 18945)
Memory: 1.4M
CPU: 2.912s
Signification : Si ce n’est pas active (running), votre job n’a jamais eu de chance.
Décision : Si inactive/failed, réparez d’abord le service (enable/start). Ne touchez pas encore au script.
Task 2: Vérifier que l’entrée crontab est installée pour le bon utilisateur
cr0x@server:~$ crontab -l
MAILTO=""
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
15 2 * * * /opt/jobs/nightly-report.sh
Signification : Vous voyez l’horaire, le shell, le PATH, et si la sortie sera envoyée par mail.
Décision : Si c’est le mauvais utilisateur (fréquent), installez‑la avec le compte correct ou utilisez le cron système avec un utilisateur explicite.
Task 3: Vérifier les logs de cron pour le déclenchement de votre job
cr0x@server:~$ grep -E "CRON|nightly-report" /var/log/syslog | tail -n 5
Feb 5 02:15:01 server CRON[24719]: (cr0x) CMD (/opt/jobs/nightly-report.sh)
Feb 5 02:15:01 server CRON[24718]: (CRON) info (No MTA installed, discarding output)
Signification : Cron a déclenché. Aussi, la sortie est supprimée parce qu’il n’y a pas de système de mail et vous n’avez pas redirigé la sortie.
Décision : Corrigez la journalisation immédiatement : redirigez stdout/stderr vers un fichier ou vers syslog/journald.
Task 4: Prouver que le script s’exécute non‑interactivement avec l’environnement de l’ordonnanceur
cr0x@server:~$ env -i HOME=/home/cr0x USER=cr0x SHELL=/bin/bash PATH=/usr/bin:/bin /bin/bash -lc '/opt/jobs/nightly-report.sh'
/opt/jobs/nightly-report.sh: line 12: psql: command not found
Signification : Votre PATH interactif contenait psql ; le PATH du job ne le contient pas.
Décision : Utilisez le chemin absolu vers psql ou définissez PATH dans le script/unit/crontab. Ne « sourcez pas .bashrc » comme pansement.
Task 5: Ajouter du verrouillage pour prévenir les chevauchements (et le tester)
cr0x@server:~$ flock -n /run/lock/nightly-report.lock /opt/jobs/nightly-report.sh; echo "exit=$?"
exit=0
cr0x@server:~$ flock -n /run/lock/nightly-report.lock /opt/jobs/nightly-report.sh; echo "exit=$?"
exit=1
Signification : La première exécution a acquis le verrou. La deuxième a échoué immédiatement (exit 1) parce que le verrou est pris.
Décision : Décidez de la politique : ignorer si verrouillé (souvent correct), ou attendre avec timeout (parfois correct). Documentez‑le.
Task 6: Confirmer que les codes de sortie se propagent et sont visibles
cr0x@server:~$ /opt/jobs/nightly-report.sh; echo "job_exit=$?"
job failed: could not connect to database
job_exit=2
Signification : Le script retourne un code non‑zéro et affiche une erreur claire.
Décision : Si les codes de sortie sont toujours 0, corrigez le script. Les ordonnanceurs ne peuvent réagir qu’à ce que vous leur dites.
Task 7: Construire un timer systemd qui rattrape après une indisponibilité
cr0x@server:~$ systemctl cat nightly-report.timer
# /etc/systemd/system/nightly-report.timer
[Unit]
Description=Run nightly report
[Timer]
OnCalendar=*-*-* 02:15:00
Persistent=true
RandomizedDelaySec=10m
[Install]
WantedBy=timers.target
Signification : Persistent=true exécute le job après un boot s’il a été manqué. RandomizedDelaySec ajoute du jitter.
Décision : Si « doit s’exécuter quotidiennement quoi qu’il arrive », utilisez un timer avec persistance ou un moteur de workflow, pas cron pur.
Task 8: Inspecter l’unité service pour limites de ressources et journalisation
cr0x@server:~$ systemctl cat nightly-report.service
# /etc/systemd/system/nightly-report.service
[Unit]
Description=Nightly report job
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=cr0x
Group=cr0x
WorkingDirectory=/opt/jobs
Environment=PATH=/usr/local/bin:/usr/bin:/bin
ExecStart=/usr/bin/flock -n /run/lock/nightly-report.lock /opt/jobs/nightly-report.sh
TimeoutStartSec=30m
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
StandardOutput=journal
StandardError=journal
Signification : Voilà à quoi ressemble une « planification de production » : dépendances, verrou, timeout et logs dans journald.
Décision : Si votre job entre en compétition avec des charges interactives ou des services lourds en stockage, définissez les priorités IO/CPU ici plutôt que d’espérer.
Task 9: Vérifier quand le timer a été exécuté la dernière fois et s’il est en retard
cr0x@server:~$ systemctl list-timers --all | grep nightly-report
nightly-report.timer loaded active waiting Thu 2026-02-05 02:23:11 UTC 3h ago Thu 2026-02-06 02:15:00 UTC 20h left nightly-report.service
Signification : Vous obtenez les heures de dernière/exécution suivante. Si waiting manque ou si la dernière exécution est ancienne, quelque chose cloche.
Décision : Si en retard, vérifiez que le timer est activé, que l’horloge est saine et que l’unité ne plante pas ou n’est pas bloquée.
Task 10: Lire les logs pour ce job uniquement
cr0x@server:~$ journalctl -u nightly-report.service -n 20 --no-pager
Feb 05 02:15:02 server nightly-report.sh[25101]: start ts=2026-02-05T02:15:02Z
Feb 05 02:15:02 server nightly-report.sh[25101]: connecting db=reporting
Feb 05 02:19:41 server nightly-report.sh[25101]: wrote /var/reports/nightly-2026-02-05.csv bytes=1842201
Feb 05 02:19:41 server nightly-report.sh[25101]: done status=ok runtime_s=279
Signification : Vous pouvez prouver qu’il a tourné, combien de temps ça a pris et ce qu’il a produit.
Décision : Si les logs ne montrent pas un début/fin clair, améliorez d’abord la journalisation avant d’améliorer quoi que ce soit d’autre.
Task 11: Détecter chevauchement ou processus en course
cr0x@server:~$ pgrep -af nightly-report.sh
25101 /bin/bash /opt/jobs/nightly-report.sh
Signification : Vous voyez s’il tourne actuellement, et avec quel PID/commande.
Décision : Si plusieurs instances existent, ajoutez du verrouillage et envisagez un plafond de durée/timeout.
Task 12: Vérifier le détenteur du verrou si vous êtes coincé « verrouillé pour toujours »
cr0x@server:~$ ls -l /run/lock/nightly-report.lock
-rw-r--r-- 1 cr0x cr0x 0 Feb 5 02:15 /run/lock/nightly-report.lock
cr0x@server:~$ lsof /run/lock/nightly-report.lock
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
flock 25100 cr0x 3w REG 253,0 0 987 /run/lock/nightly-report.lock
Signification : L’existence du fichier ne signifie pas qu’il est verrouillé ; lsof montre si un processus le tient.
Décision : Si un processus mort ne tient pas le verrou, votre méthode de verrouillage est mauvaise (n’utilisez pas la logique « le fichier existe »). Utilisez correctement flock.
Task 13: Valider la synchronisation du temps (parce que les plannings supposent que le temps est réel)
cr0x@server:~$ timedatectl
Local time: Thu 2026-02-05 05:24:12 UTC
Universal time: Thu 2026-02-05 05:24:12 UTC
RTC time: Thu 2026-02-05 05:24:11
Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: yes
NTP service: active
RTC in local TZ: no
Signification : Si l’horloge n’est pas synchronisée, « 2:15 » devient une suggestion.
Décision : Réparez la synchronisation du temps avant de chasser des bugs fantômes d’ordonnanceur.
Task 14: Vérifier l’espace disque et la pression d’inodes (le stockage tue les jobs silencieusement)
cr0x@server:~$ df -h /var /tmp
Filesystem Size Used Avail Use% Mounted on
/dev/sda2 100G 94G 1.8G 99% /
tmpfs 16G 128M 16G 1% /tmp
cr0x@server:~$ df -i /var
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/sda2 6553600 6501000 52600 99% /
Signification : Vous pouvez avoir des octets libres mais pas d’inodes, ou l’inverse. L’un ou l’autre peut casser des tâches « écrire la sortie ».
Décision : Si l’utilisation >95%, arrêtez de prétendre que c’est un problème d’ordonnancement. Libérez de l’espace, faites pivoter les logs, corrigez les fichiers temporaires qui s’emballent.
Task 15: Repérer les goulets IO qui allongent les temps d’exécution et provoquent des chevauchements
cr0x@server:~$ iostat -xz 1 3
Linux 6.5.0 (server) 02/05/2026 _x86_64_ (8 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
8.12 0.00 3.44 31.55 0.00 56.89
Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s wrqm/s %wrqm w_await wareq-sz aqu-sz %util
sda 3.10 90.2 0.00 0.0 12.33 29.1 40.0 10240.0 12.0 23.1 210.45 256.0 8.41 98.7
Signification : Un %iowait élevé et un %util proche de 100% indique que le disque est saturé. Un w_await élevé suggère que les écritures sont en file d’attente.
Décision : Si votre job est devenu « lent », vérifiez l’IO. Ensuite envisagez de le programmer hors pic, de le limiter, ou de déplacer les écritures lourdes vers un stockage plus performant.
Task 16: Confirmer que les permissions correspondent à l’identité de l’ordonnanceur
cr0x@server:~$ sudo -u cr0x test -w /var/reports && echo writable || echo not_writable
not_writable
Signification : L’utilisateur qui exécute le job ne peut pas écrire dans le chemin cible.
Décision : Corrigez la propriété du répertoire/ACLs, ou exécutez le job sous le compte de service correct. Ne « lancez simplement en root » que si vous aimez les postmortems.
Task 17: Valider les dépendances DNS/réseau pour les jobs qui appellent des services
cr0x@server:~$ getent hosts db.internal
10.40.12.8 db.internal
cr0x@server:~$ nc -vz db.internal 5432
Connection to db.internal (10.40.12.8) 5432 port [tcp/postgresql] succeeded!
Signification : La résolution de nom fonctionne ; le port est atteignable.
Décision : Si le DNS échoue ou que le port est bloqué, corrigez la politique réseau/santé avant de réécrire le script.
Task 18: Gotcha façon Windows sur Linux : trouver des scripts avec terminaisons CRLF
cr0x@server:~$ file -b /opt/jobs/nightly-report.sh
Bourne-Again shell script, ASCII text executable, with CRLF line terminators
Signification : CRLF peut casser les interprètes de manière subtile, surtout avec les shebangs.
Décision : Convertissez en LF (dos2unix), committez correctement, et arrêtez de copier des scripts par e‑mail.
Trois mini‑histoires d’entreprise (qu’on apprend une fois)
Mini‑histoire 1 : L’incident causé par une mauvaise hypothèse (les fuseaux horaires ne sont pas un détail)
Une entreprise de taille moyenne exécutait un job nocturne de « gel de données » à 00:05 heure locale. Le script taguait les lignes avec freeze_date=$(date +%F), puis les systèmes en aval utilisaient cette date comme clé de partition. Pendant des années, ça fonctionnait, parce que « heure locale » et « jour ouvré » semblaient identiques.
Puis ils se sont étendus à une seconde région. Quelqu’un a fait la chose sensée pour l’infra et a mis les serveurs en UTC. L’ordonnanceur tournait toujours à « 00:05 » parce que c’est ce que disait la crontab. Mais maintenant « 00:05 » était en UTC, pas local. Le gel a tourné des heures plus tôt que prévu pour la région originale, et des heures plus tard pour la nouvelle région.
Le vrai dommage n’était pas l’heure d’exécution — c’était le tampon de date. Certaines lignes qui auraient dû être taguées pour le jour ouvré suivant ont été estampillées de la veille. Des partitions « manquaient » de données, les tableaux de bord ont montré une chute soudaine et l’équipe finance a déclenché une petite panique polie.
La première correction proposée était classique : « Remettez le fuseau horaire. » Ça aurait aidé une région et blessé l’autre. La deuxième correction était meilleure : définir une frontière de journée métier dans le code (TZ explicite pour le calcul de date) et enregistrer les timestamps en UTC. Le job calculait désormais la clé de partition en utilisant un fuseau métier configuré et loggait à la fois la clé et le timestamp UTC.
La leçon durable était simple et agaçante : vous ne pouvez pas externaliser la sémantique à date. Si la sortie d’un job dépend de « quel jour c’est », vous devez définir quelle horloge vous entendez.
Mini‑histoire 2 : L’optimisation qui s’est retournée contre eux (la compression n’est pas gratuite)
Une autre organisation avait un job de sauvegarde qui dumpait une base et la compressait. Les coûts de stockage montaient, donc quelqu’un a « optimisé » en poussant la compression au max et en augmentant le parallélisme. Les sauvegardes sont devenues plus petites. Tout le monde a félicité le graphique.
Deux semaines plus tard, 02:00 est devenu la nouvelle heure de trafic maximal. Le job de sauvegarde a saturé CPU et IO disque, et il l’a fait avec une parfaite régularité. D’autres tâches planifiées — rotation des logs, ETL, même scans de sécurité — ont commencé à se chevaucher et à expirer. L’ordonnanceur ne faillait pas ; il exécutait fidèlement un plan qui ne correspondait plus à la réalité.
Le premier symptôme n’était pas « sauvegarde échouée. » Le premier symptôme était « des services aléatoires lents le matin. » L’astreinte a poursuivi la latence applicative, puis les verrous de BD, puis le réseau. Ce n’est qu’après avoir graphié l’iowait hôte que le schéma est apparu : la sauvegarde « optimisée » transformait l’hôte en brique vibrante et triste.
La correction n’était pas d’abandonner la compression complètement. Il s’agissait de borner le périmètre : réduire le niveau de compression, plafonner l’usage CPU, lui donner une priorité IO, et la déplacer vers une fenêtre horaire différente avec jitter sur la flotte. Ils ont aussi introduit un runtime maximal ; si le job le dépassait, il échouait bruyamment et alertait, au lieu de voler silencieusement toute la nuit.
Une optimisation qui ignore la contention n’est pas une optimisation. C’est déplacer le coût de « espace disque » vers « sommeil de tout le monde ».
Mini‑histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la situation (idempotence + écritures atomiques)
Une équipe exécutait un job planifié qui générait un CSV utilisé pour la facturation client. La version précédente écrivait directement dans /srv/billing/current.csv. Une nuit, la machine a redémarré en plein écriture après une mise à jour kernel. Le fichier existait, le job « a tourné », et les systèmes en aval ont joyeusement consommé un CSV tronqué. De mauvaises factures ont suivi. Pas catastrophique, mais coûteux en temps humain.
L’équipe a changé une chose : le job écrivait désormais dans un fichier temporaire avec un nom unique, validait le compte de lignes et un checksum, puis le renommait atomiquement en place. Ils ont aussi conservé l’ancien fichier valide pendant quelques jours. C’était ennuyeux. C’était correct.
Des mois plus tard, un hic de stockage a causé une brève erreur IO en cours d’exécution. Le job a échoué avant le rename, laissant l’ancien current.csv intact. Les systèmes en aval ont continué d’utiliser la dernière sortie valide pendant que le job alertait l’astreinte qu’il n’avait pas pu produire un nouvel artefact.
Pas de panique. Pas d’appels « qu’est‑ce qui a changé ? ». Pas de récupération judiciaire de données partielles. Juste un échec propre et un contrat de sortie stable. La fiabilité ressemble souvent à refuser d’être trop malin.
Mode opératoire pour un diagnostic rapide
Quand un job planifié ne tourne pas (ou tourne mal), vous avez besoin d’une séquence qui trouve rapidement le goulot. Pas une séance de debug basée sur le feeling.
Premier point : L’ordonnanceur a‑t‑il déclenché quelque chose ?
- cron : vérifiez les entrées syslog pour la ligne CMD ; confirmez que l’entrée crontab existe et que le démon tourne.
- systemd : vérifiez
list-timers, le status de l’unité et l’heure de la dernière exécution ; confirmez que le timer est activé. - Windows : vérifiez Last Run Time / Last Run Result et l’onglet History (si activé).
S’il n’y a aucun enregistrement de déclenchement, arrêtez. Réparez la planification et l’activation. Ne touchez pas au script.
Second point : A‑t‑il démarré puis mouru immédiatement ?
- Lisez les logs : journald ou votre fichier de log redirigé.
- Recherchez « command not found », permission denied, config manquante, mauvais répertoire de travail.
- Relancez sous un environnement minimal pour reproduire.
Si ça meurt immédiatement, c’est presque toujours environnement, identité ou permissions.
Troisième point : A‑t‑il tourné mais pris trop de temps ?
- Vérifiez les chevauchements : PIDs multiples, contention de verrou, messages « déjà en cours ».
- Vérifiez les goulets de ressources : disque plein, épuisement d’inodes, IO wait, CPU steal, timeouts réseau.
- Vérifiez les dépendances aval : verrous DB, limitation d’API, DNS.
Si l’exécution s’est étirée, corrigez la contention et ajoutez timeouts/limites avant d’ajouter des retries.
Quatrième point : A‑t‑il « réussi » mais produit des mauvaises sorties ?
- Validez les artefacts : taille, checksum, compte de lignes, versions de schéma.
- Recherchez des écritures partielles : timestamps de fichier, usage d’un rename atomique.
- Vérifiez les « critères de succès » dans le code : vérifiez‑vous réellement quelque chose ?
Une mauvaise sortie silencieuse est pire qu’un échec. Rendez la publication de mauvaises sorties impossible.
Blague 2/2 : La seule chose plus fiable qu’un job cron est un job cron qui échoue dès que vous arrêtez de le surveiller.
Erreurs courantes : symptômes → cause racine → correction
1) Symptom: « Il tourne manuellement mais pas sur la planification »
Cause racine : différences PATH/environnement ; mauvais répertoire de travail ; le script dépend des fichiers d’initialisation du shell interactif.
Correction : Utilisez des chemins absolus, définissez PATH explicitement, définissez WorkingDirectory dans systemd, évitez de sourcer .bashrc. Reproduisez avec env -i.
2) Symptom : Pas de logs, pas de sortie, pas d’erreurs
Cause racine : sortie supprimée (pas de MTA pour cron), ou logs écrits quelque part inaccessible à l’identité de l’ordonnanceur.
Correction : Redirigez stdout/stderr vers un fichier ou vers journald. Assurez‑vous des permissions du répertoire de logs et de la rotation.
3) Symptom : Le job s’exécute deux fois (ou chevauche) et corrompt les données
Cause racine : pas de verrouillage ; runtime long ; retries sans idempotence ; heure DST dupliquée.
Correction : Ajoutez flock. Ajoutez des limites de runtime. Rendez les sorties atomiques et idempotentes. Envisagez la planification en UTC ou une gestion explicite du TZ.
4) Symptom : « Échecs aléatoires » avec erreurs réseau
Cause racine : flapping DNS, dépendance transitoire qui échoue, pas de retries/backoff, réseau pas prêt au boot.
Correction : Ajoutez des vérifications de dépendance, des retries bornés avec backoff, et en systemd utilisez After=network-online.target plus Wants=network-online.target.
5) Symptom : Il tourne, mais l’aval dit que la sortie est vide/partielle
Cause racine : écritures directes vers le nom final ; le consommateur lit pendant que le producteur écrit ; reboot en cours d’écriture.
Correction : Écrivez en temp + fsync si nécessaire + rename atomique. Conservez la dernière sortie connue bonne.
6) Symptom : Ça marche pendant des semaines, puis ça s’arrête pour toujours
Cause racine : expiration d’identifiants (tokens API, Kerberos, rotation de mot de passe DB) ; verrou bloqué à jamais à cause d’un mauvais implémentation ; disque qui se remplit progressivement.
Correction : Alertez sur « job n’a pas tourné » et sur « sortie manquante ». Utilisez des verrous gérés par l’OS (flock). Surveillez l’utilisation disque et faites la rotation des logs.
7) Symptom : « C’est planifié, mais ça ne rattrape jamais après une indisponibilité »
Cause racine : cron ne refait pas les runs manqués ; timers non persistants ; laptop/VM était éteint.
Correction : Utilisez des timers systemd avec Persistent=true, ou concevez le job pour qu’il s’exécute en se basant sur un checkpoint « dernier succès » plutôt que sur l’horloge murale uniquement.
8) Symptom : Pics CPU à l’heure planifiée sur toute la flotte
Cause racine : horaires identiques provoquant un thundering herd ; pas de jitter.
Correction : Ajoutez un délai aléatoire dans les timers systemd ou un sleep aléatoire borné dans le wrapper ; étalez les horaires.
Listes de contrôle / plan étape par étape
Checklist A : Avant de planifier un script
- Définissez le succès : quel artefact/effet prouve que ça a marché ?
- Définissez la cadence : « quotidien » n’est pas précis. Un rattrapage est‑il nécessaire ?
- Définissez l’identité : quel utilisateur/compte de service l’exécute ? Quelles permissions ?
- Définissez les dépendances : réseau, base, points de montage, secrets.
- Rendez‑le répétable : idempotent ou sûr à relancer.
- Rendez‑le non chevauchant : verrouillage plus runtime maximal.
- Rendez les logs inévitables : stdout/stderr capturés ; statut début/fin.
- Rendez le temps explicite : UTC vs local ; comportement DST documenté.
- Planifiez la supervision : alerte sur échec et sur absence d’exécution.
Checklist B : Un pattern de wrapper « assez bon » (Linux)
Même si vous gardez cron, encapsulez le script. Votre wrapper est l’endroit où vit la discipline de production : environnement, verrouillage, journalisation, timeouts.
cr0x@server:~$ cat /opt/jobs/wrappers/nightly-report-wrapper.sh
#!/bin/bash
set -euo pipefail
export PATH="/usr/local/bin:/usr/bin:/bin"
export LC_ALL="C"
umask 027
log_dir="/var/log/jobs"
mkdir -p "$log_dir"
log_file="$log_dir/nightly-report.log"
exec >> "$log_file" 2>&1
echo "start ts=$(date -u +%FT%TZ) host=$(hostname -f)"
timeout 30m flock -n /run/lock/nightly-report.lock /opt/jobs/nightly-report.sh
echo "done ts=$(date -u +%FT%TZ) status=ok"
Pourquoi ça marche : ça échoue rapidement (set -euo pipefail), capture les logs, impose un timeout et empêche les chevauchements. Ça rend aussi l’environnement prévisible.
Checklist C : Plan de déploiement d’un timer systemd (hôte unique vers flotte)
- Écrire l’unité
.serviceavec User/Group explicites, PATH, WorkingDirectory, timeout et journalisation. - Écrire le
.timeravecOnCalendar,Persistent=trueet du jitter. - Tester :
systemctl start job.servicemanuellement ; vérifier logs et code de sortie. - Activer le timer :
systemctl enable --now job.timer. - Surveiller :
list-timerset journald pendant au moins un cycle complet. - Ajouter la supervision : alerte si dernier succès > fenêtre attendue ; alerte sur code de sortie non‑zéro.
- Puis seulement : déployer sur plus d’hôtes. Ne changez pas la flotte entière à minuit. Vous n’êtes pas un chaos engineer ; vous essayez de dormir.
Checklist D : Règles d’ordonnancement conscientes du stockage
- Ne planifiez pas des jobs lourds en écriture en même temps que sauvegardes, compactations ou réplications d’instantanés.
- Surveillez l’espace libre et l’utilisation d’inodes sur les répertoires de sortie et temporaires.
- Utilisez des écritures atomiques pour les artefacts partagés.
- Throttlez la compression et la concurrence ; mesurez l’iowait.
- Préparez la croissance des logs ; faites la rotation ou expédiez les logs.
FAQ
1) Cron ou systemd timers : lequel choisir sur Linux ?
Si vous avez systemd, préférez les timers pour tout ce qui compte : journalisation intégrée, contrôle des dépendances, Persistent=true et limites de ressources. Cron convient pour des tâches périodiques simples et non critiques, ou quand la portabilité est importante.
2) Comment empêcher les jobs de se chevaucher ?
Utilisez des verrous gérés par l’OS. Sur Linux, encapsulez la commande avec flock. Fixez aussi une durée maximale (timeout) pour que « bloqué » ne devienne pas « verrouillé pour toujours ».
3) Pourquoi mon job tourne bien via SSH mais échoue dans cron ?
Votre shell interactif définit PATH, locales et parfois des identifiants. Cron ne le fait pas. Reproduisez avec env -i, puis faites en sorte que le script déclare ce dont il a besoin (chemins absolus, chemins de config explicites, locale explicite).
4) Dois‑je rediriger la sortie vers un fichier ou vers journald ?
Si vous utilisez déjà systemd, journald est généralement le plus propre : searchable, tagué par unité et gérable centralement. Si vous êtes sur cron, un fichier suffit — faites juste la rotation et assurez‑vous que les permissions permettent l’écriture.
5) Quelle est la bonne façon de gérer les secrets pour les jobs planifiés ?
N’inscrivez pas de secrets en dur dans les scripts ou crontabs. Utilisez un gestionnaire de secrets si vous en avez un, ou au moins des fichiers de config lisibles par root uniquement avec des permissions strictes. Pour systemd, envisagez des fichiers d’environnement avec accès contrôlé. Faites tourner les secrets et alertez sur les échecs d’authent.
6) Comment rendre un job capable de « rattraper » après une indisponibilité ?
cron ne refait pas les runs manqués. Utilisez des timers systemd avec Persistent=true, ou concevez le job pour traiter à partir d’un checkpoint stocké (« dernier timestamp réussi ») plutôt que de supposer qu’il s’exécute toujours.
7) Comment gérer DST pour un job qui doit tourner à une heure métier locale ?
Décidez du comportement pour l’heure « manquante » et l’heure « dupliquée » du DST. Choix courants : exécuter à une heure sûre (par ex. 03:15), ou exécuter en UTC et traduire les sorties. Si l’heure locale est requise, loggez le fuseau, enregistrez les timestamps UTC et protégez‑vous contre les exécutions dupliquées avec verrouillage et idempotence.
8) Mon job est parfois lent. Dois‑je ajouter des retries ?
Pas en premier. La lenteur vient souvent de la contention (saturation IO, verrous DB) ou d’un problème de dépendance. Mesurez où le temps est passé, plafonnez la durée, et empêchez les chevauchements. Les retries sans contrôle peuvent multiplier la charge et aggraver l’incident.
9) Quel est le minimum de supervision à ajouter ?
Deux signaux : (1) timestamp de la dernière exécution réussie, (2) statut de sortie de la dernière exécution. Alertez si le job est en retard/manquant ou s’il sort non‑zéro. Suivez aussi la fraîcheur des artefacts s’il produit un fichier ou un rapport.
10) Quand dois‑je arrêter d’utiliser des « scripts planifiés » et passer à un outil de workflow ?
Quand vous avez des dépendances entre étapes, besoin de backfills, exigence de pistes d’audit détaillées ou logique de retry complexe. Si vous construisez un DAG avec du bash et des mails, la réponse est déjà évidente.
Conclusion : prochaines étapes pratiques
Si vous voulez des scripts planifiés qui s’exécutent réellement, ne commencez pas par débattre cron vs timers. Commencez par rendre votre job survivable : environnement explicite, verrouillage, idempotence, journalisation et supervision de l’absence.
- Choisissez l’identité d’exécution et sécurisez‑la (moindre privilège, permissions prévisibles).
- Ajoutez du verrouillage (
flock) et un runtime maximal (timeouts). - Rendez les sorties atomiques (fichier temporaire + rename) et définissez des vérifications de succès.
- Capturez les logs quelque part de façon consultable, et gérez leur rotation/rétention intentionnellement.
- Alertez sur « échoué » et « n’a pas tourné », pas seulement sur « a imprimé une erreur ».
- Si vous êtes sur Linux avec systemd : migrez les jobs critiques vers des timers avec
Persistent=trueet du jitter. - Effectuez un drill contrôlé d’échec : cassez le DNS ou remplissez un répertoire temporaire dans un environnement de test et confirmez que votre job échoue bruyamment et sûrement.
Planifier c’est facile. Planifier de façon fiable est une discipline. Faites maintenant les parties ennuyeuses pour que votre futur vous puisse dormir à 02:15.