Exécuter des Jobs et des CronJobs Kubernetes

KubernetesBeginner
Pratiquer maintenant

Introduction

Les déploiements maintiennent les services en fonctionnement continu. Les traitements par lots répondent à une autre logique : aller jusqu’au bout, enregistrer le résultat, puis s’arrêter. Kubernetes modélise les tâches ponctuelles avec les Jobs et les exécutions répétées avec les CronJobs.

Vous allez exécuter un Job, examiner son Pod et ses journaux, ajouter un parallélisme contrôlé, planifier un CronJob, puis découvrir comment la suspension et les limites d’historique facilitent l’exploitation des tâches récurrentes.

Considérez cet atelier comme une comparaison des conditions de réussite. Un Deployment réussit en maintenant ses réplicas disponibles. Un Job réussit lorsqu’un nombre requis de Pods se termine avec le code de sortie 0. Un CronJob réussit lorsqu’il crée des Jobs aux moments prévus. Bien distinguer ces trois niveaux rendra les fichiers YAML beaucoup plus faciles à comprendre.

Exécuter un Job ponctuel

Dans cette étape, vous allez créer un Job dont le Pod affiche un rapport, puis se termine avec succès. Créez le manifeste avec un document intégré au shell ; les guillemets autour de EOF empêchent le shell de développer les caractères présents dans le YAML.

Avant de saisir le YAML, observez sa structure : kind: Job sélectionne le contrôleur, spec.template décrit le Pod qu’il créera et ce Pod contient un conteneur à durée de vie courte. Contrairement au modèle d’un Deployment, un Pod géré par un Job doit utiliser restartPolicy: Never ou OnFailure, car une tâche terminée avec succès est censée s’arrêter.

cd /home/labex/project/batch-workloads
cat <<'EOF' > report-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: report-job
  namespace: practice-batch
spec:
  backoffLimit: 2
  template:
    metadata:
      labels:
        app: report-job
    spec:
      restartPolicy: Never
      containers:
        - name: reporter
          image: busybox:1.36
          imagePullPolicy: IfNotPresent
          command: ["sh", "-c", "echo report-complete"]
EOF

Commencez par demander à Kubernetes de valider localement le fichier. --dry-run=client vérifie la structure de l’objet sans rien créer ; c’est une bonne habitude lorsque vous rédigez un nouveau manifeste :

kubectl apply --dry-run=client -f report-job.yaml

Si le résultat est job.batch/report-job created (dry run), le fichier est structurellement valide. Appliquez-le réellement, puis attendez la condition Complete du Job au lieu d’estimer le temps nécessaire :

kubectl apply -f report-job.yaml
kubectl wait --for=condition=Complete job/report-job -n practice-batch --timeout=60s

restartPolicy: Never laisse le contrôleur du Job décider si un autre Pod est nécessaire. backoffLimit: 2 limite le nombre de nouvelles tentatives après échec. Lisez la sortie du Pod terminé via le contrôleur :

kubectl logs job/report-job -n practice-batch

Vous devriez voir report-complete. En ciblant job/report-job, kubectl retrouve automatiquement le Pod du Job ; il n’est donc pas nécessaire de recopier son nom généré.

Ces trois commandes produisent un cycle de vie court et lisible :

job.batch/report-job created
job.batch/report-job condition met
report-complete

condition met signifie que le contrôleur du Job a enregistré l’achèvement ; la ligne de journal suivante correspond à la sortie de l’application. Sur un cluster local fraîchement démarré, même ce petit Job peut prendre plusieurs secondes, le temps que le réseau soit opérationnel. Attendre une condition rend le processus fiable, sans avoir à deviner la durée d’une pause.

Suivre la propriété et l’achèvement d’un Job

Dans cette étape, vous allez comparer un Job terminé avec un Deployment qui s’exécute en continu. Un Job terminé conserve son Pod afin que ses journaux et son état d’arrêt restent consultables.

Commencez par l’affichage sous forme de tableau classique. Le Job devrait afficher une complétion réussie, tandis que son Pod devrait avoir la phase Completed ou Succeeded, selon la colonne consultée :

