Les Pods de Kubernetes sont généralement utilisés pour démarrer des services persistants (en utilisant des Deployments par exemple). Kubernetes propose un certain nombre de solutions pour lancer des tâches récurrentes ou à exécution unique, principalement grâce à l'API Job.
Conteneurs d'initialisation
Avant d'aborder l'API Job, il est intéressant de se pencher sur le mécanisme puissant et très souvent indispensable que sont les conteneurs d'initialisation.
Au démarrage d'une application, il est en effet fréquemment nécessaire d'exécuter un certain nombre de tâches, par exemple :
- lancer les migrations de bases de données
- créer des répertoires et définir leurs droits d'accès
- exécuter un appel d'API externe
- etc.
Une solution possible est d'écrire un script qui exécute ces tâches avant de démarrer l'application, mais cela implique de modifier l'image Docker. De plus, cela créé une dépendance entre l'image Docker et sa plateforme d'exécution, ce qui n'est pas une bonne pratique.
Avec un conteneur d'initialisation, nous avons la possibilité d'exécuter ces tâches dans un conteneur dédié qui sera lancé au démarrage du Pod.
Nous allons illustrer le principe avec une image busybox qui affiche le contenu d'un fichier qui n'existe pas dans l'image.
apiVersion: v1
kind: Pod
metadata:
name: app-pod
labels:
app: app
spec:
containers:
- name: app-container
image: busybox
command: ['sh', '-c', 'cat /data/config && sleep 3600']
Appliquer le manifeste tel quel et afficher ses logs, on doit voir un message d'erreur.
kubectl apply -f app.yaml
kubectl logs -l app=app
cat: can't open '/data/config': No such file or directory
Modifier le manifeste en ajoutant :
- Un conteneur d'initialisation qui écrit
successdans/data/config - Un volume
emptyDirmonté sur/datadans les deux conteneurs
Utiliser le manifeste suivant comme base :
apiVersion: v1
kind: Pod
metadata:
name: app-pod
labels:
app: app
spec:
initContainers:
...
containers:
- name: app-container
image: busybox
command: ['sh', '-c', 'cat /data/config && sleep 3600']
Appliquer les changements et afficher les logs, on doit maintenant voir apparaitre success.
Lancement d'un Job simple
Pour illustrer l'intérêt des Jobs, nous allons commencer par déployer une tâche simple dans un Pod avant de la déployer dans un Job.
À titre d'exercice, nous allons utiliser un script bash qui simule l'exécution d'une tâche arbitraire. Nous allons simplement utiliser un script qui attend, et nous allons le munir d'un léger défaut : il a une chance sur deux d'échouer au démarrage.
- Déployer le Pod suivant
apiVersion: v1
kind: Pod
metadata:
name: sometask
spec:
containers:
- name: sometask
image: bash
command: ["bash"]
args: ["-c", "if [ $(( $RANDOM % 2)) == 1 ]; then exit 1; fi; echo working ...; sleep 10; echo done;"]
restartPolicy: Never
- Afficher les logs, on devrait voir la tâche s'exécuter, ou rien du tout (le pod a échoué).
Si le Pod échoue, il faut le relancer à la main. Le processus étant laborieux, nous allons créer un Job qui va gérer ça à notre place.
Un Job est un controlleur de Pod, au même titre qu'un ReplicaSet ou qu'un Deployment. La différence étant qu'un
Job ne peut opérer que sur des Pods dont l'attribut restartPolicy vaut Never ou OnFailure
- Utiliser le modèle suivant pour créer le Job responsable de l'exécution de notre tâche
apiVersion: batch/v1
kind: Job
metadata:
name: sometask-job
spec:
template:
spec:
...
- Afficher la liste des Pods, selon les tirages aléatoires, on peut voir des résultats différents
pod/sometask-job-8dg9f 0/1 Completed 0 18s
pod/sometask-job-shqpd 0/1 Error 0 21s
Ici on voit qu'un premier Pod a échoué, suite à quoi un nouveau a démarré et s'est terminé avec succès.
Lancement de tâches parallèles
Prenons maintenant un cas hypotétique dans lequel nous avons besoin d'exécuter 10 fois notre tâche. Une solution triviale serait de créer 10 Pods à la main, et de recréer ceux qui échouent. Nous n'allons bien évidemment pas faire ça, mais utiliser un Job pour s'occuper de cette logique.
- Ajouter l'attribut suivant au bon endroit dans la définition du Job, et le redéployer.
completions: 10
- Afficher la liste des Pods
pod/sometask-job-56bkx 0/1 Completed 0 28s
pod/sometask-job-6xwss 0/1 Completed 0 1m
pod/sometask-job-jmhf4 0/1 Completed 0 14s
pod/sometask-job-kf84r 0/1 Completed 0 57s
pod/sometask-job-rfb7b 0/1 Error 0 31s
pod/sometask-job-s2l72 0/1 Completed 0 1m
pod/sometask-job-sspzb 0/1 Completed 0 1m
pod/sometask-job-vbx2k 0/1 ContainerCreating 0 2s
pod/sometask-job-zbb9n 0/1 Completed 0 45s
On voit que nos Pods sont créés un par un, si un Pod échoue, un autre prend sa place. L'exécution va prendre au minimum 100 secondes, aussi pourrions nous vouloir raccourcir cette durée en exécutant un certain nombre des tâches en parallèle.
Une solution pourrait être de créer un ReplicaSet avec l'attribut replicas=10, le problème étant que nos 10 tâches vont démarrer
en même temps. Pour peu que la tâche soit consommatrice en ressources, cette solution n'est pas viable.
Heureusement notre Job va nous permettre de résoudre cette contrainte très facilement.
- Ajouter l'attribut suivant au Job, et le redéployer.
parallelism: 5
- Afficher la liste des Pod immédiatement après
NAME READY STATUS RESTARTS AGE
sometask-job-6plgz 0/1 ContainerCreating 0 1s
sometask-job-7d4z2 0/1 ContainerCreating 0 1s
sometask-job-9xrlv 0/1 ContainerCreating 0 1s
sometask-job-kwp4s 0/1 ContainerCreating 0 1s
sometask-job-t7nrp 0/1 ContainerCreating 0 1s
Les Pods sont maintenant créés 5 par 5, nous avons donc effectivement divisé le temps total d'exécution par 5.
Tâches récurrentes avec les CronJobs
Là où les Jobs permettent le lancement de tâches à exécution unique, les CronJobs vont nous permettre d'introduire de la récurrence dans leur lancement. Il s'agit d'une version cloud native des crontabs.
- Utiliser le modèle ci-dessous pour créer un CronJob déployant notre Job chaque minute.
apiVersion: batch/v1
kind: CronJob
metadata:
name: sometask-cronjob
spec:
schedule: " ... "
jobTemplate:
...
- Appliquer le manifeste et afficher le CronJob ainsi créé
Au bout d'une minute, la première instance de Job va être créée, et avec elle les 10 instances de Pod, 5 par 5. Actuellement, l'exécution du Job prend moins d'une minute. Que se passerait-il si elle durait plus longtemps ?
- Modifier le temps d'attente dans la commande du Pod, le passer à
sleep 60. - Appliquer les changements et attendre.
Le Job va démarrer une première fois et tous ses Pods n'auront pas terminé au boût d'une minute.
- Que se passe-t-il lorsque le CronJob atteint son délai d'exécution alors que le Job précédent n'a pas terminé ?
Ce comportement peut être conrollé via l'attribut concurrencyPolicy
- Afficher la valeur actuelle de cet attribut.
kubectl get cronjob sometask-cronjob -o yaml | grep concurrencyPolicy
-
Modifier l'attribut
concurrencyPolicy, essayer les différentes valeurs possibles : -
Allow
- Forbid
- Replace