## Introduction

Les CronJobs Kubernetes sont le mécanisme de prédilection pour exécuter des charges de travail basées sur le temps : sauvegardes nocturnes de bases de données, génération de rapports horaires, réchauffement périodique du cache, renouvellement de certificats, ou toute tâche qui doit se répéter selon un horaire fixe. Si les bases sont simples, les CronJobs de niveau production exigent une compréhension approfondie de la sémantique de planification, du contrôle de concurrence, du comportement de relance, de l'idempotence, de l'observabilité et de la sécurité.

Cet article s'adresse aux développeurs, aux ingénieurs DevOps et aux équipes plateforme qui savent déjà créer un CronJob simple, mais qui doivent gérer les cas limites et les modes de défaillance. Nous explorerons des concepts avancés avec des exemples concrets, des sorties de commandes et des extraits de configuration que vous pourrez adapter immédiatement.

Vous apprendrez :

- Comment contrôler les exécutions concurrentes de manière fiable avec `concurrencyPolicy`
- Comment gérer le comportement de relance et d'échec avec `backoffLimit` et `failedJobsHistoryLimit`
- Comment éviter les horaires manqués et les chevauchements avec `startingDeadlineSeconds`
- Comment concevoir des tâches idempotentes pouvant être relancées en toute sécurité
- Comment surveiller les CronJobs avec les métriques Prometheus et les alertes
- Comment sécuriser les CronJobs en utilisant des comptes de service dédiés, le RBAC et la gestion des secrets
- Une liste de contrôle pratique pour le dépannage des pannes courantes des CronJobs

Prérequis : un cluster Kubernetes en cours d'exécution (v1.21 ou ultérieure recommandée pour une API CronJob stable), `kubectl` configuré, et une familiarité de base avec les objets Kubernetes comme les Pods, les Jobs et les Deployments.

## Inventaire de la version et de l'environnement

Avant d'appliquer toute configuration, vérifiez la version de Kubernetes de votre cluster et l'API CronJob disponible. CronJob est passé en version stable (`batch/v1`) dans Kubernetes 1.21 ; les clusters plus anciens peuvent encore utiliser `batch/v1beta1`.

Vérifiez la version de votre serveur :

```bash
kubectl version --short
```

Sortie attendue (exemple) :
```
Client Version: v1.27.3
Kustomize Version: v5.0.1
Server Version: v1.28.2
```

Si la version de votre serveur est inférieure à 1.21, envisagez une mise à niveau ou utilisez l'API bêta, mais notez que les API bêta sont supprimées dans les versions ultérieures. Épinglez toujours vos manifestes à la version d'API prise en charge par votre cluster.

Listez tous les CronJobs dans le namespace actuel et inspectez leurs détails :

```bash
kubectl get cronjobs -n myapp
kubectl describe cronjob <cronjob-name> -n myapp
```

La sortie comprend des champs importants comme `Schedule`, `Concurrency Policy`, `Starting Deadline Seconds`, `Last Schedule Time` et `Active Jobs`. Capturez cet état avant d'apporter des modifications.

Pour une observation en lecture seule du comportement du contrôleur, consultez les journaux du contrôleur CronJob (si accessibles) :

```bash
kubectl logs -n kube-system deployment/cronjob-controller --tail=50
```

Cela peut révéler les décisions de planification, les exécutions manquées ou les erreurs d'API. Cependant, dans les services Kubernetes gérés (EKS, GKE, AKS), les journaux du plan de contrôle peuvent ne pas être directement accessibles ; utilisez plutôt la surveillance du fournisseur cloud.

Enfin, assurez-vous de disposer des autorisations RBAC nécessaires pour gérer les CronJobs :

```bash
kubectl auth can-i create cronjobs -n myapp
kubectl auth can-i list jobs -n myapp
```

Si une commande renvoie `no`, demandez le rôle approprié à votre administrateur de cluster.

## Chemin de configuration sécurisé

