La plateforme GitOps sur kind
Le correctif « en urgence » fait par kubectl edit chez Ferrand Médical a vécu neuf jours, puis le déploiement suivant l'a effacé sans que personne le remarque. Le problème n'était pas le collègue : c'était l'absence d'une source unique de ce qui doit tourner. Avec GitOps, cette source est un dépôt Git, et un contrôleur dans le cluster compare en continu le cluster à ce dépôt. Cette leçon monte la plateforme complète sur un cluster kind de trois nœuds et déclare l'application.
Le mécanisme
Les quatre principes d'OpenGitOps se résument en une boucle : l'état voulu est déclaratif, versionné dans Git, tiré par un agent du cluster (pas poussé par la CI), et réconcilié en continu.
flowchart LR
subgraph POSTE["Poste de l'équipe"]
SRC["gitops-source/ (versionné avec le projet)"] -->|"make gitops-init"| REPO["gitops-repo/ (dépôt git local)"]
end
REPO -->|"make gitops-push (git://, port-forward)"| GIT["git-server (namespace git)"]
subgraph KIND["kind lab-k8s-gitops"]
GIT -->|"git fetch toutes les 3 min ou sur refresh"| REPOSRV["argocd-repo-server : helm template"]
REPOSRV --> CTRL["argocd-application-controller : diff + apply"]
CTRL -->|"Rollout, Service, Ingress, NetworkPolicy, ServiceMonitor..."| NS["namespace assistant"]
CTRL -.->|"surveille les objets : toute dérive est vue"| NS
end
Trois points comptent pour la suite :
- Argo CD ne lance pas
helm install. Le repo-server rend le chart (helm template) et le contrôleur applique les manifestes. Il n'y a donc pas de release Helm dans le cluster :helm list -n assistantest vide, et l'historique est celui de Git. - Le dépôt GitOps n'est pas le dépôt de l'application. Le code et l'image vivent ailleurs ; le dépôt GitOps ne contient que le chart et les valeurs par environnement. La CI propose une nouvelle étiquette d'image par pull request (github-actions.yml), Argo CD déploie.
- Le serveur Git est dans le cluster. Dans le lab, un
git daemonminimal (git-server.yaml) joue le rôle de GitHub ou GitLab. Argo CD ne sait pas lire un dossier de votre poste : il lui faut une URL.
La mise en œuvre
Le dépôt GitOps : versionné en clair, transformé en dépôt git par make
Le dépôt de la plateforme de formation ne peut pas contenir un autre dépôt git (le dossier .git imbriqué serait ignoré). Le contenu du dépôt GitOps existe donc en fichiers ordinaires dans gitops-source/, et une cible le transforme en dépôt :
gitops-init: ## Crée le dépôt git gitops-repo/ à partir de gitops-source/ (sans effet s'il existe)
@test -d gitops-repo/.git || { mkdir -p gitops-repo && cp -r gitops-source/. gitops-repo/ \
&& git init -q -b main gitops-repo && $(GIT) add -A && $(GIT) commit -qm "Déploiement initial : assistant 0.2.0" \
&& echo "Dépôt gitops-repo/ créé"; }gitops-source/
chart/inference/ le chart (Rollout, Service, Ingress, NetworkPolicy, PDB, ServiceMonitor, PrometheusRule...)
environments/kind/values.yaml ce que l'on modifie pour déployer dans le lab
environments/gke/values.yaml Vertex AI, Workload Identity, pas d'Ingress (leçon 6)Ensuite, on ne modifie plus gitops-source/ mais gitops-repo/ : c'est votre historique de déploiement. make clean ne l'efface jamais ; pour repartir de l'état initial, rm -rf gitops-repo && make gitops-init.
L'application déclarée
spec:
source:
repoURL: git://git-server.git.svc.cluster.local/assistant.git
targetRevision: main
path: chart/inference
helm:
releaseName: inference
valueFiles: [../../environments/kind/values.yaml]
syncPolicy:
automated: { prune: true, selfHeal: true }prune supprime du cluster ce qui a disparu de Git ; selfHeal réapplique Git quand quelqu'un modifie le cluster à la main. Sans les deux, Argo CD ne fait qu'afficher des écarts.
releaseName: inference n'est pas cosmétique. Sans lui, Argo CD prend le nom de l'application (assistant) et toutes les ressources s'appellent assistant-inference. C'est arrivé au premier essai de ce lab : make e2e cherchait le Rollout inference et échouait. Sur GKE, le même décalage casserait Workload Identity, dont le principal IAM désigne le ServiceAccount assistant/inference par son nom (leçon 5).
Le mode simulé
Le cluster ne contient aucune clé. L'environnement kind déploie LLM_BACKEND: fake : l'API répond « [fake] Réponse simulée à : ... » sans appel réseau. Le chart référence quand même le Secret inference-gemini en optional: true, ce qui permet de passer en Gemini réel par un seul commit après make secret. L'appel réel au modèle se fait hors cluster, par Vertex AI (leçon 5). Ce découpage a un avantage qui dépasse le lab : les démonstrations canary des leçons 3 et 4 envoient plus de 160 000 requêtes chacune, et aucune ne coûte un appel modèle.
Monter la plateforme
make setup && make test
make up # cluster, Ingress NGINX, metrics-server, kube-prometheus-stack, Argo Rollouts, Argo CD, git-server, image, application
make status
make e2eL'ordre de make up suit les dépendances : kube-prometheus-stack avant l'application (le chart crée un ServiceMonitor, dont la CRD vient de kube-prometheus-stack), Argo Rollouts avant l'application (le chart crée un Rollout), le serveur Git avant le premier push.
Ce qui casse en production
| Symptôme | Cause | Parade |
|---|---|---|
Application Unknown, condition ComparisonError ... remote repository is empty | le serveur Git a perdu le dépôt (ici : pod redémarré, volume emptyDir) | dépôt Git hébergé et sauvegardé ; dans le lab, make gitops-push le repeuple |
make app échoue : no matches for kind "Rollout" ou "ServiceMonitor" | CRD absente au moment de la synchronisation | installer les opérateurs avant l'application, ou vagues de synchronisation (sync-wave) |
Ressources nommées assistant-inference au lieu de inference | nom de release dérivé du nom de l'application | helm.releaseName explicite |
| Argo CD ne voit pas un commit pendant 3 minutes | intervalle de lecture par défaut | webhook du forge Git vers Argo CD ; dans le lab, annotation refresh posée par make gitops-push |
La première ligne a été reproduite : le pod git-server supprimé, Argo CD passe en Unknown mais ne touche à rien dans le namespace assistant, les trois pods continuent de servir. Une source illisible gèle les déploiements, elle ne détruit pas ce qui tourne.
La règle de décision
- Argo CD dès que plus d'une personne déploie, ou dès qu'un auditeur demande « qui a changé quoi, quand ». Pour un service unique déployé par une seule CI,
helm upgrade --atomicdans la CI suffit. - Un dépôt GitOps séparé du code : les droits d'écriture ne sont pas les mêmes, et un commit applicatif ne doit pas déclencher de déploiement.
automatedavecpruneetselfHealen production ; sansselfHeal, la dérive s'accumule et le prochain déploiement réserve des surprises.- À surveiller : l'état
Synced/Healthyde chaque application, et le délai entre commit et synchronisation.
Pratique
Le serveur Git de l'entreprise est en maintenance pendant une heure. Que fait Argo CD aux applications en automated + prune ?
Une source illisible n'est pas une source vide : Argo CD ne peut pas calculer l'état voulu, il ne prune donc rien. Mesuré dans ce lab : ComparisonError, trois pods toujours Running. Il n'y a pas de release Helm à laquelle revenir (Argo CD rend le chart sans helm install), et il ne redémarre jamais de pods de lui-même.
Un collègue propose de mettre le dépôt GitOps dans le dépôt du code, sous un dossier deploy/, pour simplifier. Quel argument l'emporte contre ?
Argo CD lit très bien un sous-dossier (path). Le vrai coût est organisationnel : qui peut déployer en production devient qui peut pousser du code, et l'historique de déploiement est noyé dans l'historique du code. Pour une petite équipe, le dépôt unique reste acceptable si la branche et les droits sont protégés.
Exercice
Ajoutez une deuxième application Argo CD, assistant-preprod, qui déploie le même chart dans le namespace assistant-preprod avec replicaCount: 1. Quels fichiers modifiez-vous, et lequel ne doit surtout pas changer ?
Voir la solution
Dans gitops-repo/ : un fichier environments/preprod/values.yaml (copie de kind avec replicaCount: 1), commit, make gitops-push. Hors du dépôt : une deuxième Application (copie de argocd/application.yaml) avec metadata.name: assistant-preprod, destination.namespace: assistant-preprod, valueFiles: [../../environments/preprod/values.yaml], appliquée par kubectl apply.
Le chart ne change pas : un environnement est un fichier de valeurs, jamais une copie du chart. Gardez aussi releaseName: inference : la NetworkPolicy, le ServiceMonitor et l'analyse canary sélectionnent les pods par les étiquettes app.kubernetes.io/instance. Point d'attention : l'Ingress des deux environnements déclare le même hôte ; changez ingress.host dans les valeurs preprod, sinon NGINX répartit entre les deux.
Résultat obtenu
Exécution du 2026-10-16 (Docker 33 Go, kind 0.33, Kubernetes 1.37) :
$ time make up
...
kube-prometheus-stack 92.9.0 prêt
Argo Rollouts prêt
Argo CD prêt
Image lab-k8s-gitops/inference:0.2.0 chargée
Pas de clé Gemini : le lab tourne en mode simulé (LLM_BACKEND=fake).
Poussé : 68185e5 Déploiement initial : assistant 0.2.0
Application assistant déclarée
make up 4:23,20 total
$ make status
APP SYNC SANTE REVISION
assistant Synced Healthy 68185e50985c377d67ca76acfc13be6979b18b1e
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
rollout.argoproj.io/inference 3 3 3 3 44s
$ make e2e
[OK] Argo CD : application synchronisée et saine : Synced/Healthy, commit 68185e5
[OK] Argo Rollouts : Rollout sain : Healthy, 3 pods prêts
[OK] aucun Secret dans le dépôt Git
[OK] réponse via l'Ingress : HTTP 200
backend=fake modèle=fake version=0.2.0 pod=inference-5f6757f64d-wl2cp latence=0 ms
[fake] Réponse simulée à : Comment signaler un e-mail d'hameçonnage ?
[OK] Prometheus collecte les métriques du service : 1 requêtes comptées
e2e : tout est vertLe premier make e2e avait échoué sur la dernière étape (« aucune série ») : il interrogeait Prometheus moins de 15 s après la requête, avant la première collecte. Le script attend désormais jusqu'à 60 s.
Empreinte mesurée (docker stats, plateforme au repos) : 1,92 Gio pour le nœud control-plane (Argo CD, Prometheus, Ingress), 1,18 Gio et 1,0 Gio pour les deux workers, soit 4,1 Gio. Pods : 7 dans argocd, 4 dans monitoring, 1 dans argo-rollouts, 1 dans git, 3 pour l'application.
Perte du serveur Git, puis repeuplement :
$ kubectl -n git delete pod -l app=git-server
Unknown Healthy
ComparisonError Failed to load target state: failed to generate manifest for source 1 of 1: rpc error: code = Unknown desc = failed to list refs: remote repository is empty
3 <- pods de l'application toujours en service
$ make gitops-push
Poussé : 9e8a014 Alertes d'astreinte : 5xx > 5 %, cible absente
assistant Synced Healthy 9e8a014a01aac01107fe68b629dc0dd92a367ca1Je sais faire
- Expliquer la boucle GitOps : déclaratif, versionné, tiré, réconcilié
- Créer le dépôt GitOps à partir de
gitops-source/avecmake gitops-init - Déclarer une
Applicationavecprune,selfHealet unreleaseNamefixe - Monter la plateforme avec
make upet la vérifier avecmake statusetmake e2e - Diagnostiquer une
ComparisonErrorsans paniquer : ce qui tourne continue de tourner