Aller au contenu
Conteneurs & Orchestration medium

Jobs et CronJobs Kubernetes : exécuter des tâches ponctuelles et planifiées

70 min de lecture

logo kubernetes

Un Job exécute une tâche jusqu'à sa complétion : migration de base de données, traitement batch, génération de rapport. Un CronJob crée des objets Job selon un planning ; ce sont ensuite ces Jobs qui exécutent les Pods nécessaires.

Contrairement aux Pods gérés par un Deployment qui tournent en continu, les Jobs s'arrêtent une fois leur travail terminé. Kubernetes garantit que la tâche sera complétée, même en cas d'échec temporaire.

Prérequis : un cluster Kubernetes avec kubectl configuré.

  • Créer un Job pour une tâche ponctuelle
  • Configurer le comportement en cas d'échec (backoffLimit, activeDeadlineSeconds)
  • Paralléliser l'exécution avec completions et parallelism
  • Planifier des tâches récurrentes avec CronJob
  • Maîtriser les paramètres de production (concurrencyPolicy, suspend, timeZone)
  • Appliquer les bonnes pratiques pour le nettoyage automatique

Le choix se joue sur une seule question : la charge a-t-elle une fin ? Un Deployment redémarre indéfiniment ses Pods, c'est exactement ce qu'il faut pour un service web mais c'est catastrophique pour un script de migration, qui serait relancé en boucle après chaque succès. Le tableau ci-dessous vous aide à trancher avant d'écrire le moindre YAML.

RessourceComportementCas d'usage
DeploymentMaintient N Pods en continuApplications web, API, services
JobExécute jusqu'à complétion, puis s'arrêteMigration, backup, traitement batch
CronJobCrée un Job selon un planningTâches planifiées, maintenance

Un Job crée un Pod, exécute la tâche, et se termine une fois le travail fait :

hello-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: hello-job
spec:
template:
spec:
containers:
- name: hello
image: busybox
command: ["echo", "Hello from Kubernetes Job"]
restartPolicy: Never
Fenêtre de terminal
kubectl apply -f hello-job.yaml

Attendez la fin de l'exécution :

Fenêtre de terminal
kubectl wait --for=condition=complete job/hello-job --timeout=60s

Vérifiez le statut :

Fenêtre de terminal
kubectl get jobs
Sortie
NAME STATUS COMPLETIONS DURATION AGE
hello-job Complete 1/1 4s 10s

Un Job terminé ne dit rien de lui-même : c'est le Pod qu'il a créé qui porte la sortie du programme. Trois commandes suffisent pour tout savoir : kubectl logs pour la sortie, kubectl describe pour le compteur de tentatives et les événements, kubectl get pods pour retrouver les Pods même après la fin du Job. Gardez en tête que ces informations disparaissent avec le Job si vous avez activé un nettoyage automatique.

kubectl logs job/<nom> sélectionne automatiquement un Pod du Job, vous n'avez pas besoin de connaître son nom généré.

Fenêtre de terminal
kubectl logs job/hello-job
Sortie
Hello from Kubernetes Job

kubectl describe affiche la configuration effective et surtout la liste des événements, où apparaissent les échecs de planification et les redémarrages.

Fenêtre de terminal
kubectl describe job hello-job

Informations clés :

  • Completions : nombre d'exécutions réussies requises
  • Parallelism : nombre de Pods exécutés en parallèle
  • Active / Succeeded / Failed : état des Pods

Kubernetes appose le label job-name sur chaque Pod créé, ce qui permet de tous les retrouver, y compris ceux qui ont échoué avant une nouvelle tentative.

Fenêtre de terminal
kubectl get pods -l job-name=hello-job

Le manifeste minimal vu plus haut fonctionne, mais il laisse Kubernetes appliquer ses valeurs par défaut : six tentatives, aucune limite de durée, aucun nettoyage. L'exemple ci-dessous montre les quatre champs qui transforment un Job jetable en tâche exploitable en production. Tous se déclarent au niveau du spec du Job, pas dans le template du Pod, une confusion fréquente qui fait rejeter le manifeste par l'API.

job-complet.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: backup-database
spec:
backoffLimit: 3 # Tentatives max en cas d'échec
activeDeadlineSeconds: 300 # Timeout global (5 min)
ttlSecondsAfterFinished: 3600 # Nettoyage auto après 1h
template:
spec:
containers:
- name: backup
image: postgres:16
command: ["pg_dump", "-h", "db-host", "-U", "admin", "mydb"]
restartPolicy: Never
ChampDescriptionValeur par défaut
backoffLimitNombre de tentatives avant abandon6 (sauf Indexed Jobs)
activeDeadlineSecondsDurée max d'exécution du JobIllimitée
ttlSecondsAfterFinishedNettoyage automatique après N secondesNon activé
completionsNombre d'exécutions réussies requises1
parallelismNombre de Pods simultanés1