Lors de la modification d'un CronJob en production, suivez un processus contrôlé : apportez des modifications minimes, testez dans un namespace de staging et vérifiez avec `kubectl diff` avant d'appliquer.

### Étape 1 : Exporter le manifeste actuel comme référence

```bash
kubectl get cronjob my-backup -n myapp -o yaml > cronjob-baseline.yaml
```

Gardez toujours une sauvegarde. En cas de problème, vous pouvez restaurer avec `kubectl apply -f cronjob-baseline.yaml`.

### Étape 2 : Prévisualiser les modifications avec `kubectl diff`

Après avoir modifié le YAML, voyez exactement ce qui va changer :

```bash
kubectl diff -f cronjob-updated.yaml
```

Cela affiche un diff unifié sans modifier l'objet en direct.

### Étape 3 : Appliquer d'abord avec un essai à vide

```bash
kubectl apply -f cronjob-updated.yaml --dry-run=server
```

Le serveur valide la requête par rapport au schéma de l'API sans la persister. Attendez-vous à une erreur en cas de problème de syntaxe ou de validation, comme une expression cron invalide.

### Étape 4 : Appliquer et surveiller immédiatement

```bash
kubectl apply -f cronjob-updated.yaml
kubectl get cronjobs -n myapp -w
```

Dans un autre terminal, suivez les événements :

```bash
kubectl get events -n myapp --watch | grep -E 'CronJob|Job'
```

Recherchez `Created job`, `Saw completed job` ou des messages d'erreur concernant la planification.

### Étape 5 : Valider la prochaine exécution planifiée

Si votre planification est fréquente (par exemple, `*/5 * * * *`), attendez la prochaine exécution et inspectez le Job créé :

```bash
kubectl get jobs -n myapp --sort-by=.metadata.creationTimestamp | tail -3
```

Si la planification est moins fréquente, vous pouvez déclencher manuellement un test (voir Vérification et diagnostics).

## Vérification et diagnostics

Un CronJob n'est sain que s'il crée des Jobs qui finissent par se terminer avec succès. Utilisez ces commandes pour vérifier et diagnostiquer.

### Vérifier le statut du CronJob

```bash
kubectl get cronjob my-backup -n myapp -o yaml
```

Sous `status`, recherchez :
- `active` : tableau des Jobs en cours d'exécution
- `lastScheduleTime` : horodatage de la dernière planification
- `lastSuccessfulTime` : horodatage de la dernière exécution réussie du Job

Un CronJob sans `lastSuccessfulTime` peut indiquer des échecs ou aucune exécution jusqu'à présent.

### Inspecter les Jobs enfants

Listez les Jobs appartenant à un CronJob :

```bash
kubectl get jobs -n myapp -l app=my-backup -o wide
```

Exemple de sortie :
```
NAME                       COMPLETIONS   DURATION   AGE
my-backup-28051234         1/1           2m10s      25m
my-backup-28051834         1/1           2m05s      19m
my-backup-28052434         0/1           ---        13m
```

Le dernier Job a `0/1` complétions, ce qui indique qu'il est peut-être encore en cours d'exécution ou qu'il a échoué.

### Examiner les Pods d'un Job

Pour un Job spécifique, trouvez ses Pods :

```bash
kubectl get pods -n myapp -l job-name=my-backup-28052434
```

Ensuite, consultez les journaux :

```bash
kubectl logs <pod-name> -n myapp
```

Pour les conteneurs qui ont planté, utilisez `--previous` :

```bash
kubectl logs <pod-name> -n myapp --previous
```

### Décrire le Job pour les événements

```bash
kubectl describe job my-backup-28052434 -n myapp
```

Les événements afficheront des erreurs de tirage d'image, des OOM kills, des échecs de commande ou un dépassement de la limite de backoff.

### Déclencher manuellement un test

Pour tester sans attendre la planification, créez un Job à partir de la spécification du CronJob :

```bash
kubectl create job test-backup --from=cronjob/my-backup -n myapp
```

Cela crée un Job ad hoc avec le même modèle de Pod. Surveillez-le :

