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 :
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
L’état désiré est versionné dans Git : chaque changement est un commit, avec un auteur, une date et un message
Les changements approuvés sont appliqués automatiquement : un agent surveille Git et réconcilie l’état réel avec l’état désiré
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é sansselfHealcar 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()
# 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()
# 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()
Résumé#
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.
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.
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.
L”
ApplicationArgoCD combine une source (repo + path + revision), une destination (cluster + namespace) et une sync policy ;selfHeal: truegarantit l’immutabilité de l’état désiré.L”
ApplicationSetgé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.Les
AppProjectisolent 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.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.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.
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.
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.