22. GitOps avec ArgoCD#

Le GitOps est une pratique opérationnelle qui utilise Git comme seule source de vérité pour l’état désiré de l’infrastructure et des applications. Toute modification passe par une pull request, ce qui garantit la traçabilité, la réversibilité et la collaboration. ArgoCD est aujourd’hui l’implémentation de référence de cette approche dans l’écosystème Kubernetes.

Principes GitOps#

Git comme seule source de vérité#

Le GitOps repose sur quatre principes fondamentaux, formalisés par Weaveworks :

  1. L’état désiré est déclaratif : l’infrastructure est décrite par des manifestes (Kubernetes YAML, Helm charts, Kustomize overlays), pas par des scripts impératifs

  2. L’état désiré est versionné dans Git : chaque changement est un commit, avec un auteur, une date et un message

  3. Les changements approuvés sont appliqués automatiquement : un agent surveille Git et réconcilie l’état réel avec l’état désiré

  4. Les agents logiciels assurent la correction : si l’état réel dérive, l’agent le détecte et le corrige sans intervention humaine

Pull model vs push model#

Le push model traditionnel (Ansible, scripts de déploiement) présente des risques : les credentials de déploiement doivent être exposés à l’outil CI, et il n’y a pas de réconciliation continue. Une modification manuelle en production passe inaperçue.

Le pull model GitOps inverse la relation : un agent tournant dans le cluster surveille le dépôt Git et tire les changements lui-même. Les credentials ne quittent jamais le cluster. La réconciliation est continue et automatique.

Dérive de configuration

Sans réconciliation continue, un kubectl edit en production crée une dérive silencieuse entre Git et le cluster. Avec ArgoCD en mode selfHeal, toute modification directe est immédiatement écrasée par l’état Git, forçant le passage par les pull requests.

ArgoCD : architecture#

ArgoCD est composé de cinq services principaux :

  • Application Controller : le cœur du système. Il surveille en permanence les Applications ArgoCD, compare l’état Git avec l’état du cluster, et déclenche les synchronisations. Il s’exécute dans une boucle de réconciliation toutes les 3 minutes par défaut.

  • Repo Server : clone les dépôts Git, génère les manifestes (Helm, Kustomize, Jsonnet, YAML brut) et les met en cache. Il est stateless et scalable horizontalement.

  • API Server : expose l’API REST et gRPC utilisée par l’interface web et la CLI argocd. Il gère l’authentification (OIDC, LDAP) et l’autorisation RBAC.

  • Dex : fournisseur OIDC embarqué pour la fédération d’identité (GitHub, GitLab, LDAP, SAML). Peut être remplacé par un IdP externe.

  • Redis : cache partagé entre les composants, utilisé pour stocker l’état des applications et les manifestes générés.

Application ArgoCD#

Structure d’une Application#

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
  # Finalizer pour la suppression en cascade
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: default

  # Source : où se trouve l'état désiré
  source:
    repoURL: https://github.com/myorg/k8s-manifests
    targetRevision: main        # Branche, tag ou SHA
    path: apps/my-app/overlays/production

  # Destination : où déployer
  destination:
    server: https://kubernetes.default.svc
    namespace: production

  # Politique de synchronisation
  syncPolicy:
    automated:
      prune: true       # Supprimer les ressources absentes de Git
      selfHeal: true    # Rétablir l'état Git si dérive détectée
    syncOptions:
      - CreateNamespace=true
      - PrunePropagationPolicy=foreground

Stratégies de synchronisation#

Le champ syncPolicy.automated active la synchronisation automatique. Sans lui, ArgoCD détecte les écarts mais attend une action manuelle.

  • prune: true : les ressources présentes dans le cluster mais absentes de Git sont supprimées. Ce comportement est risqué sans selfHeal car une ressource créée manuellement serait supprimée à la prochaine sync.

  • selfHeal: true : toute dérive (modification directe dans le cluster) est immédiatement corrigée. C’est la garantie d’immutabilité du GitOps.

Bonnes pratiques de synchronisation

En production, activez selfHeal pour les environnements critiques et désactivez l’accès direct (kubectl apply) au cluster de production. Gardez prune: false en phase d’adoption pour éviter les suppressions accidentelles.