Un Job qui échoue ne s'arrête pas tout de suite : Kubernetes relance le travail jusqu'à épuisement du quota de tentatives. Trois réglages encadrent ce comportement, et ils répondent à des questions différentes : combien de fois réessayer (backoffLimit), pendant combien de temps au maximum (activeDeadlineSeconds), et comment le Pod lui-même est relancé (restartPolicy). Sans eux, une tâche cassée peut consommer des ressources pendant des heures avant que quelqu'un ne s'en aperçoive.

Si un Pod échoue, Kubernetes le relance avec un délai exponentiel (10s, 20s, 40s...). backoffLimit stoppe les tentatives :

spec:
backoffLimit: 3 # Abandon après 3 échecs

Le Job passe en statut Failed avec la raison BackoffLimitExceeded.

Pour la plupart des Jobs, backoffLimit vaut 6 par défaut. Les Indexed Jobs disposent aussi d'options avancées comme backoffLimitPerIndex.

Empêche un Job de tourner indéfiniment :

spec:
activeDeadlineSeconds: 300 # 5 minutes max

Si le temps est dépassé, tous les Pods sont arrêtés et le Job passe en Failed avec la raison DeadlineExceeded.

Ce champ décide de ce qui est recréé après un échec : un Pod entièrement neuf, ou le même conteneur relancé dans le Pod existant. La différence compte dès que la tâche écrit dans un volume emptyDir, car ce volume est perdu avec le Pod.

ValeurComportement
NeverKubernetes crée un nouveau Pod à chaque tentative
OnFailureLe même Pod redémarre (garde les volumes locaux)

Deux champs pilotent le parallélisme : completions fixe le nombre total d'exécutions réussies attendues, parallelism le nombre de Pods qui tournent en même temps. Kubernetes se charge de maintenir le second jusqu'à atteindre le premier. Attention, les Pods d'un même Job reçoivent tous la même commande : sans mécanisme de partage du travail, ils traiteraient les mêmes données.

parallel-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: parallel-job
spec:
completions: 10 # 10 exécutions au total
parallelism: 3 # 3 Pods en parallèle
template:
spec:
containers:
- name: worker
image: busybox
command: ["sh", "-c", "echo 'Processing...' && sleep 5"]
restartPolicy: Never

Kubernetes lance 3 Pods en parallèle, puis en lance de nouveaux dès qu'un se termine, jusqu'à atteindre 10 complétions.

En mode Indexed, chaque Pod reçoit un index de complétion unique. Kubernetes l'expose via l'annotation batch.kubernetes.io/job-completion-index, et la variable d'environnement JOB_COMPLETION_INDEX est disponible dans le conteneur :

spec:
completions: 5
parallelism: 5
completionMode: Indexed
template:
spec:
containers:
- name: worker
image: busybox
command: ["sh", "-c", "echo Processing chunk $JOB_COMPLETION_INDEX"]

Idéal pour traiter des partitions de données (chunk 0, chunk 1...).

Sans nettoyage, les Jobs terminés s'accumulent. ttlSecondsAfterFinished les supprime automatiquement :

spec:
ttlSecondsAfterFinished: 3600 # Supprimé 1h après complétion

Un CronJob crée automatiquement des Jobs selon un planning. Il ne lance jamais de Pod directement : à chaque échéance, il fabrique un nouvel objet Job, qui crée à son tour les Pods. C'est pour cette raison que le manifeste contient un jobTemplate qui enveloppe le template de Pod, avec deux niveaux de spec imbriqués.

daily-backup.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: daily-backup
spec:
schedule: "0 2 * * *" # Tous les jours à 2h du matin
timeZone: "Europe/Paris" # Fuseau horaire explicite
successfulJobsHistoryLimit: 3 # Garde les 3 derniers Jobs réussis
failedJobsHistoryLimit: 1 # Garde le dernier Job échoué
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: postgres:16
command: ["pg_dump", "-h", "db-host", "-U", "admin", "mydb"]
restartPolicy: Never

Le champ schedule reprend la syntaxe cron classique à cinq champs, séparés par des espaces, de la minute vers le jour de la semaine. Une expression invalide est rejetée à la création de l'objet, mais une expression valide qui ne correspond pas à votre intention passera sans avertissement.

Les cinq champs d'une expression cron, dans l'ordre : minute de 0 à 59, heure de 0 à 23, jour du mois de 1 à 31, mois de 1 à 12, puis jour de la semaine de 0 à 6 où 0 vaut dimanche

