Featured image of post Kubernetes 1.29 - sidecar container - à quoi ça peut bien servir ?

Kubernetes 1.29 - sidecar container - à quoi ça peut bien servir ?

Ecrit par ~ zwindler ~

Introduction

J’ai un copain qui doit lancer un traitement périodique. Ce traitement est réalisé par un bout de code qui tourne dans un container (sur un cluster GKE), et qui a besoin de requêter une base de donnée Cloud SQL.

Pour ce genre de choses, Google Cloud fourni une image à mettre à côté de son application (sidecar pattern) qui sert de passe plat (proxy) pour que l’application se connecte à l’instance Cloud SQL chez Google cloud.

Pas de bol, il se trouve que cette image met parfois un peu de temps à démarrer, et son absence fait crasher l’application de mon ami. On se retrouve avec une race condition et il m’a demandé si j’avais des idées de solutions pour régler ce problème.

Parmis d’autres propositions (que je détaille dans le tout dernier paragraphe), je lui propose d’essayer une toute nouvelle fonctionnalité qui est en béta dans Kubernetes 1.29 que je n’avais pas encore testé moi même : les sidecar containers !

Fun fact: en cherchant de la documentation sur cloud sql sidecar, je suis tombé sur cet article d’une personne qui a exactement le même problème que mon ami.

Une fois n’est pas coutume, pour que vous puissiez bien comprendre le problème et sa résolution, je vous propose de faire cette découverte de feature avec une démo.

La totalité du code et les instructions pour le refaire en anglais sont disponibles sur le dépôt github github.com/zwindler/sidecar-container-example.

L’idée est la suivante : on va simuler le problème de mon poto avec deux images Docker créées pour l’occasion :

  • zwindler/slow-sidecar un helloworld basique en V lang (vhelloworld) qui fait un sleep de 5 secondes avant d’écouter le port 8081.
  • zwindler/sidecar-user un script bash qui curl et exit 1 si le curl échoue.

Prérequis

Comme dit précédemment, la fonctionnalité a été introduite dans Kubernetes 1.28 en tant que fonctionnalité alpha. Si vous utilisez cette version et souhaitez la tester, vous devez activer spécifiquement le flag de fonctionnalité.

À partir de Kubernetes 1.29, cette fonctionnalité est passée en bêta et devrait être activée par défaut sur votre cluster.

Sans conteneurs sidecar

D’abord, on va essayer de déployer le CronJob de manière naïve sur un cluster :

$ cat 1-cronjob-without-sidecar-container.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: sidecar-cronjob
spec:
  schedule: "* * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: sidecar-user
            image: zwindler/sidecar-user
          - name: slow-sidecar
            image: zwindler/slow-sidecar
            ports:
            - containerPort: 8081
          restartPolicy: Never

$ kubectl apply -f 1-cronjob-without-sidecar-container.yaml

Cela devrait échouer car le conteneur “slow sidecar” ne sera pas prêt lorsque le conteneur “sidecar user” essaiera de faire un curl.

$ kubectl get pods
NAME                             READY   STATUS   RESTARTS   AGE
sidecar-cronjob-28689938-5n5x9   1/2     Error    0          9s

$ kubectl describe pods sidecar-cronjob-28689938-5n5x9
[...]
Containers:
  slow-sidecar:
[...]
    State:          Running
      Started:      Fri, 19 Jul 2024 15:38:03 +0200
    Ready:          True
[...]
  sidecar-user:
[...]
    State:          Terminated
      Reason:       Error
      Exit Code:    1
      Started:      Fri, 19 Jul 2024 15:38:05 +0200
      Finished:     Fri, 19 Jul 2024 15:38:05 +0200
    Ready:          False
    Restart Count:  0
[...]

slow-sidecar fonctionne bien mais notre requête sidecar-user a échoué car le sidecar était trop lent à démarrer.

Petit nettoyage avant de recommencer :

kubectl delete cronjob sidecar-cronjob 

Utiliser un conteneur init n’est pas une option non plus car le conteneur init ne se terminera jamais (ce n’est pas son but) et le conteneur “sidecar user” attendra éternellement son tour. Si vous voulez essayer, convertissez simplement slow-sidecar en initContainer.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: sidecar-cronjob
spec:
  schedule: "* * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: sidecar-user
            image: zwindler/sidecar-user
+         initContainers:
          - name: slow-sidecar
            image: zwindler/slow-sidecar
            ports:
            - containerPort: 8081
          restartPolicy: Never

Et lancez-le

$ kubectl apply -f 2-cronjob-with-init-container.yaml

$ kubectl get pods
NAME                             READY   STATUS     RESTARTS   AGE
sidecar-cronjob-28689955-lzbnf   0/1     Init:0/1   0          27s

Et on reste boqué à cette étape jusqu’à la fin de teeeeeeemps.

Avec des conteneurs sidecar

Pour éviter ce type de race condition, mettons à jour le manifest en convertissant slow-sidecar en initContainer MAIS EN AJOUTANT également restartPolicy: Always dans la déclaration du container slow-sidecar.

Cette bidouille est la manière de dire à Kubernetes de lancer ce conteneur en tant qu’initContainer mais de ne PAS attendre qu’il se termine (ce qu’il ne fera jamais puisqu’il s’agit d’un serveur web écoutant sur 8081 jusqu’à la fin des temps) pour démarrer l’application principale.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: sidecar-cronjob
spec:
  schedule: "* * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: sidecar-user
            image: zwindler/sidecar-user