ApplicationSet : déploiement multi-cluster et multi-tenant#

L”ApplicationSet génère automatiquement des objets Application selon des générateurs paramétriques. C’est l’outil de choix pour les architectures multi-cluster et multi-tenant.

Générateur list#

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: guestbook
  namespace: argocd
spec:
  generators:
    - list:
        elements:
          - cluster: production-eu
            url: https://k8s-eu.example.com
          - cluster: production-us
            url: https://k8s-us.example.com
          - cluster: staging
            url: https://k8s-staging.example.com
  template:
    metadata:
      name: "guestbook-{{cluster}}"
    spec:
      project: default
      source:
        repoURL: https://github.com/myorg/guestbook
        targetRevision: HEAD
        path: "deploy/{{cluster}}"
      destination:
        server: "{{url}}"
        namespace: guestbook

Générateur git#

Le générateur git crée une Application par répertoire ou par fichier de configuration trouvé dans le dépôt. Idéal pour l’onboarding de nouveaux services : créer un répertoire dans Git suffit à créer l’Application ArgoCD correspondante.

generators:
  - git:
      repoURL: https://github.com/myorg/k8s-manifests
      revision: HEAD
      directories:
        - path: "apps/*/overlays/production"

Générateur cluster#

Le générateur cluster utilise les clusters enregistrés dans ArgoCD comme source de paramètres. Il permet de déployer une application sur tous les clusters d’une flotte, ou sur un sous-ensemble filtré par labels.

Projects et RBAC ArgoCD#

Un AppProject définit un périmètre d’isolation entre équipes :

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: team-backend
  namespace: argocd
spec:
  description: "Projet équipe Backend"
  # Dépôts sources autorisés
  sourceRepos:
    - "https://github.com/myorg/backend-*"
  # Destinations autorisées
  destinations:
    - namespace: "backend-*"
      server: "https://kubernetes.default.svc"
  # Ressources Kubernetes autorisées (liste blanche)
  clusterResourceWhitelist:
    - group: ""
      kind: Namespace
  namespaceResourceWhitelist:
    - group: "apps"
      kind: Deployment
    - group: ""
      kind: Service
  # RBAC : qui peut faire quoi dans ce projet
  roles:
    - name: developer
      description: "Lecture seule pour les développeurs"
      policies:
        - "p, proj:team-backend:developer, applications, get, team-backend/*, allow"
        - "p, proj:team-backend:developer, applications, sync, team-backend/*, allow"

Resource hooks et waves de synchronisation#

Resource hooks#

Les hooks permettent d’exécuter des Jobs à des moments précis du cycle de sync :

apiVersion: batch/v1
kind: Job
metadata:
  name: db-migration
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migrate
          image: myapp:v2.0.0
          command: ["python", "manage.py", "migrate"]

Les quatre hooks disponibles :

  • PreSync : exécuté avant la synchronisation (migrations de base de données, vérifications pré-déploiement)

  • Sync : exécuté pendant la synchronisation, en parallèle des ressources normales

  • PostSync : exécuté après que toutes les ressources sont saines (tests de smoke, notifications)

  • SyncFail : exécuté si la synchronisation échoue (rollback, alertes)

Waves de synchronisation#

Les waves contrôlent l’ordre de création des ressources au sein d’une même synchronisation :

metadata:
  annotations:
    argocd.argoproj.io/sync-wave: "0"   # Namespace et RBAC en premier
    # "1" : ConfigMaps et Secrets
    # "2" : Deployments
    # "3" : Services et Ingress
    # "5" : Jobs de vérification post-déploiement

ArgoCD attend que toutes les ressources d’une wave soient saines avant de passer à la wave suivante.

App of Apps pattern#

Le pattern « App of Apps » utilise une Application ArgoCD racine qui déploie d’autres Applications ArgoCD. C’est le moyen de bootstrapper ArgoCD lui-même avec GitOps.

# Application racine : déploie toutes les autres Applications
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/argocd-apps
    targetRevision: HEAD
    path: apps    # Ce répertoire contient des fichiers Application YAML
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

