Introduction
Les CronJobs Kubernetes sont idéaux pour les tâches récurrentes : synchronisation de données, génération de rapports, préchauffage de cache et nettoyage. Ce guide montre comment automatiser des CronJobs avec CI/CD (Continuous Integration/Continuous Deployment) : intégration continue et déploiement continu en utilisant des exemples pratiques en YAML, Python, Bash et GitLab CI. Vous apprendrez à :
- Construire et tester une image de job
- Valider les manifestes avant le déploiement
- Déployer en toute sécurité avec suspension préalable et tests à la demande
- Ajouter des tests de fumée et garde-fous
- Effectuer un rollback rapide en cas d’échec
- Diagnostiquer les échecs de pipeline courants
Pourquoi ça fonctionne : une approche guidée par l’exemple aide les équipes à passer de l’intention à une automatisation fonctionnelle, tandis qu’une séparation claire des étapes réduit le retravail et accélère les itérations.
Vue d’ensemble du workflow
Le workflow de bout en bout ci-dessous est simple, sûr et prêt pour la production.
1. Écrire le code du job
Exemple de job Python avec codes de sortie explicites et logs :
# app/main.py
import logging, os, sys, time
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s" )
def validate_env():
required = ["EXAMPLE_PARAM" ]
missing = [k for k in required if not os.getenv(k)]
if missing:
logging.error("Missing required env: %s", " , " .join(missing) )
return False
return True
def run_job():
logging.info("job start" )
# Simuler du travail
time.sleep(2)
# Ajouter la logique réelle ici
logging.info("job done" )
return 0
if __name__ == "__main__":
if not validate_env():
sys.exit(2)
code = run_job()
sys.exit(code)
Point d’entrée avec gestion d’erreur stricte :
# app/entrypoint.sh
#!/usr/bin/env bash
set -Eeuo pipefail
trap 'echo "[ERROR] line $LINENO status $?" >&2' ERR
python /app/main.py
Dockerfile minimal :
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY app/requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt || true
COPY app/ /app/
RUN useradd -u 10001 appuser && chown -R appuser /app
USER 10001
ENTRYPOINT ["bash", "/app/entrypoint.sh"]
2. Définir le manifeste CronJob
Des valeurs par défaut sûres limitent le rayon d’impact et facilitent le débogage.
# k8s/cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: report-cron
namespace: staging
spec:
schedule: "*/15 * * * *" # toutes les 15 minutes
# timeZone: "UTC" # décommenter si votre cluster le supporte
concurrencyPolicy: Forbid # éviter les exécutions chevauchées
startingDeadlineSeconds: 300 # ne pas démarrer si trop en retard
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 2
suspend: true # démarrer suspendu ; test de fumée d’abord
jobTemplate:
spec:
backoffLimit: 1
activeDeadlineSeconds: 600 # limite dure sur la durée d’exécution
template:
spec:
restartPolicy: Never
containers:
- name: report
image: registry.example.com/report-cron:${IMAGE_TAG}
imagePullPolicy: IfNotPresent
env:
- name: EXAMPLE_PARAM
value: nightly
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
imagePullSecrets:
- name: regcred
Notes :
- Utilisez des tags d’image immuables (ex. SHA du commit). Évitez
latest. - Utilisez
suspend: truepour les premiers déploiements ; activez après un test de fumée réussi. - Pour les secrets, montez depuis un Secret ou utilisez votre approche standard de gestion des secrets.
3. Ajouter les étapes CI/CD
Un exemple pratique GitLab CI avec build, validation, déploiement, test de fumée, promotion et rollback.
# .gitlab-ci.yml
stages: [test, build, validate, deploy_staging, smoke_test, deploy_prod, rollback]
variables:
APP_NAME: "report-cron"
IMAGE_REGISTRY: "registry.example.com"
IMAGE_TAG: "$CI_COMMIT_SHA"
KUBE_NAMESPACE_STAGING: "staging"
KUBE_NAMESPACE_PROD: "prod"
# Fournir DOCKER_AUTH, KUBECONFIG et kubectl dans vos runners
unit-test:
stage: test
image: python:3.11-slim
script:
- pip install -r app/requirements.txt || true
- python -m py_compile app/main.py
build-image:
stage: build
image: docker:24
services: ["docker:24-dind" ]
script:
- echo "$DOCKER_AUTH" | docker login -u "$DOCKER_USER" --password-stdin $IMAGE_REGISTRY
- docker build -t $IMAGE_REGISTRY/$APP_NAME:$IMAGE_TAG .
- docker push $IMAGE_REGISTRY/$APP_NAME:$IMAGE_TAG
needs: ["unit-test" ]
validate-manifest:
stage: validate
image: bitnami/kubectl:1.30
script:
- sed "s/\${IMAGE_TAG}/$IMAGE_TAG/g" k8s/cronjob.yaml > k8s/rendered.yaml
- kubectl apply --dry-run=client -f k8s/rendered.yaml
needs: ["build-image" ]
deploy-staging:
stage: deploy_staging
image: bitnami/kubectl:1.30
script:
- kubectl config use-context "$KUBE_CONTEXT_STAGING"
- kubectl -n $KUBE_NAMESPACE_STAGING apply -f k8s/rendered.yaml
- kubectl -n $KUBE_NAMESPACE_STAGING annotate cronjob/$APP_NAME ci-commit=$CI_COMMIT_SHA --overwrite
needs: ["validate-manifest" ]
environment:
name: staging
smoke-test-staging:
stage: smoke_test
image: bitnami/kubectl:1.30
script:
- kubectl config use-context "$KUBE_CONTEXT_STAGING"
- export SMOKE_JOB=${APP_NAME}-smoke-$(date +%s)
- kubectl -n $KUBE_NAMESPACE_STAGING create job $SMOKE_JOB --from=cronjob/$APP_NAME
- kubectl -n $KUBE_NAMESPACE_STAGING wait --for=condition=complete job/$SMOKE_JOB --timeout=10m || (kubectl -n $KUBE_NAMESPACE_STAGING logs job/$SMOKE_JOB; exit 1)
- kubectl -n $KUBE_NAMESPACE_STAGING logs job/$SMOKE_JOB
- kubectl -n $KUBE_NAMESPACE_STAGING delete job/$SMOKE_JOB --ignore-not-found
- echo "Smoke test passed"
needs: ["deploy-staging" ]
promote-prod:
stage: deploy_prod
image: bitnami/kubectl:1.30
script:
- kubectl config use-context "$KUBE_CONTEXT_PROD"
- sed "s/namespace: staging/namespace: $KUBE_NAMESPACE_PROD/g" k8s/rendered.yaml > k8s/prod.yaml
- kubectl -n $KUBE_NAMESPACE_PROD apply -f k8s/prod.yaml
- kubectl -n $KUBE_NAMESPACE_PROD annotate cronjob/$APP_NAME ci-commit=$CI_COMMIT_SHA --overwrite
- # Optionnellement désuspendre après test de fumée en prod
needs: ["smoke-test-staging" ]
environment:
name: production
rollback:
stage: rollback
image: bitnami/kubectl:1.30
when: manual
variables:
ROLLBACK_TAG: "" # définir cette variable au déclenchement
script:
- test -n "$ROLLBACK_TAG" || (echo "Set ROLLBACK_TAG" && exit 1)
- kubectl config use-context "$KUBE_CONTEXT_PROD"
- kubectl -n $KUBE_NAMESPACE_PROD patch cronjob $APP_NAME --type='json' \
-p='[{"op":"replace","path":"/spec/jobTemplate/spec/template/spec/containers/0/image","value":"'$IMAGE_REGISTRY'/'$APP_NAME':'$ROLLBACK_TAG'"}]'
- # Vérifier via un job one-off avant de réactiver la planification
- export SMOKE_JOB=${APP_NAME}-rb-$(date +%s)
- kubectl -n $KUBE_NAMESPACE_PROD create job $SMOKE_JOB --from=cronjob/$APP_NAME
- kubectl -n $KUBE_NAMESPACE_PROD wait --for=condition=complete job/$SMOKE_JOB --timeout=10m || (kubectl -n $KUBE_NAMESPACE_PROD logs job/$SMOKE_JOB; exit 1)
- kubectl -n $KUBE_NAMESPACE_PROD logs job/$SMOKE_JOB
- kubectl -n $KUBE_NAMESPACE_PROD delete job/$SMOKE_JOB --ignore-not-found
Vérifications de déploiement sécurisées
- Faire un dry-run des manifestes avant l’application.
- Déployer les CronJobs suspendus dans les nouveaux environnements.
- Tester avec un Job one-off créé depuis le template du CronJob.
- Limiter la durée des exécutions avec
activeDeadlineSecondset empêcher le chevauchement avecconcurrencyPolicy: Forbid. - Utiliser des tags immuables et annoter les ressources avec le SHA du commit pour la traçabilité.
Activer les planifications après vérification
Une fois les tests de fumée passés, désuspendre la planification :
kubectl -n staging patch cronjob report-cron -p '{"spec": {"suspend": false}}'
Répéter en production après promotion et validation.
Plan pilote local
Commencez avec un seul CronJob et gardez une portée étroite.
Objectifs du pilote
- Un CronJob dans un namespace bac à sable
- Build, validation, déploiement suspendu et test de fumée réussi
- Mesuré par un code de sortie 0 et les logs attendus
Étapes
- Choisir un job à faible risque (ex. génération de rapport sans effets de bord externes).
- Conteneuriser et ajouter codes de sortie stricts et logs (comme ci-dessus).
- Écrire un manifeste CronJob avec valeurs par défaut sûres et
suspend: true. - Exécuter des vérifications locales :
- Linter le Python, lancer tests rapides
- Rendre le manifeste avec le SHA du commit
kubectl apply --dry-run=client -f k8s/rendered.yaml
- Déployer dans un namespace bac à sable.
- Créer un job one-off depuis le CronJob et attendre la fin.
- Inspecter logs et statut de sortie ; corriger les problèmes.
- Documenter les critères de succès et seulement alors désuspendre la planification.
Pourquoi ce plan : un pilote étroit, mesurable et inspectable localement est le chemin le plus rapide vers la valeur et l’apprentissage sans risquer la production.
Modèles pratiques de rollback
Les CronJobs ne supportent pas kubectl rollout undo. Gardez le rollback simple :
- Tags immuables : construire des images taguées avec le SHA du commit ; conserver une trace du dernier tag connu bon.
- Patch retour au dernier tag connu bon et vérifier avec un job one-off.
- Alternative : revenir au commit Git qui a mis à jour le tag d’image et redéployer.
Exemple de patch (déjà dans le job CI) :
kubectl -n prod patch cronjob report-cron --type='json' \
-p='[{"op":"replace","path":"/spec/jobTemplate/spec/template/spec/containers/0/image","value":"registry.example.com/report-cron:abc123"}]'
Échecs de pipeline courants et correctifs
- ImagePullBackOff : identifiants de registre manquants ou mauvais tag d’image.
- Correctif : vérifier
imagePullSecretset tags immuables ; confirmer que le push a réussi. - CrashLoopBackOff ou sortie non nulle : code ou configuration d’environnement incorrecte.
- Correctif : ajouter variables d’environnement requises ; renforcer la gestion d’erreur du point d’entrée ; inspecter les logs.
- Job dépasse la limite de temps :
- Correctif : définir
activeDeadlineSecondset revoir la charge ; diviser en tâches plus petites. - Exécutions chevauchées causent des conflits :
- Correctif :
concurrencyPolicy: Forbidet espacement raisonnable de la planification. - Heures de début manquées quand le cluster est occupé :
- Correctif : définir
startingDeadlineSecondset envisager d’augmenter les ressources. - CronJob suspendu ne s’exécute jamais :
- Correctif : désuspendre après test de fumée réussi.
- Erreurs RBAC ou de contexte dans CI :
- Correctif : confirmer contexte kubectl, namespace et permissions pour apply, create job et patch.
- Confusion de fuseau horaire Cron :
- Correctif : planifier en UTC ou valider le comportement du fuseau horaire du cluster ; documenter les attentes.
Conclusion
Automatiser les CronJobs Kubernetes avec CI/CD est simple quand on sépare les préoccupations et qu’on intègre la sécurité :
- Construire et tester l’image avec codes de sortie explicites
- Valider les manifestes avec dry runs
- Déployer suspendu, puis tester via un Job one-off
- Promouvoir avec tags immuables et annotations claires
- Garder un rollback simple et rapide en patchant le tag d’image
Commencez avec un CronJob en bac à sable, rendez-le mesurable, et n’étendez qu’une fois votre pilote vert et observable.