kubectl get jobs,pods -n practice-batch
kubectl get pods -n practice-batch -l job-name=report-job -o custom-columns='POD:.metadata.name,PHASE:.status.phase,OWNER:.metadata.ownerReferences[0].name,EXIT_CODE:.status.containerStatuses[0].state.terminated.exitCode'
kubectl describe job report-job -n practice-batch

La phase du Pod devrait être Succeeded, son propriétaire devrait être report-job et le code de sortie devrait être 0. Un Job est terminé lorsque le nombre requis de Pods a réussi ; il ne redémarre pas un conteneur qui a réussi simplement pour le maintenir en fonctionnement.

La requête à colonnes personnalisées transforme quatre champs imbriqués en un petit tableau de vérification. C’est plus pratique que de parcourir l’intégralité du YAML du Pod lorsque la question précise est : « quel est le propriétaire de ce Pod et son processus s’est-il terminé correctement ? »

Le résultat obtenu lors du test ressemblait à ceci :

POD                PHASE       OWNER        EXIT_CODE
report-job-cd54c   Succeeded   report-job   0

Le suffixe généré sera différent. Lisez la ligne de gauche à droite : OWNER=report-job relie le Pod à son contrôleur, tandis que PHASE=Succeeded associé à EXIT_CODE=0 confirme que le processus s’est terminé avec succès.

Exécuter plusieurs complétions en parallèle

Dans cette étape, vous allez créer un Job qui exige quatre Pods terminés avec succès, tout en autorisant l’exécution simultanée de deux Pods.

Avant d’écrire le manifeste, retenez ces deux nombres indépendants :

  • completions: 4 signifie que quatre unités de travail doivent réussir au total.
  • parallelism: 2 signifie que Kubernetes peut exécuter au maximum deux de ces unités simultanément.

completionMode: Indexed attribue aux unités de travail les index 0 à 3. Chaque Pod reçoit son propre index via la variable d’environnement JOB_COMPLETION_INDEX, ce qui permet aux programmes de traitement par lots de répartir un jeu de données sans que chaque Pod traite la même partition.

cat <<'EOF' > indexed-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: indexed-job
  namespace: practice-batch
spec:
  completionMode: Indexed
  completions: 4
  parallelism: 2
  backoffLimitPerIndex: 1
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: worker
          image: busybox:1.36
          imagePullPolicy: IfNotPresent
          command: ["sh", "-c", "echo worker-index=${JOB_COMPLETION_INDEX}; sleep 2"]
EOF

Appliquez le manifeste et attendez la fin des quatre index :

La dernière commande utilise -L pour ajouter, comme colonne supplémentaire, la valeur du libellé batch.kubernetes.io/job-completion-index. Vous pouvez ainsi voir à quel index de travail correspond chaque Pod généré, sans ouvrir son YAML complet.

kubectl apply -f indexed-job.yaml
kubectl wait --for=condition=Complete job/indexed-job -n practice-batch --timeout=90s
kubectl get pods -n practice-batch -l job-name=indexed-job -L batch.kubernetes.io/job-completion-index

completions correspond au nombre total d’unités de travail qui doivent réussir ; parallelism indique le nombre maximal de Pods pouvant s’exécuter en parallèle. La complétion indexée fournit à chaque Pod un index stable via JOB_COMPLETION_INDEX.

Le tableau final devrait contenir quatre Pods terminés, avec des libellés d’index différents. Ils peuvent ne pas apparaître dans l’ordre numérique : les exécutions sont concurrentes et leur ordre d’achèvement n’est donc pas déterministe.

Par exemple :

NAME                  STATUS      JOB-COMPLETION-INDEX
indexed-job-0-gw5wq   Completed   0
indexed-job-1-vw4qk   Completed   1
indexed-job-2-mpvnr   Completed   2
indexed-job-3-8z5j8   Completed   3

Voir quatre lignes ne signifie pas que quatre Pods ont été exécutés simultanément. Les quatre index prouvent que la totalité du travail a été effectuée, tandis que parallelism: 2 limitait le nombre de Pods que Kubernetes pouvait exécuter à la fois.

