Aller au contenu
GitOps : les principes et le modèle tiré

GitOps : les principes et le modèle tiré

200 Pratiquer ⏱ 1 h gitopskubernetesargocd

À la fin, vous saurez

  • Énoncer les quatre principes du GitOps et ce que chacun exige concrètement
  • Distinguer déploiement poussé et déploiement tiré, et leurs conséquences de sécurité
  • Décrire une boucle de réconciliation et ce qu'est un écart (drift)
  • Monter le laboratoire du cours : cluster kind, Argo CD et forge Forgejo
  • Identifier ce qu'un agent de réconciliation naïf ne sait pas faire, et donc ce qu'apporte Argo CD

Prérequis

Testé avec argocd 3.5.3 forgejo 16.0.5 git 2.43.0 kind 0.33.0 kubectl 1.36.0 kubernetes 1.36.4 , vérifié le 2 octobre 2026

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 kubeconfig dans 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 --set du dernier run : le fichier de valeurs du dépôt indiquait latest, 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 edit pendant 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 :

  1. 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.
  2. 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).
  3. 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.
  4. 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 clusterle pipeline, hors du clusterl'agent, dans le cluster
Quand l'état est appliquéà chaque run du pipelineen continu
Un changement manuel dans le clusterreste, jusqu'au prochain déploiement qui touche ce champest détecté, et corrigé si on le demande
Ce que dit Gitce qui a été demandé, pas forcément ce qui tournece qui doit tourner, vérifié en permanence
Retour arrièrerelancer un ancien pipelinerevenir sur un commit
Ports entrants vers le clusterl'API du cluster doit être joignable par le pipelineaucun : 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 kubeconfig propre au laboratoire. Par défaut, kind create cluster ajoute le nouveau cluster à ~/.kube/config et en fait le contexte courant. Sur un poste qui sert aussi à administrer de vrais clusters, la commande kubectl suivante, tapée par habitude, viserait le cluster de démonstration, ou l'inverse. L'option --kubeconfig et la variable KUBECONFIG isolent 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 de kubectl, 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: 3000

Forgejo 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"
done

kubectl 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: 5 contre replicas: 2) et le corrige : le cluster revient à ce que dit Git.
  • 09:11:36 : le commit d33fd49 change 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 faudraitCe que fait l'agentLeçon où Argo CD le traite
Supprimer ce qui a été retiré de Git (prune)rien3
Dire si l'application fonctionne (santé), pas seulement si elle est conformerien3
Choisir de signaler un écart sans le corrigercorrige toujours3
Comprendre Helm et Kustomizemanifestes bruts seulement3, 4
Ordonner : migration de base avant la nouvelle versionapplique tout d'un coup5
Gérer cent applications et plusieurs clustersune boucle par application6, 7
Garder les secrets hors de Gitrien8
Limiter qui déploie quoi, oùl'agent a tous les droits de son kubeconfig9
Afficher, auditer, revenir en arrière en un clicun fichier de journal2, 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-configuration une copie du dernier fichier appliqué. Au passage suivant, kubectl calcule 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 apply en 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 kubeconfig isolé.

Pour aller plus loin

Voir ma constellation →

Sources