ExpressionSignification
0 * * * *Chaque heure à :00
0 2 * * *Tous les jours à 2h
0 0 * * 0Chaque dimanche à minuit
*/15 * * * *Toutes les 15 minutes
0 9-17 * * 1-5Chaque heure de 9h à 17h, lundi à vendredi

La sortie de kubectl get cronjobs condense l'essentiel : le planning appliqué, le fuseau retenu, l'état de suspension, le nombre de Jobs actifs et la date de la dernière exécution déclenchée.

Fenêtre de terminal
kubectl get cronjobs
Sortie
NAME SCHEDULE TIMEZONE SUSPEND ACTIVE LAST SCHEDULE AGE
daily-backup 0 2 * * * Europe/Paris False 0 12h 3d

Un CronJob qui fonctionne en lab peut poser problème en production dès que la tâche dure plus longtemps que l'intervalle prévu, ou que le cluster était indisponible à l'heure dite. Quatre champs répondent à ces situations : concurrencyPolicy pour les chevauchements, startingDeadlineSeconds pour les exécutions manquées, suspend pour la mise en pause, timeZone pour l'heure locale. Les valeurs par défaut privilégient la souplesse plutôt que la sécurité, à vous de les resserrer.

Ce champ répond à une question précise : que faire si l'exécution précédente n'est pas terminée quand la suivante doit démarrer ?

ValeurComportement
AllowAutorise plusieurs Jobs simultanés (par défaut)
ForbidSkip l'exécution si un Job est en cours
ReplaceArrête l'ancien Job et en démarre un nouveau
spec:
concurrencyPolicy: Forbid # Recommandé pour les backups

Si le cluster était indisponible à l'heure prévue, combien de temps après peut-on encore démarrer ?

spec:
startingDeadlineSeconds: 3600 # 1h de délai max

Si startingDeadlineSeconds est défini, Kubernetes saute une exécution qui a dépassé ce délai. Si ce champ n'est pas défini, les occurrences n'ont pas de date limite de démarrage.

Arrêter temporairement les exécutions sans supprimer le CronJob :

Fenêtre de terminal
kubectl patch cronjob daily-backup -p '{"spec":{"suspend":true}}'

Réactiver :

Fenêtre de terminal
kubectl patch cronjob daily-backup -p '{"spec":{"suspend":false}}'

Depuis Kubernetes 1.27, vous pouvez spécifier le fuseau horaire :

spec:
timeZone: "Europe/Paris"

Sans timeZone, le planning utilise le fuseau horaire du kube-controller-manager, souvent UTC dans de nombreux clusters.

Attendre 2 h du matin pour vérifier qu'un backup fonctionne n'a aucun sens. L'option --from crée un Job indépendant à partir du jobTemplate du CronJob : le planning n'est pas modifié, et le Job manuel porte le nom que vous lui donnez, ce qui évite de le confondre avec les exécutions automatiques.

Fenêtre de terminal
kubectl create job --from=cronjob/daily-backup manual-backup

Suivez l'exécution :

Fenêtre de terminal
kubectl wait --for=condition=complete job/manual-backup --timeout=300s
kubectl logs job/manual-backup

Les trois manifestes qui suivent reprennent les champs déjà vus, mais avec des valeurs choisies pour l'usage visé. Comparez-les : une migration se veut non rejouable et rapide, un backup se veut fiable et historisé, une purge se veut discrète. Le même objet Kubernetes se configure très différemment selon la conséquence d'une exécution en double.

Une migration rejouée peut corrompre les données : le backoffLimit est ramené au strict minimum, et l'activeDeadlineSeconds évite qu'un verrou de base laisse le Job en cours indéfiniment. Les identifiants arrivent par envFrom depuis un Secret, jamais en clair dans le manifeste.

apiVersion: batch/v1
kind: Job
metadata:
name: db-migration
spec:
backoffLimit: 1 # Pas de retry sur migration
activeDeadlineSeconds: 600 # 10 min max
ttlSecondsAfterFinished: 86400
template:
spec:
containers:
- name: migrate
image: myapp:latest
command: ["./manage.py", "migrate", "--no-input"]
envFrom:
- secretRef:
name: db-credentials
restartPolicy: Never

Deux backups qui écrivent en même temps dans le même volume produisent une archive inutilisable : concurrencyPolicy: Forbid interdit ce cas. L'historique conservé (sept Jobs réussis) permet de retrouver la trace d'un échec passé, et le dump est écrit dans un PersistentVolumeClaim, pas dans le conteneur, qui disparaît avec le Pod.