Planifier un CronJob

Dans cette étape, vous allez créer un CronJob qui démarre un Job toutes les minutes. La syntaxe cron comporte cinq champs : minute, heure, jour du mois, mois et jour de la semaine.

La planification */1 * * * * signifie « toutes les minutes ». Elle est volontairement fréquente afin que vous puissiez l’observer rapidement pendant l’atelier. En production, les planifications sont généralement moins rapprochées. Le manifeste contient également un jobTemplate imbriqué, car un CronJob crée des Jobs, qui créent ensuite des Pods.

cat <<'EOF' > heartbeat-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: heartbeat
  namespace: practice-batch
spec:
  schedule: "*/1 * * * *"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 2
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: heartbeat
              image: busybox:1.36
              imagePullPolicy: IfNotPresent
              command: ["sh", "-c", "date -u; echo heartbeat-complete"]
EOF

Appliquez la planification et examinez son résumé :

kubectl apply -f heartbeat-cronjob.yaml
kubectl get cronjob heartbeat -n practice-batch

concurrencyPolicy: Forbid empêche une nouvelle exécution de chevaucher une exécution précédente qui ne serait pas terminée. Créez immédiatement un Job à partir du modèle du CronJob afin de pouvoir l’examiner sans attendre la minute suivante :

kubectl create job --from=cronjob/heartbeat heartbeat-manual -n practice-batch
kubectl wait --for=condition=Complete job/heartbeat-manual -n practice-batch --timeout=60s
kubectl logs job/heartbeat-manual -n practice-batch

Le journal contient un horodatage UTC suivi de heartbeat-complete. Le Job créé manuellement utilise le même modèle, mais ne modifie pas la planification future du CronJob.

Un résultat typique est :

job.batch/heartbeat-manual condition met
Thu Aug 13 07:48:29 UTC 2026
heartbeat-complete

Votre horodatage sera différent. Notez que le conteneur utilise l’heure UTC grâce à date -u ; cela évite de confondre l’heure de planification du cluster avec le fuseau horaire local du terminal.

Suspendre et reprendre les tâches planifiées

Dans cette étape, vous allez mettre le CronJob en pause sans supprimer sa définition. La suspension est utile pendant une maintenance ou lors de la gestion d’un incident.

kubectl patch ne modifie que le champ indiqué dans la charge utile JSON. Ici, il définit spec.suspend sur true ; l’objet CronJob, sa planification et les Jobs déjà créés restent inchangés.

kubectl patch cronjob heartbeat -n practice-batch --type=merge -p '{"spec":{"suspend":true}}'
kubectl get cronjob heartbeat -n practice-batch -o custom-columns='NAME:.metadata.name,SCHEDULE:.spec.schedule,SUSPEND:.spec.suspend,LAST_SCHEDULE:.status.lastScheduleTime'

Les Jobs existants continuent leur exécution ; la suspension empêche uniquement la création de nouveaux Jobs planifiés. Reprenez ensuite la planification :

kubectl patch cronjob heartbeat -n practice-batch --type=merge -p '{"spec":{"suspend":false}}'
kubectl get jobs -n practice-batch --sort-by=.metadata.creationTimestamp

Vous pouvez maintenant distinguer les trois niveaux : le CronJob définit une planification, chaque exécution planifiée crée un Job et chaque Job crée un ou plusieurs Pods.

Comme l’exercice peut se terminer avant le passage à la minute suivante, LAST_SCHEDULE peut encore afficher <none>. Ce n’est pas une erreur : heartbeat-manual a été créé à la demande avec --from, et non par le planificateur du CronJob. La valeur finale importante est SUSPEND=false, qui permet de nouveau les futures exécutions planifiées.

Résumé

Vous avez utilisé les Jobs pour les traitements finis, les complétions indexées pour les tâches parallèles et les CronJobs pour les planifications récurrentes. Vous avez examiné la propriété des contrôleurs, les journaux, l’état d’achèvement, la politique de concurrence, les limites d’historique et la suspension. Ces différences de cycle de vie sont essentielles pour choisir le contrôleur de charge de travail Kubernetes approprié.