CronJobs Kubernetes
Les CronJobs permettent d'exécuter des tâches planifiées de manière périodique sur un cluster Kubernetes, similairement au cron Unix/Linux. Ils sont idéaux pour les traitements batch, les sauvegardes automatiques, les rapports périodiques ou tout job nécessitant une exécution planifiée.
Introduction
Un CronJob Kubernetes crée des objets Job selon une planification définie au format cron. Chaque Job créé déploie un ou plusieurs Pods pour exécuter la tâche demandée.
Architecture et fonctionnement
CronJob (schedule: "0 2 * * *")
└─> Job-1 (créé à 02:00)
└─> Pod-1 (exécution de la tâche)
└─> Job-2 (créé à 02:00 le lendemain)
└─> Pod-2 (exécution de la tâche)Le contrôleur CronJob :
- Vérifie toutes les 10 secondes la liste des
CronJobsdu cluster - Calcule si un
Jobdoit être créé en fonction duscheduleconfiguré - Gère les politiques de concurrence et les délais de démarrage
- Conserve l'historique des
Jobsselon les paramètres de rétention
Exemple de configuration
CronJob basique
apiVersion: batch/v1
kind: CronJob
metadata:
name: backup-database
namespace: production
labels:
app: backup
component: database
spec:
# Exécution tous les jours à 2h du matin
schedule: "0 2 * * *"
# Politique de concurrence : interdire l'exécution simultanée
concurrencyPolicy: Forbid
# Nombre de Jobs réussis à conserver dans l'historique
successfulJobsHistoryLimit: 3
# Nombre de Jobs échoués à conserver dans l'historique
failedJobsHistoryLimit: 5
# Délai maximum après l'heure planifiée pour démarrer le Job
startingDeadlineSeconds: 300
# Template du Job à créer
jobTemplate:
metadata:
labels:
app: backup
component: database
spec:
# Le Job sera conservé 1 jour après son exécution
ttlSecondsAfterFinished: 86400
# Politique de retry : 3 tentatives maximum
backoffLimit: 3
# Template du Pod
template:
metadata:
labels:
app: backup
component: database
spec:
restartPolicy: OnFailure
containers:
- name: backup
image: postgres:15-alpine
command:
- /bin/sh
- -c
- |
pg_dump -h $DB_HOST -U $DB_USER -d $DB_NAME | \
gzip > /backup/dump-$(date +%Y%m%d-%H%M%S).sql.gz
env:
- name: DB_HOST
value: "postgresql-service"
- name: DB_USER
valueFrom:
secretKeyRef:
name: db-credentials
key: username
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
- name: DB_NAME
value: "production"
volumeMounts:
- name: backup-volume
mountPath: /backup
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
volumes:
- name: backup-volume
persistentVolumeClaim:
claimName: backup-pvcSyntaxe du format cron
| Champ | Valeurs possibles | Exemple |
|---|---|---|
| Minutes | 0-59 | 30 = à la 30ème minute |
| Heures | 0-23 | 14 = à 14h |
| Jour du mois | 1-31 | 15 = le 15 du mois |
| Mois | 1-12 | 6 = juin |
| Jour de la semaine | 0-6 (0 = dimanche) | 1 = lundi |
# Toutes les heures
schedule: "0 * * * *"
# Tous les jours à 3h30 du matin
schedule: "30 3 * * *"
# Tous les lundis à 8h
schedule: "0 8 * * 1"
# Le 1er de chaque mois à minuit
schedule: "0 0 1 * *"
# Toutes les 15 minutes
schedule: "*/15 * * * *"
# Du lundi au vendredi à 18h
schedule: "0 18 * * 1-5"Paramètres de configuration clés
concurrencyPolicy
Définit le comportement lorsqu'un Job est encore en cours d'exécution au moment de la prochaine planification :
| Valeur | Comportement | Cas d'usage |
|---|---|---|
Allow (défaut) | Autorise l'exécution simultanée de plusieurs Jobs | Jobs indépendants, traitement parallèle acceptable |
Forbid | Empêche le démarrage si un Job est déjà en cours | Traitement exclusif (backup, migration) |
Replace | Annule le Job en cours et démarre le nouveau | Le dernier Job doit toujours s'exécuter |
spec:
concurrencyPolicy: ForbidstartingDeadlineSeconds
Délai maximum (en secondes) après l'heure planifiée pendant lequel le Job peut encore démarrer. Si ce délai est dépassé, le Job ne sera pas créé.
spec:
# Le Job peut démarrer jusqu'à 5 minutes après l'heure planifiée
startingDeadlineSeconds: 300- Si défini : Le contrôleur compte les planifications manquées depuis
now - startingDeadlineSeconds - Si non défini (
nil) : Le contrôleur compte les planifications manquées depuislastScheduleTime, sans limite de temps
- Job critique qui perd son utilité après un certain délai (ex: traitement temps réel)
- Limiter l'accumulation de planifications manquées lors d'une indisponibilité du cluster
successfulJobsHistoryLimit et failedJobsHistoryLimit
Nombre de Jobs terminés (réussis ou échoués) à conserver dans l'historique :
spec:
successfulJobsHistoryLimit: 3 # Conserver les 3 derniers Jobs réussis
failedJobsHistoryLimit: 5 # Conserver les 5 derniers Jobs échouéssuspend
Permet de suspendre temporairement l'exécution du CronJob sans le supprimer :
spec:
suspend: true# Suspendre un CronJob
kubectl patch cronjob backup-database -n production -p '{"spec":{"suspend":true}}'
# Réactiver un CronJob
kubectl patch cronjob backup-database -n production -p '{"spec":{"suspend":false}}'Commandes kubectl utiles
Consultation et monitoring
# Lister tous les CronJobs
kubectl get cronjobs -A
# Détails d'un CronJob spécifique
kubectl -n production describe cronjob backup-database
# Voir les Jobs créés par un CronJob
kubectl -n production get jobs -l app=backup
# Voir l'historique des exécutions (status, démarrage, terminaison)
kubectl -n production get jobs -l app=backup --sort-by=.status.startTime
# Voir les Pods d'un Job spécifique
kubectl -n production get pods -l job-name=backup-database-28480640
# Consulter les logs du dernier Job exécuté
kubectl -n production logs -l app=backup --tail=100
# Événements liés au CronJob
kubectl -n production get events --field-selector involvedObject.kind=CronJob,involvedObject.name=backup-database
# Statut détaillé (lastScheduleTime, active Jobs)
kubectl -n production get cronjob backup-database -o yaml | grep -A 5 status:Opérations courantes
# Créer un CronJob depuis un manifeste
kubectl apply -f cronjob-backup.yaml
# Déclencher manuellement un Job à partir d'un CronJob
kubectl -n production create job backup-manual-$(date +%Y%m%d-%H%M%S) \
--from=cronjob/backup-database
# Suspendre un CronJob
kubectl -n production patch cronjob backup-database -p '{"spec":{"suspend":true}}'
# Réactiver un CronJob
kubectl -n production patch cronjob backup-database -p '{"spec":{"suspend":false}}'
# Supprimer tous les Jobs terminés d'un CronJob
kubectl -n production delete jobs -l app=backup --field-selector status.successful=1
# Supprimer un CronJob et ses Jobs associés
kubectl -n production delete cronjob backup-database --cascade=foregroundDebugging
# Vérifier pourquoi un Job n'a pas démarré
kubectl describe cronjob backup-database -n production | grep -A 10 Events
# Voir les Pods échoués d'un Job
kubectl get pods -n production -l job-name=backup-database-28480640 \
--field-selector status.phase=Failed
# Logs d'un Pod échoué
kubectl logs -n production <pod-name> --previous
# Diagnostiquer les erreurs d'un Job
kubectl describe job backup-database-28480640 -n production
# Voir la configuration effective d'un CronJob
kubectl get cronjob backup-database -n production -o yamlTroubleshooting
Erreur FailedNeedsStart : Trop de planifications manquées
Symptôme
Le CronJob n'exécute plus de Jobs et affiche l'événement suivant :
"FailedNeedsStart", "Cannot determine if job needs to be started. Too many missed start time (> 100). Set or decrease .spec.startingDeadlineSeconds or check clock skew."Analyse du fonctionnement
Après analyse de la base de code de Kubernetes, voici le mécanisme exact du contrôleur CronJob :
Étape 1. Vérification périodique (toutes les 10 secondes)
Le contrôleur vérifie toutes les 10 secondes l'ensemble des CronJobs du cluster pour déterminer si un Job doit être créé.
Étape 2. Calcul des planifications manquées
Pour chaque CronJob, le contrôleur calcule le nombre de planifications manquées entre lastScheduleTime et l'instant présent.
startingDeadlineSeconds :-
Si défini (valeur numérique) : Le calcul des planifications manquées se fait sur la période
[now - startingDeadlineSeconds, now]Exemple : Si
startingDeadlineSeconds: 200, le contrôleur compte combien de planifications ont été manquées dans les 200 dernières secondes uniquement. -
Si non défini (
nil) : Le calcul se fait depuislastScheduleTimejusqu'à maintenant, sans limite de temps.
L'implémentation exacte du comptage est disponible ici.
Étape 3. Limite des 100 planifications manquées
Si le nombre de planifications manquées dépasse 100, le CronJob ne créera aucun Job et émettra l'événement FailedNeedsStart.
Étape 4. Vérification du délai de démarrage
Si le nombre de planifications manquées est inférieur à 100, le contrôleur vérifie si l'heure actuelle dépasse scheduleTime + startingDeadlineSeconds.
- Dans les délais : Le
Jobest créé normalement - Hors délai : Le
Jobn'est pas créé et l'événement suivant est enregistré :
"Missed starting window for {cronjob-name}. Missed scheduled time to start a job {scheduledTime}"Causes fréquentes
| Cause | Explication |
|---|---|
| Cluster suspendu/indisponible | Le contrôleur n'a pas pu traiter les CronJobs pendant une longue période |
concurrencyPolicy: Forbid | Les exécutions successives bloquées s'accumulent comme planifications manquées |
| Job de longue durée | Un Job qui dure plusieurs cycles de planification empêche les suivants de démarrer |
| Schedule trop fréquent | Planification toutes les minutes avec Jobs durant plus d'une minute |
startingDeadlineSeconds non défini | Accumulation illimitée des planifications manquées lors d'indisponibilités |
Solutions
Étape 1. Définir ou réduire startingDeadlineSeconds
Limiter la fenêtre de calcul des planifications manquées :
spec:
# Autoriser un démarrage jusqu'à 5 minutes après l'heure planifiée
startingDeadlineSeconds: 300
schedule: "0 * * * *" # Toutes les heuresAvec cette configuration, même après une indisponibilité prolongée du cluster, seules les planifications des 5 dernières minutes seront prises en compte.
Étape 2. Supprimer temporairement le CronJob et le recréer
# Sauvegarder la configuration
kubectl get cronjob backup-database -n production -o yaml > cronjob-backup.yaml
# Supprimer le CronJob
kubectl delete cronjob backup-database -n production
# Attendre quelques secondes puis recréer
kubectl apply -f cronjob-backup.yamlCette action réinitialise le lastScheduleTime et efface l'historique des planifications manquées.
Étape 3. Ajuster concurrencyPolicy
Si les Jobs peuvent s'exécuter en parallèle sans conflit :
spec:
concurrencyPolicy: Allow # Au lieu de ForbidÉtape 4. Vérifier la synchronisation horaire du cluster
L'erreur mentionne également check clock skew. Vérifiez que les nœuds du cluster et le Control Plane sont synchronisés (NTP configuré correctement).
# Sur chaque nœud du cluster
timedatectl statusÉtape 5. Optimiser la durée d'exécution des Jobs
Si les Jobs sont trop longs :
- Optimiser le traitement
- Réduire les ressources allouées si elles sont excessives
- Envisager de découper le traitement en sous-tâches parallèles
Questions fréquentes
Q1 : SistartingDeadlineSeconds: 10 et que le CronJob ne peut pas démarrer à l'heure planifiée, peut-il démarrer dans les 10 secondes suivantes ?
Réponse : Oui. Le contrôleur tentera de créer le Job tant que le délai de 10 secondes après l'heure planifiée n'est pas écoulé. Si ce délai est dépassé, cette exécution ne sera pas faite et sera comptabilisée comme planification manquée pour les vérifications ultérieures.
concurrencyPolicy: Forbid, si un CronJob essaie d'être planifié alors qu'un Job est déjà en cours, est-ce comptabilisé comme un échec ?
Réponse : Oui, cela est considéré comme une planification manquée. Le contrôleur enregistre que l'exécution prévue n'a pas eu lieu en raison de la politique de concurrence. Si ce scénario se répète et que startingDeadlineSeconds n'est pas défini, cela peut conduire à l'accumulation de plus de 100 planifications manquées et déclencher l'erreur FailedNeedsStart.
Job qui ne démarre jamais
Symptôme
Le CronJob est actif, mais aucun Job n'est créé aux heures planifiées.
Diagnostic
# Vérifier le statut du CronJob
kubectl get cronjob backup-database -n production -o yaml | grep -A 5 status
# Consulter les événements
kubectl describe cronjob backup-database -n production | grep -A 10 Events
# Vérifier si le CronJob est suspendu
kubectl get cronjob backup-database -n production -o jsonpath='{.spec.suspend}'Causes et solutions
| Cause | Solution |
|---|---|
| CronJob suspendu | Réactiver : kubectl patch cronjob <name> -p '{"spec":{"suspend":false}}' |
| Expression cron invalide | Valider sur crontab.guru et corriger |
| Problème de fuseau horaire | Vérifier le fuseau du Control Plane |
| Quota de ressources atteint | Vérifier les ResourceQuotas du namespace |
Job échoue systématiquement
Symptôme
Le CronJob crée des Jobs, mais ils échouent à chaque exécution.
Diagnostic
# Lister les Jobs échoués
kubectl get jobs -n production -l app=backup --field-selector status.successful=0
# Détails d'un Job échoué
kubectl describe job <job-name> -n production
# Logs du Pod échoué
kubectl logs -n production <pod-name>
# Voir les tentatives de restart
kubectl get pods -n production -l job-name=<job-name> -o wideCauses courantes
| Cause | Symptôme | Solution |
|---|---|---|
| Image inexistante | ErrImagePull, ImagePullBackOff | Vérifier le nom et le tag de l'image |
| Secrets manquants | Erreur au démarrage du conteneur | Vérifier que les Secrets existent dans le bon namespace |
| Volumes non disponibles | Pod en état Pending | Vérifier les PVC et la disponibilité du stockage |
| Ressources insuffisantes | Pod en état Pending | Ajuster les requests/limits ou scaler le cluster |
| Erreur applicative | Exit code non-zero | Analyser les logs et corriger le script/application |
| Timeout | Pod tué après activeDeadlineSeconds | Augmenter la limite ou optimiser le traitement |
Jobs non nettoyés (historique surchargé)
Symptôme
Des dizaines/centaines de Jobs et Pods terminés encombrent le namespace.
Solution
# Ajuster les limites d'historique dans le CronJob
kubectl patch cronjob backup-database -n production --type=merge -p '
spec:
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
'
# Nettoyer manuellement les Jobs terminés
kubectl delete jobs -n production -l app=backup --field-selector status.successful=1
# Nettoyer les Pods terminés
kubectl delete pods -n production --field-selector status.phase=SucceededLogs disparus après expiration du Pod
Problème
Les Pods sont supprimés automatiquement et les logs ne sont plus accessibles.
Solutions
Étape 1. Augmenter la durée de rétention
spec:
successfulJobsHistoryLimit: 5 # Conserver plus de Jobs
jobTemplate:
spec:
ttlSecondsAfterFinished: 86400 # Garder les Jobs 24hÉtape 2. Centraliser les logs
Mettre en place une solution de collecte de logs (Loki, ELK, Datadog, etc.) pour conserver les logs au-delà de la durée de vie des Pods.
Étape 3. Rediriger les logs vers un volume persistant
spec:
jobTemplate:
spec:
template:
spec:
containers:
- name: job
command:
- /bin/sh
- -c
- |
# Rediriger stdout et stderr vers un fichier
/app/script.sh 2>&1 | tee /logs/job-$(date +%Y%m%d-%H%M%S).log
volumeMounts:
- name: logs-volume
mountPath: /logs
volumes:
- name: logs-volume
persistentVolumeClaim:
claimName: logs-pvcBonnes pratiques
Conception et configuration
| Pratique | Justification |
|---|---|
Définir startingDeadlineSeconds | Évite l'accumulation infinie de planifications manquées lors d'indisponibilités |
Utiliser concurrencyPolicy: Forbid | Prévient les exécutions simultanées pour les traitements exclusifs (backup, migration) |
| Limiter l'historique des Jobs | successfulJobsHistoryLimit: 3, failedJobsHistoryLimit: 5 pour éviter la surcharge etcd |
Configurer ttlSecondsAfterFinished | Nettoyage automatique des Jobs terminés après un délai défini |
Définir activeDeadlineSeconds | Évite qu'un Job bloqué consomme des ressources indéfiniment |
Spécifier resources (requests/limits) | Garantit la planification et évite la famine de ressources |
Utiliser restartPolicy: OnFailure | Permet les tentatives de retry dans le même Pod au lieu de créer de nouveaux Pods |
Exemple de configuration optimale
apiVersion: batch/v1
kind: CronJob
metadata:
name: optimized-cronjob
namespace: production
labels:
app.kubernetes.io/name: backup
app.kubernetes.io/component: database
environment: production
spec:
schedule: "0 2 * * *"
concurrencyPolicy: Forbid
startingDeadlineSeconds: 600
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
suspend: false
jobTemplate:
metadata:
labels:
app.kubernetes.io/name: backup
app.kubernetes.io/component: database
spec:
# Nettoyage automatique après 24h
ttlSecondsAfterFinished: 86400
# 3 tentatives maximum en cas d'échec
backoffLimit: 3
# Timeout global du Job : 30 minutes
activeDeadlineSeconds: 1800
template:
metadata:
labels:
app.kubernetes.io/name: backup
spec:
restartPolicy: OnFailure
# ServiceAccount dédié avec droits minimaux
serviceAccountName: backup-sa
containers:
- name: backup
image: registry.example.com/backup-tool:1.2.3
imagePullPolicy: IfNotPresent
command: ["/bin/sh", "-c"]
args:
- |
set -euo pipefail
echo "Starting backup at $(date)"
/usr/local/bin/backup-script.sh
echo "Backup completed at $(date)"
env:
- name: ENV
value: "production"
envFrom:
- secretRef:
name: backup-secrets
- configMapRef:
name: backup-config
resources:
requests:
cpu: 200m
memory: 512Mi
limits:
cpu: 1000m
memory: 1Gi
volumeMounts:
- name: backup-storage
mountPath: /backups
# Health check pour détecter les blocages
livenessProbe:
exec:
command: ["/bin/sh", "-c", "pgrep -f backup-script"]
initialDelaySeconds: 30
periodSeconds: 60
timeoutSeconds: 5
failureThreshold: 3
volumes:
- name: backup-storage
persistentVolumeClaim:
claimName: backup-pvc
# Tolérations et affinité si besoin
tolerations:
- key: "workload"
operator: "Equal"
value: "batch"
effect: "NoSchedule"Monitoring et alerting
Métriques à surveiller :| Métrique | Alerte si | Action |
|---|---|---|
| Nombre de planifications manquées | > 5 consécutives | Investiguer les logs du CronJob |
| Taux d'échec des Jobs | > 50% sur 24h | Vérifier les logs des Pods échoués |
| Durée d'exécution | > 2x la durée moyenne | Optimiser le traitement ou augmenter les ressources |
| Nombre de Jobs en cours | > 1 (si concurrencyPolicy: Forbid) | Vérifier pourquoi les Jobs ne se terminent pas |
- alert: CronJobFailing
expr: |
kube_cronjob_status_last_schedule_time
and on (cronjob) (kube_job_status_failed{job=~".*"} > 0)
for: 1h
labels:
severity: warning
annotations:
summary: "CronJob {{ $labels.cronjob }} échoue"
description: "Le CronJob {{ $labels.cronjob }} a des échecs répétés."
- alert: CronJobNotScheduled
expr: |
time() - kube_cronjob_status_last_schedule_time > 3600 * 2
for: 10m
labels:
severity: critical
annotations:
summary: "CronJob {{ $labels.cronjob }} non exécuté"
description: "Aucune exécution depuis plus de 2h."Sécurité
- ServiceAccount dédié : Créez un
ServiceAccountspécifique avec les permissions RBAC minimales nécessaires - Secrets chiffrés : Utilisez des
Secretspour les credentials, jamais de valeurs en dur - Image de confiance : Utilisez des images provenant de registres privés ou validés
- Limites de ressources : Évitez les abus de ressources avec des
limitsappropriées - Scan de vulnérabilités : Scannez les images des
CronJobsrégulièrement
Cas d'usage courants
Sauvegarde de base de données
schedule: "0 2 * * *" # Tous les jours à 2h
concurrencyPolicy: ForbidNettoyage de fichiers temporaires
schedule: "0 */6 * * *" # Toutes les 6 heures
concurrencyPolicy: Replace
ttlSecondsAfterFinished: 3600Génération de rapports hebdomadaires
schedule: "0 8 * * 1" # Tous les lundis à 8h
concurrencyPolicy: Allow
successfulJobsHistoryLimit: 10Synchronisation de données
schedule: "*/30 * * * *" # Toutes les 30 minutes
concurrencyPolicy: Forbid
startingDeadlineSeconds: 1800