```bash
kubectl logs test-backup-xxxxx -n myapp -f
```

Supprimez le Job de test après vérification :

```bash
kubectl delete job test-backup -n myapp
```

## Modes de défaillance et récupération

Les CronJobs échouent de manière prévisible. Comprendre ces modes permet une récupération plus rapide et des mesures préventives.

### 1. Le Job s'exécute mais le conteneur se termine avec un code non nul

C'est la panne la plus courante. Vérifiez les journaux pour l'erreur. Si la tâche est un script, assurez-vous qu'il ne se termine avec le code 0 qu'en cas de succès. Ajoutez une gestion explicite des erreurs.

Exemple de commande fragile :

```yaml
spec:
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: backup
            image: postgres:15
            command: ["/bin/sh", "-c"]
            args:
            - |
              pg_dump mydb > /backup/db.sql
```

Si `pg_dump` échoue, le shell continue et peut se terminer avec 0, masquant l'échec. Utilisez plutôt :

```yaml
args:
- |
  set -e
  pg_dump mydb > /backup/db.sql
  echo "Backup completed"
```

`set -e` force le script à se terminer à la première erreur, ce qui fait échouer le conteneur et permet au Job de l'enregistrer.

### 2. Le Job dépasse activeDeadlineSeconds

Si un Job s'exécute trop longtemps, il peut être interrompu. Définissez `activeDeadlineSeconds` pour imposer une durée maximale :

```yaml
spec:
  jobTemplate:
    spec:
      activeDeadlineSeconds: 300
```

Si le Job est interrompu en raison de la date limite, le statut du Pod indique `DeadlineExceeded`. Examinez les performances de la tâche et ajustez la date limite ou optimisez le Job.

### 3. Limite de backoff dépassée

`backoffLimit` contrôle le nombre de fois qu'un Job relance un Pod en échec avant de marquer le Job comme échoué. La valeur par défaut est 6. Ajustez-la en fonction de votre tolérance aux relances :

```yaml
spec:
  jobTemplate:
    spec:
      backoffLimit: 3
```

Lorsque la limite de backoff est dépassée, le statut du Job indique `Failed` et aucun autre Pod n'est créé. Le CronJob lui-même ne relance pas l'horaire manqué ; le prochain horaire créera un nouveau Job.

### 4. Horaires manqués en raison d'une panne du contrôleur ou d'une surcharge du cluster

Si le contrôleur CronJob ne peut pas créer un Job à l'heure prévue, l'exécution peut être manquée. Le champ `startingDeadlineSeconds` définit le délai après l'heure prévue pendant lequel le contrôleur peut démarrer le Job. Si la date limite est dépassée, le Job est ignoré et compté comme manqué.

```yaml
spec:
  startingDeadlineSeconds: 200
```

Sans cela, le CronJob peut attendre indéfiniment que le Job précédent se termine, en particulier avec `concurrencyPolicy: Forbid`.

### 5. Gestion de la concurrence

Le champ `concurrencyPolicy` contrôle les exécutions qui se chevauchent :
- `Allow` (par défaut) : plusieurs Jobs peuvent s'exécuter simultanément.
- `Forbid` : si un Job est toujours en cours d'exécution, le nouvel horaire est ignoré.
- `Replace` : si un Job est toujours en cours d'exécution, il est annulé et remplacé par un nouveau Job.

Choisissez `Forbid` pour les tâches qui ne peuvent pas se chevaucher (par exemple, les migrations de base de données) ou `Replace` lorsque seule la dernière exécution compte.

Exemple :

```yaml
spec:
  concurrencyPolicy: Forbid
```

### 6. Ressources insuffisantes ou échecs de planification

Si le cluster manque de ressources, les Pods peuvent rester en attente. Vérifiez les événements :

```bash
kubectl describe pod <pending-pod> -n myapp | tail -20
```

Les erreurs courantes incluent `Insufficient cpu`, `Insufficient memory` ou des incompatibilités de sélecteur de nœud. Ajustez les demandes/limites de ressources ou le placement des nœuds.