apiVersion: batch/v1
kind: CronJob
metadata:
name: postgres-backup
spec:
schedule: "0 3 * * *"
timeZone: "Europe/Paris"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 7 # 1 semaine d'historique
failedJobsHistoryLimit: 3
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: postgres:16@sha256:33f923b05f64ca54ac4401c01126a6b92afe839a0aa0a52bc5aeb5cc958e5f20
command:
- /bin/sh
- -c
- pg_dump -h $DB_HOST -U $DB_USER $DB_NAME | gzip > /backup/$(date +%Y%m%d).sql.gz
envFrom:
- secretRef:
name: db-credentials
volumeMounts:
- name: backup-volume
mountPath: /backup
volumes:
- name: backup-volume
persistentVolumeClaim:
claimName: backup-pvc
restartPolicy: Never

Une purge hebdomadaire tolère un décalage, elle est donc programmée le dimanche à 4 h, hors des créneaux de charge. Notez la position du ttlSecondsAfterFinished : il est placé dans le jobTemplate, car c'est bien le Job créé qui doit être nettoyé, pas le CronJob lui-même.

apiVersion: batch/v1
kind: CronJob
metadata:
name: data-cleanup
spec:
schedule: "0 4 * * 0" # Chaque dimanche à 4h
timeZone: "Europe/Paris"
jobTemplate:
spec:
ttlSecondsAfterFinished: 86400
template:
spec:
containers:
- name: cleanup
image: myapp:latest
command: ["./cleanup.sh", "--older-than", "90d"]
restartPolicy: Never

Quatre situations reviennent en permanence avec les Jobs, et elles se diagnostiquent toutes de la même manière : d'abord les logs du Pod, ensuite les événements du Job ou du CronJob. Le réflexe qui fait gagner du temps est de vérifier que le Pod existe encore : si un TTL l'a supprimé, ses logs sont définitivement perdus et il faut relancer la tâche pour observer.

  1. Le Job reste en "Running" indéfiniment

    Vérifiez les logs du Pod :

    Fenêtre de terminal
    kubectl logs job/my-job

    Solution : ajoutez activeDeadlineSeconds pour forcer un timeout.

  2. Le Job échoue avec BackoffLimitExceeded

    Analysez les événements :

    Fenêtre de terminal
    kubectl describe job my-job

    Vérifiez les logs des Pods échoués :

    Fenêtre de terminal
    kubectl logs -l job-name=my-job --all-containers
  3. Le CronJob ne s'exécute pas

    Vérifiez l'expression cron et le fuseau horaire :

    Fenêtre de terminal
    kubectl get cronjob my-cronjob
    kubectl describe cronjob my-cronjob | grep "Last Schedule"
  4. CronJob suspendu accidentellement

    Fenêtre de terminal
    kubectl get cronjob my-cronjob -o jsonpath='{.spec.suspend}'

    Si true, réactivez avec kubectl patch.

Les deux premières lignes du tableau correspondent aux raisons d'échec que Kubernetes inscrit lui-même dans le statut du Job, vous les retrouverez telles quelles dans kubectl describe.

ErreurCauseSolution
BackoffLimitExceededTrop d'échecsAnalyser les logs, corriger le code
DeadlineExceededJob trop longAugmenter activeDeadlineSeconds ou optimiser
Job ne démarre pasExpression cron invalideVérifier la syntaxe et le timeZone
Jobs qui s'accumulentPas de TTLAjouter ttlSecondsAfterFinished

Ce quiz porte sur les points qui coûtent le plus cher quand ils sont mal compris : la restartPolicy autorisée, le rôle du backoffLimit et l'effet de concurrencyPolicy sur les CronJobs.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

7 questions
5 min.
80% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

  1. Un Job exécute une tâche jusqu'à complétion, un CronJob la planifie
  2. backoffLimit limite les tentatives, activeDeadlineSeconds impose un timeout
  3. ttlSecondsAfterFinished nettoie automatiquement les Jobs terminés
  4. parallelism et completions permettent le traitement parallèle
  5. concurrencyPolicy: Forbid évite les exécutions simultanées de CronJobs
  6. timeZone garantit des exécutions à l'heure locale
  7. suspend permet de mettre un CronJob en pause sans le supprimer
  8. Testez avec kubectl create job --from=cronjob/... avant la mise en prod
  • Débugger une application : Les techniques pour inspecter un Job qui échoue avant d'avoir produit le moindre log.
  • Requests et Limits : Le dimensionnement des Jobs, souvent oubliés quand un quota de namespace est atteint.
  • RBAC Kubernetes : Les permissions du ServiceAccount qu'utilise un CronJob pour agir sur le cluster.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn