Aller au contenu
EN
Ouvert aux opportunités & missions

// Projet

Une plateforme Kubernetes de A à Z en GitOps

Un cluster Kubernetes où rien ne s'installe à la main : tout vient de Git, se reconstruit en 11 minutes et se teste à chaque pull request. Choix, chiffres et pièges.

Publié
Lecture
11 min
Sujets
  • kubernetes
  • argocd
  • gitops
  • observabilité
Code
github.com/Mak5ens/llmops-platform ↗
Sommaire
  1. Git comme seule source de vérité
  2. ArgoCD plutôt que Flux
  3. Un composant, un dossier
  4. Ce que la plateforme fait tourner
  5. Du commit au cluster, sans copier un tag
  6. Pourquoi pas de cluster dans le cloud
  7. Les pièges
  8. Les chiffres
  9. Les limites
  10. La suite

Dans le premier article, la gateway LLM tournait sur mon ordinateur avec Docker Compose. Un fichier, une commande, et les équipes de l’entreprise imaginaire pouvaient appeler leurs modèles sans laisser fuir de données personnelles. Pour une démonstration, c’est parfait. Pour une entreprise, beaucoup moins : personne ne sait ce qui tourne vraiment, une modification faite à la main un vendredi soir est oubliée le lundi, et le jour où la machine tombe, il faut se souvenir de tout ce qu’on avait installé.

Ce deuxième bloc de la plateforme répond à ce problème. J’ai construit un cluster Kubernetes où rien ne s’installe à la main. Tout ce qui y tourne est décrit dans un dépôt Git, et un agent placé dans le cluster compare en permanence ce qui tourne avec ce qui est écrit. Si quelqu’un modifie quelque chose directement sur le cluster, l’agent le remet comme dans Git. Si on supprime tout, il reconstruit tout.

Le cluster tourne sur ma machine et, à chaque pull request, sur un runner GitHub où il est recréé de zéro. Chaque chiffre de cet article vient de ces exécutions, et se reproduit avec les commandes du repo.

Cluster créé

26s

Trois nœuds Kubernetes dans Docker, sur un runner GitHub de 4 cœurs et 16 Go.

Plateforme complète

11min

Le temps qu'ArgoCD installe et déploie les 16 composants depuis Git, tous synchronisés et en bonne santé.

Du merge au cluster

87s

Pour la release 0.2.1 de la gateway, entre la fusion de la PR et son déploiement.

Commande à la main

0

Après l'installation d'ArgoCD, plus rien ne s'applique sur le cluster sans passer par Git.

Git comme seule source de vérité

On appelle cette façon de faire le GitOps. L’idée tient en une phrase : l’état voulu du cluster est écrit dans Git, et un programme s’occupe de faire correspondre la réalité à ce qui est écrit.

Une comparaison aide à voir la différence. Sans GitOps, administrer un cluster ressemble à une cuisine où chaque cuisinier ajoute un peu de sel à sa façon. Le plat finit par être bon, mais personne ne saurait le refaire. Avec le GitOps, la recette est écrite, et un chef relit la cuisine toutes les minutes. Un ingrédient manque ? Il l’ajoute. Quelqu’un a changé la cuisson sans modifier la recette ? Il remet la cuisson prévue. Pour changer le plat, on change la recette, et cette modification passe par une relecture, comme n’importe quel code.

Concrètement, une modification de la plateforme est une pull request. La CI la teste sur un cluster tout neuf, quelqu’un la relit, puis on la fusionne. L’agent voit le nouveau commit et l’applique. Le dépôt garde l’historique de chaque changement, avec son auteur et sa raison, et revenir en arrière revient à annuler un commit.

ArgoCD plutôt que Flux

Deux outils dominent ce rôle d’agent, ArgoCD et Flux. Tous deux sont des projets matures de la CNCF, la fondation qui héberge Kubernetes, et font très bien le travail de base. Ce qui les sépare, c’est ce qu’on voit.

ADR-008 Statut · Acceptée

Décision : ArgoCD plutôt que Flux

ArgoCD a une interface web qui montre chaque composant, son état de santé et ses écarts avec Git. Pour une plateforme qui sert plusieurs équipes, dont toutes ne lisent pas du YAML, c’est ce qu’on ouvre en premier quand quelque chose ne va pas. Flux n’a plus d’interface à lui depuis la fermeture de Weaveworks, l’entreprise qui le portait, en février 2024. ArgoCD est plus lourd, avec 7 pods et environ 730 Mo de mémoire sur mon cluster, mais c’est peu à côté des 13 Go de la plateforme qu’il déploie.

Lire l’ADR ↗

Un composant, un dossier

Toute la plateforme tient dans un dossier par composant. Chacun décrit ce qu’il installe avec Kustomize, l’outil standard de Kubernetes pour assembler des fichiers de configuration, et une couche par environnement ajoute ce qui est propre à cet environnement, comme l’autorité de certification locale.

llmops-platform
apps/  # une Application ArgoCD par composant
values.yaml  # la liste des composants et leur ordre
platform/
argocd/  # ArgoCD se gère lui-même
envoy-gateway/
cert-manager/
external-secrets/
cloudnative-pg/
monitoring/
llm-gateway/  # la gateway du premier bloc
job-queue/
scripts/  # les vérifications de just test
docs/adr/  # une fiche par décision

Les composants ne peuvent pas démarrer dans n’importe quel ordre. Un cluster PostgreSQL ne se crée pas tant que l’opérateur qui sait le gérer n’est pas là. ArgoCD les déploie donc par vagues : il attend que chaque vague soit en bonne santé avant de lancer la suivante. Une seule Application, la racine, crée toutes les autres. On l’appelle le motif app of apps. Et ArgoCD fait partie de la liste : il se met à jour lui-même quand son dossier change.

L'ordre de déploiement

Ce que la plateforme fait tourner

La liste des composants ressemble à celle d’une plateforme de production, en plus petit. Je ne les détaille pas tous ici, mais chacun a une histoire.

L’entrée HTTP passe par Envoy Gateway, et jamais par un Ingress. Pendant des années, la réponse par défaut était ingress-nginx. Le 11 novembre 2025, l’équipe réseau de Kubernetes a annoncé son retrait : plus aucune correction de sécurité après mars 2026. Démarrer une plateforme en 2026 avec ingress-nginx, c’était choisir un composant déjà abandonné. J’ai pris la Gateway API, son successeur officiel, implémentée par Envoy Gateway, et un hook refuse tout Ingress avant même le commit.

Aucun secret n’est dans Git. Les clés des équipes, les mots de passe des bases et les clés de Langfuse sont générés au démarrage du cluster, puis distribués aux applications par External Secrets. Le jour où un vrai gestionnaire de secrets arrive, seule la configuration de cette source change, pas les applications. gitleaks vérifie tout l’historique du dépôt à chaque pull request.

PostgreSQL est géré par un opérateur, CloudNativePG. Chaque base a un serveur principal et une réplique sur un autre nœud, et ses journaux de transactions partent en continu vers un stockage S3. La CI tue le serveur principal à chaque exécution et vérifie que la réplique prend le relais sans perdre la dernière écriture, en 22 secondes sur le runner. Elle restaure aussi une sauvegarde dans un cluster tout neuf, à partir du stockage seul.

L’observabilité relie les métriques, les traces et les logs. Sur le dashboard de la gateway, un point sur la courbe de latence correspond à une vraie requête. Un clic ouvre sa trace, un second ses logs. La CI refait ces deux clics par les API à chaque exécution, pour vérifier que les liens tiennent.

Deux SLO protègent la gateway, sa disponibilité et sa latence, avec des alertes qui préviennent quand le budget d’erreurs fond trop vite et un runbook pour chacune. La CI arrête le serveur d’un modèle et vérifie que l’alerte se déclenche bien, ce qui prend 107 secondes sur le runner.

Une file de jobs accueille les traitements longs. Un job qui échoue revient après 5 secondes, puis 10, et part dans une file d’attente à part après son troisième échec. Ce comportement est entièrement configuré dans RabbitMQ, sans une ligne de code, et testé à chaque pull request.

Du commit au cluster, sans copier un tag

Avoir tout dans Git ne suffit pas si chaque nouvelle version d’une application demande de recopier un numéro à la main. La gateway vit dans son propre dépôt, et je voulais qu’un correctif fusionné là-bas arrive sur le cluster sans que personne ne touche au dépôt de la plateforme.

D'un correctif de la gateway au cluster

Le maillon central est la CI de la plateforme. Avant de laisser passer une nouvelle version, elle vérifie que les images sont bien signées par le workflow de build du projet, avec la liste de leurs composants (le SBOM), que les fichiers de configuration correspondent à cette version, puis elle crée un cluster, y déploie la plateforme et lance les 46 tests d’intégration de la gateway, ceux de la version déployée.

La signature n’est pas une précaution théorique. Le 19 mars 2026, 76 des 77 tags de trivy-action, une action GitHub de scan de sécurité utilisée partout, ont été déplacés vers du code qui volait les secrets des pipelines. Depuis, chaque action de mes workflows est épinglée sur l’empreinte exacte d’un commit, et la plateforme refuse une image qu’elle ne sait pas rattacher à son build. Le premier scan a d’ailleurs trouvé de quoi s’occuper : les deux images de la gateway avaient 10 et 9 vulnérabilités critiques corrigeables, dont une dans LiteLLM lui-même qui permettait de contourner l’authentification.

Voici le vrai parcours de la version 0.2.1, un petit correctif de tests.

  1. 20:33

    Correctif fusionné dans llmops-gateway

  2. 20:37

    PR de release fusionnée, tag v0.2.1

    release-please calcule la version à partir des messages de commit.

  3. 20:40

    Images 0.2.1 publiées, signées, avec leur SBOM

  4. 20:51

    Renovate ouvre la PR sur llmops-platform

  5. +17 min

    CI verte

    Signature, configuration, cluster complet et 46 tests de la gateway.

  6. 09:46

    Renovate fusionne la PR lui-même

    Le lendemain matin, à son passage suivant. Un correctif se fusionne sans relecture humaine, une fois toute la CI passée.

  7. 09:47

    ArgoCD déploie la 0.2.1

    87 secondes après la fusion.

Deux fusions à la main en tout, toutes deux dans le dépôt de la gateway : le correctif et sa release. Le seul temps mort est Renovate, hébergé par Mend, qui ne fusionne qu’à son passage suivant. La PR était verte en 17 minutes, et elle a attendu jusqu’au lendemain matin, quand j’ai relancé Renovate. Une version mineure, elle, attend une fusion humaine, et une version majeure une validation de plus.

Pourquoi pas de cluster dans le cloud

Le plan d’origine prévoyait de déployer la même plateforme sur Kapsule, le Kubernetes managé de Scaleway, avec Terraform. J’y ai renoncé en cours de route, et je préfère dire pourquoi.

Un cluster managé m’aurait surtout appris à cliquer, ou à écrire du Terraform, pour obtenir trois nœuds. La partie difficile d’une plateforme, c’est ce qui tourne dessus et la façon dont on le prouve. Or le cluster local fait déjà tourner exactement les mêmes manifestes, et il est recréé de zéro à chaque pull request. Le script d’installation a aussi été vérifié sur minikube, et rien n’y est propre à k3d. Enfin, j’opère déjà un cluster de production dans mon travail. Payer un cluster pour ce portfolio n’aurait rien montré de plus.

Scaleway reste dans le projet, pour ce qu’un ordinateur portable ne sait pas faire : servir de vrais modèles. Ses API d’inférence, hébergées à Paris et facturées au token, remplaceront bientôt les petits modèles Ollama derrière les mêmes noms d’usage. Les données personnelles sortiront alors du cluster, ce qui donne au masquage du premier article tout son sens.

Les pièges

C’est la partie que je relirais en premier si je reprenais un projet comme celui-ci. Aucun de ces problèmes n’apparaît dans la documentation des outils.

Envoy coupe une requête après 15 secondes par défaut. Quand le grand modèle de la gateway ne répond pas, elle bascule sur le petit, et l’ensemble peut prendre jusqu’à 20 secondes. Sur le cluster, ces requêtes échouaient alors qu’elles passaient avec Docker Compose. La route vers LiteLLM a maintenant un délai de 90 secondes.

Kustomize oubliait un namespace, et LiteLLM ne redémarrait plus. Kustomize ajoute un suffixe à chaque ConfigMap qu’il génère, une empreinte de son contenu, pour qu’une modification de la configuration redémarre les pods qui l’utilisent. Mais il ne touche pas aux ressources rendues depuis un chart Helm quand elles n’ont pas de namespace. Le Deployment de LiteLLM pointait donc vers le nom de la ConfigMap sans son empreinte, et une nouvelle configuration ne le redémarrait jamais. Un patch qui fixe le namespace a réglé le problème.

k3s installe déjà la Gateway API, dans une version qu’on ne peut pas désactiver, et elle entrait en conflit avec celle d’Envoy Gateway. Le cluster local demande donc à k3s de sauter ce fichier au démarrage.

ArgoCD voyait des écarts qui n’existaient pas. L’API de Kubernetes complète les routes HTTP avec des valeurs par défaut, et ArgoCD les prenait pour des modifications faites à la main. Lui faire comparer avec une simulation côté serveur a fait disparaître ces faux écarts.

Les tests de la gateway ont eu deux échecs mystérieux. Le test qui vérifie qu’aucune donnée personnelle n’atteint le modèle lit les logs détaillés de LiteLLM. Sur le cluster, il ne trouvait d’abord que 18 requêtes sur 100. Kubernetes découpe les logs d’un conteneur tous les 10 Mo, et kubectl logs ne lit que le dernier morceau. Une fois la taille relevée, il n’en trouvait plus aucune : ArgoCD remettait LiteLLM dans sa configuration normale au milieu du test, parce qu’une synchronisation était déjà en cours quand le test le passait en mode détaillé. Le test suspend maintenant la réconciliation d’ArgoCD, laisse se terminer celle qui était en cours, et vérifie que le mode détaillé est toujours actif avant de conclure. C’est le revers d’un agent qui répare tout : il répare aussi ce qu’on a modifié exprès.

Les chiffres

Ces mesures viennent de la dernière exécution de la CI sur main, sur un runner GitHub standard de 4 cœurs et 16 Go, sauf la reconstruction de la file de jobs, mesurée sur ma machine.

Ce que mesure chaque exécution de la CI
Étape Durée (s)
Créer le cluster (k3d, 3 nœuds) 26
Installer ArgoCD et déployer les 16 composants depuis Git 664
just test : santé, self-heal, HTTPS, PostgreSQL, file de jobs, traces, SLO 263
46 tests d'intégration de la gateway sur le cluster 326
Source : .github/workflows/ci.yml · job cluster
Les incidents que la CI provoque, et le temps de réaction
Incident provoqué Durée (s)
Serveur PostgreSQL principal tué, la réplique prend le relais 22
Modèle arrêté, alerte SLO déclenchée 107
File de jobs supprimée avec son volume, recréée depuis Git (local) 109
Source : scripts/postgres-check.sh · queue-check.sh · slo-check.sh

Le cluster local ne coûte rien, mais il a un prix en mémoire. La plateforme occupe entre 13 et 14 Go, dont 3 pour Presidio et LiteLLM, 2 pour Langfuse et 1,5 pour l’observabilité. Le runner de la CI en a 16 : le pic de la dernière exécution était à 14,1 Go. C’est la contrainte qui pèse le plus sur la suite.

Terminal
just bootstrap k3d-llmops local main
== ArgoCD Applications
33 s: argocd synced and healthy
85 s: external-secrets synced and healthy
176 s: cert-manager synced and healthy
209 s: envoy-gateway synced and healthy
321 s: job-queue synced and healthy
332 s: cloudnative-pg synced and healthy
381 s: monitoring synced and healthy
453 s: langfuse synced and healthy
631 s: llm-gateway synced and healthy
631 s: root synced and healthy
Every Application is synced and healthy

Un extrait de la sortie de la CI. Chaque composant est signalé quand il devient sain, et la gateway LLM ferme la marche : elle est dans la dernière vague, après Langfuse, où elle crée les projets des équipes.

Les limites

  • Une seule machine. Le cluster a trois nœuds, mais ils tournent tous sur le même ordinateur. La bascule de PostgreSQL d’un nœud à l’autre est réelle, la panne d’une vraie machine ne l’est pas.
  • La mémoire est comptée. Avec 1,8 Go de marge sur le runner, chaque nouveau composant se paie. Le prochain allègement sera de remplacer, en CI seulement, le second modèle Ollama par un plus petit.
  • ArgoCD interroge GitHub au lieu d’être prévenu. C’est ce qui explique la minute et demie entre une fusion et le déploiement. Un webhook la ferait disparaître, mais il faudrait que GitHub puisse joindre le cluster.
  • Les signatures sont vérifiées avant la fusion, pas à l’entrée du cluster. Une commande kubectl apply lancée à la main pourrait déployer une image non signée. Un contrôle d’admission, avec Kyverno par exemple, fermerait cette porte.

La suite

Le troisième jalon commence. La file de jobs est en place, il reste les workers qui la consomment et KEDA pour les faire passer de zéro à plusieurs selon la longueur de la file, puis les vrais modèles sur Scaleway, avec le coût de chaque job mesuré dans le même tableau de bord que la file et la latence. Ce sera l’objet du prochain article.

Le code, les décisions d’architecture et les scripts de test sont sur GitHub. Deux commandes suffisent pour tout reconstruire : just up, puis just test.