GitOps : les principes et le modèle tiré
Pourquoi
Le cours GitHub Actions a montré deux façons de déployer depuis un pipeline. Dans la première, le pipeline pousse : il détient les identifiants du cluster et exécute kubectl apply ou helm upgrade. Dans la seconde, il se contente d'écrire dans Git ce qui doit tourner, et un agent installé dans le cluster tire ce changement et l'applique. C'est la seconde que suivent les applications de Lyneko, et ce n'était pas le cas au départ.
Les premières applications de Lyneko se déployaient depuis GitHub Actions avec helm upgrade --install --set image.tag=<sha>. Cela fonctionnait, avec trois défauts qui sont ceux de tout déploiement poussé :
- Le pipeline détenait un accès complet au cluster (le fichier
kubeconfigdans un secret du dépôt). Toute personne capable de modifier le workflow pouvait faire n'importe quoi au cluster. - Git ne disait pas ce qui tournait. L'étiquette déployée n'existait que dans la commande
--setdu dernier run : le fichier de valeurs du dépôt indiquaitlatest, et le cluster autre chose. Personne ne pouvait répondre à « quelle version est en production ? » sans interroger le cluster. - Les changements faits à la main n'étaient vus par personne. Un
kubectl editpendant un incident restait en place jusqu'au déploiement suivant, qui l'écrasait sans prévenir, ou ne l'écrasait pas s'il ne touchait pas le même champ.
Le GitOps traite ces trois défauts à la racine, en inversant le sens du déploiement et en faisant de Git la description, versionnée et relue, de ce qui doit tourner. Cette leçon pose les principes, puis les fait sentir en écrivant un agent de réconciliation de vingt lignes. Ses limites, très vite atteintes, expliquent tout ce qu'Argo CD fait de plus, et donnent le plan du cours.
Les concepts
Les quatre principes
Le terme GitOps a été proposé en 2017 par l'éditeur Weaveworks. Pour lui donner une définition indépendante des produits, un groupe de travail de la CNCF, OpenGitOps, a publié en 2021 quatre principes. L'état voulu (desired state) d'un système géré en GitOps doit être :
- Déclaratif. Il décrit ce qui doit exister (deux répliques de Signalements en version 1.1.0, exposées par un service), pas comment y arriver (lancer telle commande, puis telle autre). Kubernetes s'y prête naturellement : ses objets sont des déclarations.
- Versionné et immuable. Il est stocké de façon à garantir l'immuabilité, le versionnement et la conservation de tout l'historique. Git en est l'exemple évident : chaque état est un commit, que l'on ne modifie pas et auquel on peut revenir. Le nom de la méthode vient de là, mais le principe n'exige pas Git, seulement ses propriétés (un registre OCI qui conserve des artefacts immuables les a aussi).
- Tiré automatiquement. Des agents logiciels vont chercher eux-mêmes les déclarations de l'état voulu à leur source. Personne ne pousse le changement dans le système.
- Réconcilié en continu. Ces agents observent en permanence l'état réel du système et cherchent à y appliquer l'état voulu.
Les deux derniers principes font toute la différence avec un pipeline qui exécuterait kubectl apply depuis Git : l'application est faite depuis l'intérieur du système, et elle ne s'arrête jamais.
Pousser ou tirer
flowchart LR
subgraph Pousse["Modèle poussé"]
direction LR
D1["Développeur"] -->|commit| G1["Git"]
G1 -->|déclenche| CI1["Pipeline<br/>(identifiants du cluster)"]
CI1 -->|"kubectl apply / helm upgrade"| K1["Cluster"]
end
subgraph Tire["Modèle tiré (GitOps)"]
direction LR
D2["Développeur"] -->|commit| G2["Git"]
A2["Agent<br/>(dans le cluster)"] -->|"lit"| G2
A2 -->|"applique, observe, corrige"| K2["Cluster"]
end
| Poussé | Tiré | |
|---|---|---|
| Qui détient l'accès au cluster | le pipeline, hors du cluster | l'agent, dans le cluster |
| Quand l'état est appliqué | à chaque run du pipeline | en continu |
| Un changement manuel dans le cluster | reste, jusqu'au prochain déploiement qui touche ce champ | est détecté, et corrigé si on le demande |
| Ce que dit Git | ce qui a été demandé, pas forcément ce qui tourne | ce qui doit tourner, vérifié en permanence |
| Retour arrière | relancer un ancien pipeline | revenir sur un commit |
| Ports entrants vers le cluster | l'API du cluster doit être joignable par le pipeline | aucun : l'agent sort vers Git |
La dernière ligne compte en sécurité : l'API d'un cluster dont le seul déployeur est un agent interne peut rester privée, et aucun identifiant du cluster ne circule hors de lui.
La boucle de réconciliation
Kubernetes lui-même est construit sur des boucles de contrôle : un contrôleur compare l'état voulu d'un objet (sa spec) à l'état observé, et agit pour réduire l'écart. Le contrôleur des Deployments crée des pods tant qu'il en manque. Le GitOps applique la même idée un étage au-dessus : l'état voulu n'est plus seulement dans l'API du cluster, il est dans Git, et un agent le recopie dans l'API.
flowchart LR
G["État voulu<br/>(Git)"] --> C{"Comparer"}
R["État réel<br/>(API du cluster)"] --> C
C -->|"identiques"| A["Attendre"]
C -->|"écart"| P["Appliquer"]
P --> R
A --> C
Un écart (drift) est toute différence entre les deux : un commit pas encore appliqué, ou une modification faite directement dans le cluster. L'agent peut signaler l'écart, ou le corriger (on parle d'auto-réparation, self-healing). Ce sont deux choix distincts, que l'on règle séparément dans Argo CD.
En pratique
Le laboratoire
Le cours s'appuie sur un laboratoire entièrement local. Téléchargez d'abord les deux binaires qui manquent, en vérifiant leur empreinte :
$ curl -sSLo kind https://github.com/kubernetes-sigs/kind/releases/download/v0.33.0/kind-linux-amd64
$ curl -sSL https://github.com/kubernetes-sigs/kind/releases/download/v0.33.0/kind-linux-amd64.sha256sum \
| awk '{print $1" kind"}' | sha256sum -c
kind: OK
$ curl -sSLo argocd https://github.com/argoproj/argo-cd/releases/download/v3.5.3/argocd-linux-amd64
$ curl -sSL https://github.com/argoproj/argo-cd/releases/download/v3.5.3/cli_checksums.txt \
| grep 'argocd-linux-amd64$' | awk '{print $1" argocd"}' | sha256sum -c
argocd: OK
$ chmod +x kind argocd # puis placez-les dans votre PATH
Le script installer-labo.sh monte ensuite le cluster, Argo CD et la forge. Il a besoin de deux fichiers à côté de lui : forge.yaml (la forge, donnée plus bas) et les images de Signalements, construites à partir du Dockerfile des cours précédents avec deux étiquettes :
$ docker build -q --build-arg VERSION=1.0.0 -t registre.lyneko.example/signalements:1.0.0 .
$ docker build -q --build-arg VERSION=1.1.0 -t registre.lyneko.example/signalements:1.1.0 .
Le nom de registre registre.lyneko.example est fictif : les images ne sont jamais publiées, elles sont chargées directement dans le nœud du cluster.
#!/usr/bin/env bash
# Monte le laboratoire du cours GitOps : cluster kind, Argo CD, forge Forgejo,
# images de Signalements. Tout est local ; rien n'est publié.
# Prérequis : docker, kind, kubectl, git, curl, jq, openssl.
set -euo pipefail
cd "$(dirname "$0")"
export KUBECONFIG=$PWD/kubeconfig # jamais le kubeconfig par défaut
ARGOCD_VERSION=v3.5.3
NOEUD=kindest/node:v1.36.4@sha256:099e049362a1526b2db71494e1947aae99bd16290d7c895f2b7ea312e3cbfaed
# 1. Le cluster, sans toucher au contexte courant de l'utilisateur
kind create cluster --name gitops --image "$NOEUD" --kubeconfig "$KUBECONFIG" --wait 120s
# 2. Argo CD
kubectl create namespace argocd
kubectl apply -n argocd --server-side --force-conflicts \
-f "https://raw.githubusercontent.com/argoproj/argo-cd/$ARGOCD_VERSION/manifests/install.yaml" > /dev/null
kubectl -n argocd rollout status deploy/argocd-server --timeout=300s
# 3. La forge et son compte d'administration (mot de passe aléatoire, fichier 600)
kubectl apply -f forge.yaml
kubectl -n forge rollout status deploy/forgejo --timeout=300s
umask 077
openssl rand -base64 18 | tr -d '/+=' > forge.mdp
kubectl -n forge exec deploy/forgejo -- forgejo admin user create --username lyneko \
--password "$(cat forge.mdp)" --email formation@lyneko.example --admin --must-change-password=false
# 4. Les images de Signalements, chargées directement dans le nœud
kind load docker-image --name gitops \
registre.lyneko.example/signalements:1.0.0 registre.lyneko.example/signalements:1.1.0
echo "Laboratoire prêt. Mot de passe initial d'Argo CD : argocd admin initial-password -n argocd"Trois choix méritent une explication :
- Un fichier
kubeconfigpropre au laboratoire. Par défaut,kind create clusterajoute le nouveau cluster à~/.kube/configet en fait le contexte courant. Sur un poste qui sert aussi à administrer de vrais clusters, la commandekubectlsuivante, tapée par habitude, viserait le cluster de démonstration, ou l'inverse. L'option--kubeconfiget la variableKUBECONFIGisolent complètement le laboratoire. Pensez à exporter cette variable dans chaque terminal du cours. - Une image de nœud épinglée en 1.36.4. kind 0.33 crée par défaut un nœud Kubernetes 1.37 ; or Argo CD 3.5 est testé avec les versions 1.33 à 1.36. On reste dans la matrice testée, et sur la version qui tourne chez Lyneko au moment de l'écriture.
- L'installation d'Argo CD en mode serveur (
--server-side). La définition de la ressource ApplicationSet est trop volumineuse pour l'application classique dekubectl, qui stocke une copie de chaque objet dans une annotation limitée à 256 Kio. La section Pièges courants montre l'erreur.
La forge, forge.yaml, est un Forgejo sans privilèges, avec une base SQLite, dans le namespace forge :
# Forge Git de démonstration : Forgejo sans racine, SQLite, dans le cluster.
apiVersion: v1
kind: Namespace
metadata:
name: forge
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: forgejo-donnees
namespace: forge
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 2Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: forgejo
namespace: forge
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels: {app: forgejo}
template:
metadata:
labels: {app: forgejo}
spec:
securityContext:
fsGroup: 1000
containers:
- name: forgejo
image: codeberg.org/forgejo/forgejo:16.0.5-rootless
ports:
- containerPort: 3000
env:
- {name: FORGEJO__database__DB_TYPE, value: sqlite3}
- {name: FORGEJO__security__INSTALL_LOCK, value: "true"}
- {name: FORGEJO__server__ROOT_URL, value: "http://forgejo.forge.svc.cluster.local:3000/"}
- {name: FORGEJO__server__DISABLE_SSH, value: "true"}
- {name: FORGEJO__service__DISABLE_REGISTRATION, value: "true"}
volumeMounts:
- {name: donnees, mountPath: /var/lib/gitea}
readinessProbe:
httpGet: {path: /api/healthz, port: 3000}
volumes:
- name: donnees
persistentVolumeClaim:
claimName: forgejo-donnees
---
apiVersion: v1
kind: Service
metadata:
name: forgejo
namespace: forge
spec:
selector: {app: forgejo}
ports:
- port: 3000
targetPort: 3000Forgejo est une forge libre, gouvernée par une communauté et hébergée chez Codeberg, une association de droit allemand : un choix cohérent avec les exigences de souveraineté de nombreux clients de Lyneko, et assez léger pour tourner dans un cluster de démonstration. Ses variables FORGEJO__section__CLE remplacent le fichier de configuration.
$ time ./installer-labo.sh
...
namespace/argocd created
deployment "argocd-server" successfully rolled out
namespace/forge created
persistentvolumeclaim/forgejo-donnees created
deployment.apps/forgejo created
service/forgejo created
deployment "forgejo" successfully rolled out
New user 'lyneko' has been successfully created!
...
Laboratoire prêt. Mot de passe initial d'Argo CD : argocd admin initial-password -n argocd
real 2m46,613s
$ export KUBECONFIG=$PWD/kubeconfig
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
gitops-control-plane Ready control-plane 2m12s v1.36.4
Reste à créer le dépôt de déploiement dans la forge. Le script preparer-depot.sh ouvre un tunnel vers la forge (kubectl port-forward) et crée l'organisation plateforme et le dépôt par l'API de Forgejo :
#!/usr/bin/env bash
# Crée l'organisation « plateforme » et le dépôt de déploiement dans la forge,
# et y pousse le contenu initial. La forge est jointe par un port-forward.
set -euo pipefail
cd "$(dirname "$0")"
export KUBECONFIG=$PWD/kubeconfig
pgrep -f "port-forward svc/forgejo 3000" > /dev/null || {
kubectl -n forge port-forward svc/forgejo 3000:3000 > pf-forge.log 2>&1 &
sleep 3
}
api() { curl -sf -u "lyneko:$(cat forge.mdp)" -H 'Content-Type: application/json' "$@"; }
api -X POST http://127.0.0.1:3000/api/v1/orgs -d '{"username":"plateforme","visibility":"public"}' > /dev/null
api -X POST http://127.0.0.1:3000/api/v1/orgs/plateforme/repos \
-d '{"name":"signalements-deploiement","default_branch":"main"}' | jq -r .clone_url$ ./preparer-depot.sh
http://forgejo.forge.svc.cluster.local:3000/plateforme/signalements-deploiement.git
L'adresse affichée est celle que verront les composants du cluster ; depuis votre poste, on passe par le tunnel, http://127.0.0.1:3000/. Pour que git connaisse le mot de passe sans l'écrire dans .git/config, un petit programme le lui fournit à la demande (la variable GIT_ASKPASS) :
$ printf '#!/bin/sh\ncase "$1" in Username*) echo lyneko ;; *) cat %s/forge.mdp ;; esac\n' "$PWD" > askpass.sh
$ chmod 700 askpass.sh && export GIT_ASKPASS=$PWD/askpass.sh
Le contenu du dépôt
Le dépôt de déploiement ne contient pas le code de Signalements, seulement la description de ce qui doit tourner. Pour commencer, deux manifestes Kubernetes ordinaires dans un répertoire signalements/ :
# signalements/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: signalements
labels:
app: signalements
spec:
replicas: 2
selector:
matchLabels:
app: signalements
template:
metadata:
labels:
app: signalements
spec:
securityContext:
runAsNonRoot: true
containers:
- name: signalements
image: registre.lyneko.example/signalements:1.0.0
ports:
- containerPort: 8000
env:
- name: ENVIRONNEMENT
value: recette
readinessProbe:
httpGet:
path: /sante
port: 8000
resources:
requests:
cpu: 50m
memory: 96Mi
limits:
memory: 192Mi# signalements/service.yaml
apiVersion: v1
kind: Service
metadata:
name: signalements
spec:
selector:
app: signalements
ports:
- port: 80
targetPort: 8000$ git init -q -b main && git add . && git commit -qm "Signalements : déploiement et service"
$ git remote add origin http://127.0.0.1:3000/plateforme/signalements-deploiement.git
$ git push -q origin main
Un agent de réconciliation en vingt lignes
Avant d'utiliser Argo CD, écrivons l'agent le plus simple possible. Il clone le dépôt, puis, toutes les dix secondes, le met à jour, compare son contenu au cluster avec kubectl diff, et applique s'il y a un écart :
#!/usr/bin/env bash
# Un agent GitOps minimal : il tire le dépôt, compare, applique, recommence.
# Usage : ./reconciliateur.sh <url du dépôt> <répertoire> <namespace> [intervalle]
set -euo pipefail
depot=$1 chemin=$2 ns=$3 intervalle=${4:-10}
travail=$(mktemp -d)
trap 'rm -rf "$travail"' EXIT
git clone --quiet "$depot" "$travail"
kubectl create namespace "$ns" --dry-run=client -o yaml | kubectl apply -f - > /dev/null
while true; do
git -C "$travail" pull --quiet --ff-only
commit=$(git -C "$travail" rev-parse --short HEAD)
# kubectl diff : code 0 si rien ne diffère, 1 s'il y a une différence
if kubectl diff -n "$ns" -f "$travail/$chemin" > "$travail/ecart.diff" 2>&1; then
echo "$(date +%T) $commit : synchronisé"
else
echo "$(date +%T) $commit : écart détecté, application"
grep -E '^[-+] ' "$travail/ecart.diff" | grep -v -E 'generation|resourceVersion|managedFields' | head -4 | sed 's/^/ /'
kubectl apply -n "$ns" -f "$travail/$chemin" | sed 's/^/ /'
fi
sleep "$intervalle"
donekubectl diff est la pièce centrale : il envoie les manifestes au serveur en simulation (dry run côté serveur), récupère l'objet tel qu'il serait après application, et le compare à l'objet existant. Son code de sortie vaut 0 sans différence, 1 avec. C'est exactement la question « l'état réel est-il l'état voulu ? ».
Lancez-le en arrière-plan sur un namespace de démonstration, attendez une vingtaine de secondes, puis provoquez deux événements : une modification manuelle du cluster, et une modification dans Git.
$ ./reconciliateur.sh http://127.0.0.1:3000/plateforme/signalements-deploiement.git signalements demo-maison 10 > reconciliateur.log 2>&1 &
$ kubectl -n demo-maison scale deployment signalements --replicas=5 # un geste fait à la main
deployment.apps/signalements scaled
$ sed -i 's|signalements:1.0.0|signalements:1.1.0|' signalements/deployment.yaml
$ git commit -qam "Signalements 1.1.0" && git push -q origin main # un changement voulu
$ cat reconciliateur.log
09:10:54 e401012 : écart détecté, application
+ creationTimestamp: "2026-10-02T07:10:54Z"
+ labels:
+ app: signalements
+ name: signalements
deployment.apps/signalements created
service/signalements created
09:11:04 e401012 : synchronisé
09:11:15 e401012 : synchronisé
09:11:25 e401012 : écart détecté, application
- replicas: 5
+ replicas: 2
deployment.apps/signalements configured
service/signalements unchanged
09:11:36 d33fd49 : écart détecté, application
- image: registre.lyneko.example/signalements:1.0.0
+ image: registre.lyneko.example/signalements:1.1.0
deployment.apps/signalements configured
service/signalements unchanged
09:11:46 d33fd49 : synchronisé
09:11:57 d33fd49 : synchronisé
Tout le GitOps est dans ces lignes :
- 09:10:54 : premier passage, le namespace est vide, l'agent crée les objets. Personne n'a lancé
kubectl applyà la main. - 09:11:25 : le passage à cinq répliques, fait directement dans le cluster, est un écart. L'agent le voit (
replicas: 5contrereplicas: 2) et le corrige : le cluster revient à ce que dit Git. - 09:11:36 : le commit
d33fd49change l'image. L'agent le tire et l'applique. Le déploiement de la version 1.1.0 n'a demandé qu'un commit.
Ce que l'agent ne sait pas faire
Retirons maintenant le service du dépôt, comme on le ferait pour supprimer un composant devenu inutile :
$ git rm -q signalements/service.yaml && git commit -qm "Retrait du service" && git push -q origin main
$ cat reconciliateur2.log
09:12:11 14a1ed4 : synchronisé
09:12:22 14a1ed4 : synchronisé
$ kubectl -n demo-maison get service signalements
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
signalements ClusterIP 10.96.199.61 <none> 80/TCP 92s
L'agent se déclare « synchronisé », et le service tourne toujours. Il compare ce qui est dans le dépôt au cluster ; ce qui n'y est plus, il ne le voit pas. Pour supprimer un objet retiré de Git, il faudrait savoir quels objets du cluster il gère, donc les marquer et en tenir la liste. C'est la première de ses limites, et la liste est longue :
| Ce qu'il faudrait | Ce que fait l'agent | Leçon où Argo CD le traite |
|---|---|---|
| Supprimer ce qui a été retiré de Git (prune) | rien | 3 |
| Dire si l'application fonctionne (santé), pas seulement si elle est conforme | rien | 3 |
| Choisir de signaler un écart sans le corriger | corrige toujours | 3 |
| Comprendre Helm et Kustomize | manifestes bruts seulement | 3, 4 |
| Ordonner : migration de base avant la nouvelle version | applique tout d'un coup | 5 |
| Gérer cent applications et plusieurs clusters | une boucle par application | 6, 7 |
| Garder les secrets hors de Git | rien | 8 |
| Limiter qui déploie quoi, où | l'agent a tous les droits de son kubeconfig | 9 |
| Afficher, auditer, revenir en arrière en un clic | un fichier de journal | 2, 10 |
Remettez le service dans le dépôt (git revert --no-edit HEAD && git push -q origin main), la leçon 2 en a besoin. Puis arrêtez l'agent (kill %1) et supprimez son namespace (kubectl delete namespace demo-maison) : la suite du cours utilise Argo CD.
Sous le capot
kubectl apply est l'application « déclarative » de Kubernetes, mais il doit résoudre un problème délicat : quand un champ disparaît du fichier, faut-il le supprimer de l'objet, ou a-t-il été posé par quelqu'un d'autre (un contrôleur, un autre outil) ? Deux mécanismes existent :
- L'application côté client, historique, stocke dans l'annotation
kubectl.kubernetes.io/last-applied-configurationune copie du dernier fichier appliqué. Au passage suivant,kubectlcalcule un three-way merge entre cette copie, le nouveau fichier et l'objet réel : un champ présent dans la copie mais plus dans le fichier est supprimé, un champ jamais déclaré est laissé tranquille. C'est cette annotation, limitée à 256 Kio, qui fait échouer l'installation classique d'Argo CD. - L'application côté serveur (server-side apply) confie ce calcul à l'API : chaque champ a un gestionnaire (field manager) enregistré dans
metadata.managedFields, et un outil ne supprime que les champs dont il est gestionnaire. Deux outils qui revendiquent le même champ provoquent un conflit explicite.
Notre agent a « oublié » le service, et ce n'est pas une affaire de côté client ou de côté serveur : dans les deux cas, la mémoire de kubectl apply est par objet, pas par ensemble d'objets, et rien ne tient l'inventaire de ce qui a été appliqué. kubectl apply --prune existe, avec une liste de types et une étiquette de sélection, mais il est resté en version alpha pendant des années, précisément parce que le problème est difficile. Argo CD résout autrement : il marque chaque objet qu'il crée (une annotation de suivi, que la leçon 3 examine) et peut donc retrouver ceux qui n'existent plus dans Git.
Les contrôleurs de Kubernetes et les agents GitOps partagent une propriété qui rend la boucle robuste : ils sont idempotents et convergents. Appliquer deux fois le même état ne change rien ; un passage raté sera rattrapé au suivant. Le redémarrage du démon Docker de la machine de test, survenu pendant la préparation de ce cours, l'a illustré malgré nous : tous les conteneurs, dont le nœud du cluster, ont redémarré, et le cluster est revenu à l'état décrit sans aucune intervention.
Pièges courants
metadata.annotations: Too long: may not be more than 262144 bytes. L'installation d'Argo CD par kubectl apply classique échoue sur la définition de ressource ApplicationSet :
$ kubectl apply -n argocd --dry-run=server -f install.yaml 2>&1 | grep -i "too long"
The CustomResourceDefinition "applicationsets.argoproj.io" is invalid: metadata.annotations: Too long: may not be more than 262144 bytes
Utilisez kubectl apply --server-side --force-conflicts, comme le script du laboratoire et la documentation d'Argo CD.
Le contexte kubectl a changé tout seul. kind create cluster sans --kubeconfig fait du nouveau cluster le contexte courant. Vérifiez avec kubectl config current-context avant toute commande sur un poste qui gère aussi de vrais clusters.
« GitOps » qui n'en est pas. Un pipeline qui exécute kubectl apply depuis Git après chaque commit applique les deux premiers principes, pas les deux derniers : rien n'est tiré, rien n'est réconcilié entre deux commits, et le pipeline détient les identifiants du cluster.
Une étiquette d'image mobile dans Git. image: signalements:latest rend l'état voulu ambigu : le même commit peut désigner deux images différentes selon le jour. Git doit contenir une étiquette immuable (1.1.0, main-6ef8842) ou, mieux, une empreinte.
Des corrections manuelles qui « reviennent ». Avec l'auto-réparation, un kubectl edit fait pendant un incident est annulé en quelques secondes. Ce n'est pas un défaut : c'est le principe. La correction d'urgence se fait dans Git, ou en suspendant explicitement la synchronisation automatique de l'application (leçon 3), ou par une fenêtre de synchronisation (leçon 7).
L'œuf et la poule. L'agent qui installe tout doit lui-même être installé. On l'installe une première fois à la main (ou par Terraform, comme chez Lyneko), puis on peut lui confier sa propre configuration (leçon 4).
Sécurité
Git devient le plan de contrôle. Quiconque peut écrire dans le dépôt de déploiement peut déployer. La protection du dépôt (relecture obligatoire, branches protégées, signatures de commits, liste restreinte de personnes ayant le droit d'écrire) devient la protection du cluster. Le cours GitHub Actions a montré ce que coûtent les limites d'un plan gratuit sur ce point.
L'agent a des droits étendus. L'installation standard d'Argo CD lui donne des droits d'administrateur sur le cluster, pour pouvoir y créer n'importe quoi. Un agent compromis, ou un dépôt compromis qu'il suit aveuglément, c'est un cluster compromis. Les projets et le RBAC d'Argo CD (leçon 9) servent à restreindre ce que chaque application peut déployer.
Aucun identifiant du cluster hors du cluster. C'est le gain majeur du modèle tiré. Chez Lyneko, le passage au GitOps a supprimé le secret KUBE_CONFIG des dépôts d'applications : un pipeline compromis peut écrire une mauvaise étiquette dans Git, visible et réversible, mais ne peut plus agir sur le cluster.
Les secrets n'ont rien à faire dans Git, même dans un dépôt privé : l'historique est éternel, et chaque clone en emporte une copie. La leçon 8 montre comment les faire arriver dans le cluster sans passer par le dépôt.
En production
Chez Lyneko. Argo CD 3.5 tourne sur le cluster Kapsule qui héberge les applications internes et clientes. Chaque application a un dépôt avec son code et son chart Helm ; son pipeline construit l'image, écrit l'étiquette dans le fichier de valeurs et pousse le commit ; Argo CD, qui suit la branche main, déploie. La couche plateforme (contrôleurs d'ingress, cert-manager, l'opérateur de secrets, Argo CD lui-même) est, elle, installée par Terraform : le GitOps commence au-dessus de la plateforme.
Argo CD ou Flux. Les deux outils de référence sont des projets diplômés de la CNCF (Argo en décembre 2022, Flux en novembre 2022) et suivent les quatre principes. Argo CD est centré sur l'application : des ressources Application, AppProject et ApplicationSet, un serveur d'API avec interface web, authentification unique et RBAC propre, et la gestion de plusieurs clusters depuis un point central. Flux est un ensemble de contrôleurs composables, sans interface intégrée, qui s'appuie sur le RBAC de Kubernetes et installe de vraies releases Helm. Le choix dépend surtout du modèle d'exploitation : une équipe plateforme qui sert des équipes produit appréciera l'interface et le cloisonnement d'Argo CD ; un cluster géré par une seule équipe très à l'aise avec Kubernetes peut préférer la sobriété de Flux.
Quand ne pas faire de GitOps. Pour des ressources très dynamiques (des jobs créés à la demande par une application, des environnements éphémères créés et détruits en minutes), Git n'est pas le bon support d'état. Et une petite équipe avec une seule application peut vivre longtemps avec un déploiement poussé bien protégé. Le GitOps se justifie quand le nombre d'applications, d'équipes ou d'environnements rend indispensable de savoir, à tout instant, ce qui doit tourner et si c'est bien ce qui tourne.
Exercices
1. Pour chacun de ces dispositifs, dites quels principes d'OpenGitOps il respecte : (a) un pipeline qui exécute helm upgrade depuis Git à chaque commit ; (b) un CronJob dans le cluster qui exécute toutes les heures git pull && kubectl apply -f ; (c) le script de cette leçon.
Solution
(a) Déclaratif et versionné (l'état est dans Git) ; ni tiré (le pipeline pousse), ni réconcilié en continu (rien ne se passe entre deux commits). (b) Les quatre, avec une réconciliation lente (une heure) et les limites du script : pas d'élagage, pas de santé. (c) Les quatre, avec les mêmes limites, et une réconciliation toutes les dix secondes.
2. Modifiez le script pour qu'il signale les écarts sans les corriger, sauf ceux qui viennent d'un nouveau commit. Comment distinguer les deux cas ?
Solution
Il suffit de mémoriser le dernier commit appliqué : si HEAD a changé depuis le passage précédent, l'écart vient de Git et l'agent applique ; sinon, il vient du cluster et l'agent se contente d'afficher le diff. Par exemple, une variable dernier mise à jour après chaque kubectl apply, et un test if [ "$commit" != "$dernier" ] avant d'appliquer. C'est la distinction que fait Argo CD entre synchronisation automatique et auto-réparation (selfHeal), deux réglages séparés.
3. Proposez une façon de faire supprimer par le script les objets retirés de Git. Quels risques voyez-vous ?
Solution
Marquer chaque objet appliqué (une étiquette geree-par=reconciliateur-signalements, ajoutée par kubectl label ou dans les manifestes), puis, à chaque passage, lister les objets portant cette étiquette et supprimer ceux qui ne sont plus dans le dépôt (ou utiliser kubectl apply --prune -l ...). Risques : une erreur dans le dépôt (un répertoire vidé par mégarde, un clone incomplet) fait supprimer toute l'application ; un objet avec données (un volume persistant) disparaît avec ses données. C'est pourquoi Argo CD n'élague pas par défaut, et permet d'exclure des objets de l'élagage.
4. Un collègue objecte : « avec le GitOps, si Git tombe, on ne peut plus déployer ». Est-ce vrai ? Et si le cluster ne peut plus joindre Git, que deviennent les applications ?
Solution
On ne peut plus déployer par le chemin normal : c'est vrai, et c'est aussi vrai d'un pipeline qui déploie depuis Git. Les applications, elles, continuent de tourner : l'agent ne peut plus tirer de nouvel état, mais l'état appliqué reste en place, et Kubernetes continue de le maintenir (redémarrer les pods, etc.). La forge devient un service critique de la chaîne de livraison, à superviser et à sauvegarder comme tel ; en cas de panne longue, on peut suspendre la synchronisation et intervenir à la main, puis remettre Git à jour.
5. Pourquoi le laboratoire épingle-t-il l'image du nœud kind en version 1.36.4, alors que kind propose 1.37 par défaut ?
Solution
Parce qu'Argo CD 3.5 est testé avec Kubernetes 1.33 à 1.36. Une version de Kubernetes plus récente que celles testées peut fonctionner, mais rien ne le garantit : des API peuvent avoir changé ou disparu. En production comme en formation, on reste dans la matrice testée de chaque composant, et on met à jour le cluster et l'outil de déploiement de façon coordonnée.
Récapitulatif
- Le GitOps, selon OpenGitOps, c'est un état voulu déclaratif, versionné et immuable, tiré automatiquement par des agents qui le réconcilient en continu avec l'état réel.
- Le modèle tiré retire au pipeline l'accès au cluster et fait de Git la description vérifiée de ce qui tourne.
- Un écart est toute différence entre Git et le cluster ; l'agent peut le signaler ou le corriger.
- Un agent naïf (
git pull,kubectl diff,kubectl applyen boucle) suffit à sentir le principe, et montre vite ses limites : pas d'élagage, pas de santé, pas d'ordre, pas de cloisonnement. - Git devient le plan de contrôle : sa protection est celle du cluster.
- Le laboratoire du cours tourne en local : kind (Kubernetes 1.36.4), Argo CD 3.5.3, Forgejo 16.0.5, avec un
kubeconfigisolé.
Pour aller plus loin
- OpenGitOps, principes et glossaire : la définition de référence, courte et précise.
- Kubernetes, Controllers : la boucle de contrôle, dont le GitOps est une extension.
- Leçon suivante : Installer Argo CD et déployer une application.
Sources
- OpenGitOps, GitOps Principles v1.0.0 (2021)
- OpenGitOps, glossaire (état voulu, réconciliation, tirer)
- Kubernetes, Declarative Management of Kubernetes Objects Using Configuration Files
- Kubernetes, kubectl diff
- Kubernetes, Controllers (boucle de contrôle)
- Argo CD, Getting Started et architecture
- Argo CD, tested Kubernetes versions
- kind, Quick Start
- Weaveworks, Operations by Pull Request (2017, article fondateur du terme GitOps)