Le répertoire apps/ contient un fichier YAML par Application. Ajouter un service dans la flotte ne nécessite que d’ajouter un fichier dans ce répertoire.

Multi-cluster ArgoCD#

Enregistrement d’un cluster#

# Ajouter un cluster distant (utilise le kubeconfig local)
argocd cluster add production-eu \
  --name production-eu \
  --system-namespace argocd

# Lister les clusters enregistrés
argocd cluster list

ArgoCD crée un ServiceAccount dans le cluster distant avec les permissions nécessaires et stocke les credentials dans un Secret dans le namespace argocd.

ArgoCD Image Updater#

ArgoCD Image Updater surveille les registries OCI et met à jour automatiquement les annotations des Applications lorsqu’une nouvelle image est disponible. Il crée un commit dans le dépôt Git (write-back via Git ou Kustomize).

# Annotation sur l'Application ArgoCD
metadata:
  annotations:
    argocd-image-updater.argoproj.io/image-list: |
      myapp=ghcr.io/myorg/myapp:~1.2
    argocd-image-updater.argoproj.io/myapp.update-strategy: semver
    argocd-image-updater.argoproj.io/write-back-method: git

Flux v2 : alternative à ArgoCD#

Flux v2 est l’autre implémentation GitOps CNCF de référence. Son architecture est plus modulaire : chaque fonctionnalité est un controller Kubernetes indépendant.

Dimension

ArgoCD

Flux v2

Interface

Web UI + CLI riche

CLI seule (flux)

Architecture

Monolithique (5 composants)

Modulaire (controllers séparés)

Multi-tenancy

AppProject + RBAC

Namespace isolation native

Templating

Helm, Kustomize, Jsonnet, YAML

Helm, Kustomize

Notifications

Plugin notifications

Notification controller

Adoption

Dominante (CNCF graduated)

Forte (CNCF graduated)

ArgoCD vs Flux v2

ArgoCD est préféré lorsque l’interface web et la visibilité sont importantes (équipes multiples, dashboards). Flux v2 est préféré dans les architectures « operators-first » où tout est Kubernetes-natif et où l’interface web n’est pas une priorité.


Visualisations#

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch
import networkx as nx
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
# Simulation de la boucle de réconciliation GitOps
# Écart desired state / actual state → détection → correction

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

np.random.seed(7)
temps = np.linspace(0, 60, 600)  # 60 minutes

# État désiré : stable à 3 réplicas
desired = np.full(len(temps), 3.0)

# État réel : commence à 3, dérives simulées, corrections automatiques
actual = np.full(len(temps), 3.0, dtype=float)

# Événements : dérive manuelle à t=10, correction à t=12
# Nouvelle dérive à t=30, correction à t=32
# Panne partielle à t=48, correction à t=50
evenements = [
    (10, 30, 5.0, 12, 30),   # (t_debut_derive, t_fin_derive, val, t_correction, idx_correction)
    (30, 50, 1.0, 32, 50),
    (48, 60, 2.0, 50, 60),
]

for t_d, t_f, val, t_c, idx_f in evenements:
    mask_derive = (temps >= t_d) & (temps < t_c)
    mask_apres  = (temps >= t_c) & (temps < t_f)
    actual[mask_derive] = val
    actual[mask_apres]  = np.linspace(val, 3.0, mask_apres.sum()) if mask_apres.sum() > 0 else actual[mask_apres]

actual += np.random.normal(0, 0.05, len(temps))

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(11, 7), sharex=True)

ax1.plot(temps, desired, "--", color="#2ca02c", linewidth=2, label="État désiré (Git)")
ax1.plot(temps, actual, color="#4c72b0", linewidth=1.5, alpha=0.85, label="État réel (cluster)")
ax1.fill_between(temps, desired, actual, alpha=0.2, color="#d62728")
ax1.set_ylabel("Nombre de réplicas")
ax1.set_title("Boucle de réconciliation ArgoCD : état désiré vs état réel")
ax1.legend()
ax1.set_ylim(0, 6.5)

