---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# 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.

```{admonition} Dérive de configuration
:class: warning
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

```yaml
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.

```{admonition} Bonnes pratiques de synchronisation
:class: tip
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

```yaml
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.

```yaml
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 :

```yaml
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 :

```yaml
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 :

```yaml
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.

```yaml
# 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

```bash
# 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).

```yaml
# 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) |

```{admonition} ArgoCD vs Flux v2
:class: note
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

```{code-cell} python3
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)
```

```{code-cell} python3
# 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()
```

```{code-cell} python3
# 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()
```

```{code-cell} python3
# 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é

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.