+         initContainers:
          - name: slow-sidecar
            image: zwindler/slow-sidecar
+           restartPolicy: Always
            ports:
            - containerPort: 8081
          restartPolicy: Never

Note: C’est la manière officielle de déclarer un conteneur sidecar dans Kubernetes. Je n’ai pas encore lu le KEP donc je ne peux pas dire pourquoi l’équipe de développement n’a pas introduit un nouveau mot-clé sidecarContainers dans le schéma de spécification du Pod et a réutilisé les initContainers déjà existants.

$ kubectl apply -f 3-cronjob-with-sidecar-container.yaml

Cette fois, le conteneur init devrait se lancer et ENSUITE seulement, l’application :

$ kubectl get pods -w
NAME                             READY   STATUS    RESTARTS   AGE
sidecar-cronjob-28689958-zrmhh   0/2     Pending   0          0s
sidecar-cronjob-28689958-zrmhh   0/2     Pending   0          0s
sidecar-cronjob-28689958-zrmhh   0/2     Init:0/1   0          0s
sidecar-cronjob-28689958-zrmhh   1/2     PodInitializing   0          2s
sidecar-cronjob-28689958-zrmhh   1/2     Error             0          3s

On voit que c’est mieux (sidecar-user démarre dans un second temps) mais dans cet exemple particulier, ça échoue encore…

Avec des conteneurs sidecar ET une startupProbe

Par défaut, le kubelet considère que le conteneur sidecar est up dès que le processus dans le conteneur est en cours d’exécution, puis si les autres initContainers ont tous terminé (ou s’il n’y en a pas), passe à la phase principale de démarrage des containers.

Malheureusement, dans notre cas, le conteneur sidecar est très lent (sleep 5), donc le fait que le processus soit en cours d’exécution n’est pas une indication de l’état du sidecar…

Nous devons ajouter une startupProbe pour que Kubernetes sache QUAND passer la phase d’init et démarrer la phase principale.

After a sidecar-style init container is running (the kubelet has set the started status for that init container to true), the kubelet then starts the next init container from the ordered .spec.initContainers list. That status either becomes true because there is a process running in the container and no startup probe defined, or as a result of its startupProbe succeeding.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: sidecar-cronjob
spec:
  schedule: "* * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: sidecar-user
            image: zwindler/sidecar-user
          initContainers:
          - name: slow-sidecar
            image: zwindler/slow-sidecar
            restartPolicy: Always
            ports:
            - containerPort: 8081
+           startupProbe:
+             httpGet:
+               path: /
+               port: 8081
+             initialDelaySeconds: 5
+             periodSeconds: 1
+             failureThreshold: 5
          restartPolicy: Never

Une dernière fois :

$ kubectl apply -f 4-cronjob-with-sidecar-container-and-startup-probe.yaml && kubectl get pods -w
cronjob.batch/sidecar-cronjob created
NAME                             READY   STATUS    RESTARTS   AGE
sidecar-cronjob-28689977-lt77c   0/2     Pending   0          0s
sidecar-cronjob-28689977-lt77c   0/2     Pending   0          0s
sidecar-cronjob-28689977-lt77c   0/2     Init:0/1   0          0s
sidecar-cronjob-28689977-lt77c   0/2     Init:0/1   0          1s
sidecar-cronjob-28689977-lt77c   0/2     PodInitializing   0          6s
sidecar-cronjob-28689977-lt77c   1/2     PodInitializing   0          6s
sidecar-cronjob-28689977-lt77c   1/2     Completed         0          7s

Hooray!

Bonus : si vous n’avez pas sidecarContainers activé

Si vous êtes toujours en Kubernetes 1.28 (ou pire) et que vous n’avez pas la possibilité d’activer les alpha featureFlags, il va falloir trouver une autre méthode.

Malheureusement, il est probable que la solution soit de modifier le code de votre application principale ou son image Docker. Vous pouvez :

  • ajouter une politique de retry dans l’application sidecar-user
  • ajouter un script dans l’application sidecar-user qui attend un peu (sleep) avant d’essayer de contacter le sidecar

La première est une bonne pratique lorsqu’on traite avec des microservices et vous devriez l’envisager de toute façon pour gérer les problèmes temporaires de connexion à la base de données.

La deuxième est une rustine sur une jambe de bois. Je le déconseille fortement car la vitesse de démarrage peut varier dans le sidecar et ajouter trop de délai dans l’application est également mauvais lorsqu’on doit gérer des incidents et des bugs en prod (induisant potentiellement d’autres problèmes).

Licensed under CC BY-SA 4.0

Vous aimez ce blog ou cet article ? Partagez-le avec vos amis !   Twitter Linkedin email Facebook

Vous pouvez également vous abonner à la mailing list des articles ici

L'intégralité du contenu appartenant à Denis Germain (alias zwindler) présent sur ce blog, incluant les textes, le code, les images, les schémas et les supports de talks de conf, sont distribués sous la licence CC BY-SA 4.0.

Les autres contenus (thème du blog, police de caractères, logos d'entreprises, articles invités...) restent soumis à leur propre licence ou à défaut, au droit d'auteur. Plus d'informations dans les Mentions Légales

Généré avec Hugo
Thème Stack conçu par Jimmy