for t_d, t_f, val, t_c, _ in evenements:
    ax1.axvspan(t_d, t_c, alpha=0.12, color="#d62728")
    ax1.annotate("Dérive\ndétectée",
                 xy=(t_d + (t_c - t_d) / 2, val),
                 xytext=(t_d + (t_c - t_d) / 2 + 1, val + 0.8),
                 fontsize=8, color="#d62728",
                 arrowprops=dict(arrowstyle="->", color="#d62728", lw=1))

ecart = np.abs(actual - desired)
ax2.fill_between(temps, ecart, alpha=0.5, color="#d62728", label="Écart |réel − désiré|")
ax2.axhline(y=0, color="#2ca02c", linewidth=1.5, linestyle="--")
ax2.set_xlabel("Temps (minutes)")
ax2.set_ylabel("Écart absolu")
ax2.set_title("Ampleur de la dérive et corrections automatiques")
ax2.legend()

plt.show()
_images/ec409f718f88aead023c1e47043cee16f0454da51f98b5a72a4efb37a13d56dc.png
# Topologie multi-cluster ArgoCD avec NetworkX

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

G = nx.DiGraph()

# Noeuds : ArgoCD hub, clusters, applications
G.add_node("ArgoCD\n(hub)", type="hub")
G.add_node("Git\nRepository", type="git")

clusters = ["Cluster\nEU-West", "Cluster\nUS-East", "Cluster\nAP-South"]
for c in clusters:
    G.add_node(c, type="cluster")

apps_par_cluster = {
    "Cluster\nEU-West":  ["frontend-eu", "api-eu", "db-eu"],
    "Cluster\nUS-East":  ["frontend-us", "api-us", "db-us"],
    "Cluster\nAP-South": ["frontend-ap", "api-ap"],
}
for cluster, apps in apps_par_cluster.items():
    for app in apps:
        G.add_node(app, type="app")

# Arêtes
G.add_edge("Git\nRepository", "ArgoCD\n(hub)", label="watch")
for c in clusters:
    G.add_edge("ArgoCD\n(hub)", c, label="manage")
    for app in apps_par_cluster[c]:
        G.add_edge(c, app, label="deploy")

fig, ax = plt.subplots(figsize=(13, 8))
ax.axis("off")

pos = {
    "Git\nRepository": (-2, 0),
    "ArgoCD\n(hub)":   (0, 0),
    "Cluster\nEU-West":  (-1.5, -2.5),
    "Cluster\nUS-East":  (0,    -2.5),
    "Cluster\nAP-South": (1.5,  -2.5),
}
for cluster, apps in apps_par_cluster.items():
    cx, cy = pos[cluster]
    for i, app in enumerate(apps):
        offset = (i - (len(apps) - 1) / 2) * 0.7
        pos[app] = (cx + offset, cy - 1.8)

couleurs_types = {"hub": "#2ca02c", "git": "#ff7f0e", "cluster": "#4c72b0", "app": "#9ecae1"}
node_colors = [couleurs_types[G.nodes[n]["type"]] for n in G.nodes()]
node_sizes  = [2800 if G.nodes[n]["type"] in ("hub", "git") else
               1800 if G.nodes[n]["type"] == "cluster" else 900
               for n in G.nodes()]

nx.draw_networkx_nodes(G, pos, node_color=node_colors, node_size=node_sizes,
                       ax=ax, alpha=0.9)
nx.draw_networkx_labels(G, pos, font_size=8, font_color="white",
                        font_weight="bold", ax=ax)
nx.draw_networkx_edges(G, pos, ax=ax, arrows=True, arrowsize=18,
                       edge_color="#555555", width=1.5,
                       connectionstyle="arc3,rad=0.05")

legend_elements = [
    mpatches.Patch(color="#ff7f0e", label="Dépôt Git"),
    mpatches.Patch(color="#2ca02c", label="ArgoCD Hub"),
    mpatches.Patch(color="#4c72b0", label="Cluster Kubernetes"),
    mpatches.Patch(color="#9ecae1", label="Application"),
]
ax.legend(handles=legend_elements, loc="upper right", fontsize=10)
ax.set_title("Topologie multi-cluster ArgoCD : hub et ApplicationSets", fontsize=13)

plt.show()
_images/e5078920110a9e279b1269d12bcd7d6f503a54239b3229344762972a00bb1f2d.png
# Timeline de propagation GitOps
# commit → détection → sync → health check → ready

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