## Liste de contrôle des opérations

Utilisez cette liste de contrôle avant, pendant et après le déploiement ou la mise à jour d'un CronJob :

### Pré-déploiement
- [ ] La version de Kubernetes prend en charge le CronJob `batch/v1`
- [ ] Expression de planification testée (par exemple, avec un validateur d'expression cron en ligne)
- [ ] L'image du conteneur existe et est accessible depuis le cluster
- [ ] Demandes et limites de ressources définies
- [ ] `restartPolicy` défini sur `Never` ou `OnFailure` (les Jobs l'exigent)
- [ ] Les secrets sont référencés comme variables d'environnement ou volumes, pas codés en dur
- [ ] Compte de service avec autorisations RBAC minimales attribué
- [ ] `concurrencyPolicy` défini de manière appropriée
- [ ] `startingDeadlineSeconds` défini si les horaires manqués comptent
- [ ] `backoffLimit` et `activeDeadlineSeconds` ajustés
- [ ] Limites d'historique (`successfulJobsHistoryLimit`, `failedJobsHistoryLimit`) définies pour contrôler l'encombrement
- [ ] Sondes de vivacité et de préparation configurées le cas échéant
- [ ] Journalisation vers stdout et structurée pour la collecte
- [ ] Point de terminaison de métriques exposé pour Prometheus (si utilisé)

### Pendant le déploiement
- [ ] Appliqué avec un aperçu `kubectl diff`
- [ ] `kubectl apply --dry-run=server` réussi
- [ ] Événements surveillés et `kubectl get cronjobs -w` pour un retour immédiat
- [ ] Job de test déclenché manuellement si la planification est peu fréquente

### Vérification post-déploiement
- [ ] `lastScheduleTime` mis à jour après la prochaine planification
- [ ] `lastSuccessfulTime` dans la fenêtre attendue
- [ ] Job enfant terminé avec le code de sortie souhaité
- [ ] Les journaux affichent la sortie attendue
- [ ] Les métriques (le cas échéant) montrent une augmentation du nombre de succès
- [ ] Les règles d'alerte (le cas échéant) ne se déclenchent pas

### Actions de récupération
- Si un Job échoue, inspectez les journaux et les événements de son Pod
- Si la limite de backoff est dépassée, supprimez le Job en échec pour nettoyer (ou ajustez `failedJobsHistoryLimit`)
- Si le CronJob est bloqué en raison de `concurrencyPolicy: Forbid`, supprimez le Job actif et laissez le prochain horaire se poursuivre
- Si le contrôleur CronJob manque fréquemment des horaires, examinez la charge du serveur d'API ou la santé du contrôleur
- Si un Pod est tué par OOM, augmentez les limites de mémoire ou réduisez l'empreinte mémoire de la charge de travail

## Conclusion

L'utilisation avancée des CronJobs dans Kubernetes exige plus que l'écriture d'une expression cron. Vous devez concevoir pour l'idempotence, contrôler la concurrence, gérer les échecs avec élégance et observer le système avec des métriques et des journaux. En suivant les concepts et les exemples de cet article, vous pouvez créer des charges de travail planifiées fiables qui survivent aux conditions du monde réel.

N'oubliez pas de :
- Toujours tester les modifications avec `kubectl diff` et un essai à vide
- Surveiller `lastScheduleTime` et `lastSuccessfulTime`
- Utiliser `concurrencyPolicy` et `startingDeadlineSeconds` pour éviter les chevauchements et les exécutions manquées
- Définir des `backoffLimit` et `activeDeadlineSeconds` appropriés
- Sécuriser vos CronJobs avec des comptes de service dédiés et le RBAC
- Garder les limites d'historique sous contrôle pour éviter l'encombrement
- Créer des Jobs idempotents qui peuvent être relancés en toute sécurité

Un CronJob bien configuré est un cheval de trait silencieux ; un CronJob mal configuré devient une source de pages à minuit. Appliquez ces pratiques et dormez mieux en sachant que vos tâches planifiées sont en ordre.