etapes = [
    ("Commit\nmerge",      0,   0.5,  "#4c72b0"),
    ("Détection\nArgoCD",  0.5, 1.5,  "#dd8452"),
    ("Génération\nmanifestes", 1.5, 2.5, "#55a868"),
    ("Apply\nKubernetes",  2.5, 4.0,  "#8172b2"),
    ("Rollout\nPods",      4.0, 7.0,  "#c44e52"),
    ("Health\ncheck",      7.0, 8.0,  "#64b5cd"),
    ("Sync OK\n✓",         8.0, 8.8,  "#2ca02c"),
]

fig, ax = plt.subplots(figsize=(13, 4))
ax.set_xlim(-0.5, 10)
ax.set_ylim(-0.5, 2.5)
ax.axis("off")

y_bar = 1.2
hauteur_barre = 0.5

for label, debut, fin, couleur in etapes:
    boite = FancyBboxPatch(
        (debut, y_bar - hauteur_barre / 2), fin - debut, hauteur_barre,
        boxstyle="round,pad=0.05",
        facecolor=couleur, edgecolor="white", linewidth=1.5, alpha=0.9
    )
    ax.add_patch(boite)
    milieu = (debut + fin) / 2
    ax.text(milieu, y_bar, label, ha="center", va="center",
            fontsize=8.5, color="white", fontweight="bold")
    ax.text(milieu, y_bar - 0.55, f"{fin - debut:.1f} min",
            ha="center", va="top", fontsize=7.5, color="#555555")

# Axe temporel
ax.annotate("", xy=(9.5, 0.4), xytext=(-0.3, 0.4),
            arrowprops=dict(arrowstyle="->", color="#333333", lw=1.5))
for t in range(0, 10):
    ax.axvline(x=t, ymin=0.12, ymax=0.28, color="#aaaaaa", linewidth=0.8)
    ax.text(t, 0.15, f"{t} min", ha="center", fontsize=7.5, color="#777777")

ax.set_title(
    "Timeline de propagation GitOps : du commit merge au déploiement sain",
    fontsize=13, pad=15
)

plt.show()
_images/32ad0640c0a4c9a15f3606c87a1ade39f6ed1602e0429835f07d406ab6c3a308.png

Résumé#

  1. Le GitOps définit Git comme seule source de vérité et garantit la traçabilité, la réversibilité et la collaboration via pull requests pour chaque modification d’infrastructure.

  2. Le pull model inverse la relation traditionnelle CI → cluster : l’agent ArgoCD tire les changements depuis Git, éliminant le besoin d’exposer des credentials de déploiement à l’extérieur du cluster.

  3. ArgoCD est composé de cinq services (Application Controller, Repo Server, API Server, Dex, Redis) aux responsabilités clairement séparées, permettant un scaling indépendant.

  4. L”Application ArgoCD combine une source (repo + path + revision), une destination (cluster + namespace) et une sync policy ; selfHeal: true garantit l’immutabilité de l’état désiré.

  5. L”ApplicationSet génère automatiquement des Applications via des générateurs (list, git, cluster), rendant le déploiement multi-cluster et multi-tenant déclaratif et sans duplication.

  6. Les AppProject isolent les équipes en limitant les dépôts sources, les namespaces de destination et les types de ressources Kubernetes autorisés, avec un RBAC granulaire.

  7. Les resource hooks (PreSync, PostSync, SyncFail) permettent d’orchestrer les migrations de base de données, les tests de smoke et les alertes sans sortir du paradigme GitOps.

  8. Les waves de synchronisation contrôlent l’ordre de création des ressources (namespace → secrets → deployments → services) et attendent la santé de chaque wave avant de progresser.

  9. Le pattern App of Apps permet de gérer ArgoCD lui-même avec GitOps : ajouter un service dans la flotte se réduit à créer un fichier YAML dans le dépôt.

  10. Flux v2 est l’alternative CNCF avec une architecture plus modulaire ; le choix entre ArgoCD et Flux dépend principalement du besoin en interface web et de la philosophie « operators-first » de l’équipe.