Partie 7 : ArgoCD en action - Le GitOps workflow

Partie 7 : ArgoCD en action - Le GitOps workflow

Partie 7 : ArgoCD en action - Le GitOps workflow

17-20 décembre 2025

Le problème du certificat SSL

Dans la partie précédente, j'avais installé ArgoCD mais il n'était accessible que via port-forward à cause d'un problème de certificat SSL.

Rappel du problème : Le certificat wildcard-home-fonta-tls existe dans le namespace home-fonta, mais l'Ingress ArgoCD est dans le namespace argocd. Les Secrets Kubernetes ne traversent pas les namespaces.

Solution : Reflector

Reflector est un outil qui réplique automatiquement les ConfigMaps et Secrets entre namespaces Kubernetes.

Installation via Helm :

# Ajouter le repository Helm
helm repo add emberstack https://emberstack.github.io/helm-charts
helm repo update

# Installer Reflector
helm install reflector emberstack/reflector \
  --namespace reflector \
  --create-namespace

Vérification :

kubectl get pods -n reflector

Le pod Reflector doit être en Running.

Configuration de la réplication

Reflector fonctionne avec des annotations sur les Secrets. Il faut annoter le Secret source pour autoriser la réplication.

Annotation du certificat wildcard :

kubectl annotate secret wildcard-home-fonta-tls \
  -n home-fonta \
  reflector.v1.k8s.emberstack.com/reflection-allowed="true" \
  reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces="argocd,blog" \
  --overwrite

Création du Secret miroir dans argocd :

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
  name: wildcard-home-fonta-tls
  namespace: argocd
  annotations:
    reflector.v1.k8s.emberstack.com/reflects: "home-fonta/wildcard-home-fonta-tls"
type: Opaque
EOF

Reflector détecte l'annotation et copie automatiquement le contenu du Secret source vers le Secret miroir.

Vérification :

kubectl get secret wildcard-home-fonta-tls -n argocd

Le Secret existe maintenant dans argocd ! Et il sera automatiquement mis à jour quand cert-manager renouvellera le certificat.

Résultat

Maintenant, https://argo.home-fonta.fr fonctionne avec un certificat SSL valide !

argocd_home-fonta_cert.png

Première Application ArgoCD : home-fonta

Maintenant qu'ArgoCD est configuré, il est temps de créer notre première Application.

Qu'est-ce qu'une Application ArgoCD ?

Une Application ArgoCD est une Custom Resource qui décrit :

  • Où se trouve le code source (Git repo + path)
  • Où déployer (cluster + namespace)
  • Comment synchroniser (automatique ou manuel)

C'est la brique de base de GitOps avec ArgoCD.

Création de l'Application home-fonta

Structure du repository :

ansible-k3s/
├── argocd-apps/
│   └── home-fonta.yaml      # Définition de l'Application ArgoCD
└── helm-charts/
    └── homefonta/           # Helm chart de l'application
        ├── Chart.yaml
        ├── values.yaml
        └── templates/
            ├── deployment.yaml
            ├── service.yaml
            └── ingress.yaml

Fichier : argocd-apps/home-fonta.yaml

---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: home-fonta
  namespace: argocd
spec:
  project: default

  source:
    repoURL: git@github.com:Nikob2o/ansible-k3s.git
    targetRevision: HEAD
    path: helm-charts/homefonta
    helm:
      valueFiles:
      - values.yaml

  destination:
    server: https://kubernetes.default.svc
    namespace: home-fonta

  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
    - CreateNamespace=true

Explication des champs :

Champ Description
source.repoURL URL SSH du repository Git
source.targetRevision Branch à suivre (HEAD = main/master)
source.path Chemin vers le Helm chart
destination.server Cluster Kubernetes cible (in-cluster)
destination.namespace Namespace de déploiement
syncPolicy.automated.prune Supprime les ressources supprimées dans Git
syncPolicy.automated.selfHeal Corrige si modifié manuellement dans K8s
syncOptions.CreateNamespace Crée le namespace automatiquement

Suppression du déploiement Helm manuel

Avant de laisser ArgoCD gérer l'application, je dois supprimer le déploiement Helm manuel existant :

helm uninstall homefonta -n home-fonta

Ceci supprime tout ce qui avait été déployé via helm install.

Application de l'Application ArgoCD

kubectl apply -f argocd-apps/home-fonta.yaml

Résultat :

application.argoproj.io/home-fonta created

Vérification dans ArgoCD UI

Je me connecte sur https://argo.home-fonta.fr et je vois maintenant l'application home-fonta apparaître !

argocd_home-fonta_health.png

Statut de l'application :

  • Sync Status : Synced (Git et K8s sont identiques)
  • Health Status : Healthy (tous les pods sont Running)

Synchronisation via CLI

Vérification via CLI :

argocd app list

Résultat :

NAME        CLUSTER                         NAMESPACE   PROJECT  STATUS  HEALTH   SYNCPOLICY
home-fonta  https://kubernetes.default.svc  home-fonta  default  Synced  Healthy  Auto-Prune

Détails de l'application :

argocd app get home-fonta

argocd_home-fonta.png

Premier test GitOps : Modification des replicas

Le moment de vérité : est-ce que la synchronisation automatique fonctionne vraiment ?

Test : Augmenter le nombre de replicas

Avant : replicaCount: 1 dans helm-charts/homefonta/values.yaml

Je modifie le fichier :

replicaCount: 2

Commit et push :

git add helm-charts/homefonta/values.yaml
git commit -m "Test ArgoCD: augmentation replicas à 2"
git push

Observation dans ArgoCD

Par défaut, ArgoCD poll le repository Git toutes les 3 minutes. J'attends...

Après environ 2 minutes, je rafraîchis l'interface ArgoCD et je vois :

  1. Sync Status passe à OutOfSync (ArgoCD a détecté le changement)
  2. Puis automatiquement : ArgoCD lance la synchronisation
  3. Sync Status repasse à Synced

Dans Kubernetes :

kubectl get pods -n home-fonta

Résultat :

NAME                              READY   STATUS    RESTARTS   AGE
home-fonta-web-59c5486fb5-8x9kl   1/1     Running   0          3m
home-fonta-web-59c5486fb5-nl22t   1/1     Running   0          10s

2 pods maintenant ! ArgoCD a automatiquement appliqué le changement.

Zero commande kubectl nécessaire !

Le workflow GitOps validé

Le workflow complet fonctionne :

  1. Modification du code dans Git
  2. Commit et push
  3. ArgoCD détecte automatiquement (polling 3 min)
  4. ArgoCD synchronise Kubernetes avec Git
  5. Kubernetes déploie les changements
  6. Zéro intervention manuelle

C'est exactement ce que je voulais !

Migration des autres applications

Maintenant que le concept est validé, je vais migrer toutes mes applications vers ArgoCD.

Application 2 : Ghost Blog

Fichier : argocd-apps/ghost.yaml

---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: ghost
  namespace: argocd
spec:
  project: default

  source:
    repoURL: git@github.com:Nikob2o/ansible-k3s.git
    targetRevision: HEAD
    path: helm-charts/ghost
    helm:
      valueFiles:
      - values.yaml

  destination:
    server: https://kubernetes.default.svc
    namespace: blog

  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
    - CreateNamespace=true

Application :

kubectl apply -f argocd-apps/ghost.yaml

Le blog Ghost est maintenant géré par ArgoCD !

Application 3 : Infrastructure (cert-manager)

Cette application est particulière : elle gère l'infrastructure de base (namespace, ClusterIssuer cert-manager, Certificate wildcard).

Création du Helm chart infrastructure :

helm-charts/infrastructure/
├── Chart.yaml
├── values.yaml
└── templates/
    ├── namespace.yaml
    ├── clusterissuer.yaml
    └── certificate.yaml

Fichier : helm-charts/infrastructure/values.yaml

# Configuration de l'infrastructure K3s

namespace:
  name: home-fonta

letsencrypt:
  email: nikob2o@hotmail.fr
  server: https://acme-v02.api.letsencrypt.org/directory

certificate:
  name: wildcard-home-fonta
  secretName: wildcard-home-fonta-tls
  dnsNames:
    - "home-fonta.fr"
    - "*.home-fonta.fr"
  renewBefore: 720h  # 30 jours

Template Certificate avec annotations Reflector :

---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: {{ .Values.certificate.name }}
  namespace: {{ .Values.namespace.name }}
  annotations:
    reflector.v1.k8s.emberstack.com/reflection-allowed: "true"
    reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: "argocd,blog"
spec:
  secretName: {{ .Values.certificate.secretName }}
  issuerRef:
    name: letsencrypt-prod-dns
    kind: ClusterIssuer
  dnsNames:
    {{- range .Values.certificate.dnsNames }}
    - {{ . | quote }}
    {{- end }}
  renewBefore: {{ .Values.certificate.renewBefore }}

Application ArgoCD : argocd-apps/infrastructure.yaml

---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: infrastructure
  namespace: argocd
spec:
  project: default

  source:
    repoURL: git@github.com:Nikob2o/ansible-k3s.git
    targetRevision: HEAD
    path: helm-charts/infrastructure
    helm:
      valueFiles:
      - values.yaml

  destination:
    server: https://kubernetes.default.svc

  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
    - CreateNamespace=true
    - ServerSideApply=true

Note importante : Pas de namespace dans destination car les ressources sont dans différents namespaces (namespace home-fonta, ClusterIssuer cluster-wide, etc.).

Problème : Secret Cloudflare API Token

Le ClusterIssuer cert-manager a besoin du token API Cloudflare pour faire les challenges DNS-01.

Problème de sécurité : Je ne peux pas mettre le token en clair dans Git (repository public).

Solution : SealedSecrets

SealedSecrets : Chiffrer les secrets dans Git

SealedSecrets permet de chiffrer des Secrets Kubernetes avec une clé publique, et seul le contrôleur dans le cluster a la clé privée pour les déchiffrer.

Principe :

  1. Je chiffre le Secret avec kubeseal (clé publique)
  2. Je commit le Secret chiffré dans Git (sans risque)
  3. Le contrôleur SealedSecrets déchiffre et crée le Secret dans K8s

Installation de SealedSecrets

Contrôleur Kubernetes :

kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.27.1/controller.yaml

Vérification :

kubectl get pods -n kube-system | grep sealed-secrets

CLI kubeseal :

cd /tmp
wget https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.27.1/kubeseal-0.27.1-linux-amd64.tar.gz
tar -xvzf kubeseal-0.27.1-linux-amd64.tar.gz
sudo install -m 755 kubeseal /usr/local/bin/kubeseal

# Vérification
kubeseal --version

Chiffrement du token Cloudflare

Récupération du Secret existant :

kubectl get secret cloudflare-api-token -n cert-manager -o yaml > /tmp/cloudflare-secret.yaml

Chiffrement avec kubeseal :

kubeseal -f /tmp/cloudflare-secret.yaml \
  -w helm-charts/infrastructure/templates/cloudflare-sealed-secret.yaml

Le fichier généré contient le Secret chiffré :

---
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: cloudflare-api-token
  namespace: cert-manager
spec:
  encryptedData:
    api-token: AgAsP/j4LsjCFxHQp+GK68Nnrs4wh7VUAl4aLTQwZ4VDT92uvhvGc...
  template:
    metadata:
      name: cloudflare-api-token
      namespace: cert-manager
    type: Opaque

Ce fichier peut être committé dans Git sans risque ! La valeur encryptedData est chiffrée et inutilisable sans la clé privée du contrôleur.

Backup de la clé privée SealedSecrets

IMPORTANT : Si je perds la clé privée SealedSecrets, je ne pourrai plus déchiffrer mes secrets.

Backup de la clé :

kubectl get secret sealed-secrets-keyfqmqg -n kube-system \
  -o yaml > ~/sealed-secrets-private-key-BACKUP.yaml

Ce fichier doit être stocké en sécurité (Passbolt, coffre-fort, etc.), PAS dans Git.

Application du SealedSecret

kubectl apply -f helm-charts/infrastructure/templates/cloudflare-sealed-secret.yaml

Le contrôleur SealedSecrets détecte le SealedSecret, le déchiffre et crée automatiquement le Secret cloudflare-api-token dans le namespace cert-manager.

Vérification :

kubectl get secret cloudflare-api-token -n cert-manager

Le Secret existe et est utilisable par cert-manager !

Application 4 : External Services

Cette application gère les services externes (Plex, Passbolt, Fonas, arr-stack) via des Endpoints Kubernetes.

Principe : Créer des Services Kubernetes qui pointent vers des IPs externes au cluster.

Exemple pour Plex :

Endpoints : helm-charts/external-services/templates/plex-endpoints.yaml

---
apiVersion: v1
kind: Endpoints
metadata:
  name: plex-external
  namespace: {{ .Values.namespace }}
subsets:
  - addresses:
      - ip: {{ .Values.plex.ip }}
    ports:
      - port: {{ .Values.plex.port }}
        name: http

Service : helm-charts/external-services/templates/plex-service.yaml

---
apiVersion: v1
kind: Service
metadata:
  name: plex-external
  namespace: {{ .Values.namespace }}
spec:
  ports:
    - port: 80
      targetPort: {{ .Values.plex.port }}
      protocol: TCP
      name: http

Ingress : helm-charts/external-services/templates/plex-ingress.yaml

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: plex-ingress
  namespace: {{ .Values.namespace }}
  annotations:
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
    cert-manager.io/cluster-issuer: "letsencrypt-prod-dns"
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - {{ .Values.plex.host }}
    secretName: wildcard-home-fonta-tls
  rules:
  - host: {{ .Values.plex.host }}
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: plex-external
            port:
              number: 80

Values : helm-charts/external-services/values.yaml

namespace: home-fonta

plex:
  ip: "192.168.1.18"
  port: 32400
  host: "plex.home-fonta.fr"

passbolt:
  ip: "192.168.1.18"
  port: 8006
  host: "passbolt.home-fonta.fr"

# etc...

Application ArgoCD :

kubectl apply -f argocd-apps/external-services.yaml

Tous mes services externes sont maintenant accessibles via des sous-domaines avec HTTPS !

Le pattern App of Apps

À ce stade, j'ai 4 Applications ArgoCD :

  • home-fonta
  • ghost
  • infrastructure
  • external-services

Chaque fois que je veux ajouter une application, je dois faire kubectl apply -f argocd-apps/xxx.yaml.

Problème : Ça casse le principe GitOps (je fais encore des commandes manuelles).

Solution : Le pattern "App of Apps"

Qu'est-ce qu'une App of Apps ?

Une App of Apps est une Application ArgoCD qui gère d'autres Applications ArgoCD.

Principe :

  1. Je crée une Application spéciale qui pointe vers argocd-apps/applications/
  2. Cette Application déploie automatiquement toutes les Applications définies dans ce dossier
  3. Quand j'ajoute un nouveau fichier YAML dans argocd-apps/applications/, l'App of Apps le détecte et le déploie automatiquement

Structure du repository

ansible-k3s/
├── argocd-apps/
│   ├── app-of-apps.yaml          # L'App of Apps
│   └── applications/              # Applications individuelles
│       ├── home-fonta.yaml
│       ├── ghost.yaml
│       ├── infrastructure.yaml
│       └── external-services.yaml

Création de l'App of Apps

Fichier : argocd-apps/app-of-apps.yaml

---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: app-of-apps
  namespace: argocd
  finalizers:
  - resources-finalizer.argocd.argoproj.io
spec:
  project: default

  source:
    repoURL: git@github.com:Nikob2o/ansible-k3s.git
    targetRevision: HEAD
    path: argocd-apps/applications
    directory:
      recurse: false

  destination:
    server: https://kubernetes.default.svc
    namespace: argocd

  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
    - CreateNamespace=true

Différences avec une Application normale :

Champ Valeur Explication
source.path argocd-apps/applications Pointe vers un dossier, pas un chart
source.directory.recurse false Ne pas chercher dans les sous-dossiers
destination.namespace argocd Les Applications sont des ressources ArgoCD
finalizers resources-finalizer... Supprime les apps enfants avant l'App of Apps

Migration vers App of Apps

Suppression des Applications existantes :

kubectl delete application infrastructure ghost home-fonta external-services -n argocd

Déplacement des fichiers :

mv argocd-apps/home-fonta.yaml argocd-apps/applications/
mv argocd-apps/ghost.yaml argocd-apps/applications/
mv argocd-apps/infrastructure.yaml argocd-apps/applications/
mv argocd-apps/external-services.yaml argocd-apps/applications/

Application de l'App of Apps :

kubectl apply -f argocd-apps/app-of-apps.yaml

Vérification :

kubectl get applications -n argocd

Résultat :

NAME               SYNC STATUS   HEALTH STATUS
app-of-apps        Synced        Healthy
home-fonta         Synced        Healthy
ghost              Synced        Healthy
infrastructure     Synced        Healthy
external-services  Synced        Healthy

Toutes les applications sont automatiquement créées par l'App of Apps !

argocd_appofapps.png

Avantage de l'App of Apps

Maintenant, pour ajouter une nouvelle application :

  1. Je crée un fichier YAML dans argocd-apps/applications/
  2. Je commit et push
  3. L'App of Apps détecte le nouveau fichier
  4. L'App of Apps crée automatiquement la nouvelle Application
  5. La nouvelle Application se synchronise

100% GitOps, zéro commande kubectl !

Test de rollback Git

Un des gros avantages de GitOps : le rollback est trivial.

Simulation d'erreur

Je modifie helm-charts/infrastructure/values.yaml et je mets une adresse email invalide :

letsencrypt:
  email: test@test.test  # Email invalide

Commit et push :

git add helm-charts/infrastructure/values.yaml
git commit -m "Test rollback"
git push

ArgoCD synchronise et applique le changement. Le ClusterIssuer est modifié avec l'email invalide.

Vérification :

kubectl get clusterissuer letsencrypt-prod-dns -o yaml | grep email

Résultat :

email: test@test.test

Rollback via Git

Pour revenir en arrière, je fais simplement un git revert :

git revert HEAD --no-edit
git push

ArgoCD détecte le nouveau commit, synchronise et restaure l'email correct !

kubectl get clusterissuer letsencrypt-prod-dns -o yaml | grep email

Résultat :

email: nikob2o@hotmail.fr

Rollback en moins de 3 minutes, sans aucune commande kubectl !

C'est la puissance de GitOps.

État final de l'infrastructure

Après cette migration complète vers ArgoCD, voici l'état de mon infrastructure :

Applications gérées par ArgoCD :

  • home-fonta (site Flask)
  • ghost (blog)
  • infrastructure (cert-manager, certificats)
  • external-services (Plex, Passbolt, Fonas, arr-stack)

Outils d'infrastructure :

  • ArgoCD (GitOps)
  • Reflector (réplication de secrets entre namespaces)
  • SealedSecrets (chiffrement de secrets dans Git)
  • cert-manager (certificats Let's Encrypt)
  • nginx-ingress-controller (reverse proxy)

Workflow GitOps complet :

  1. Modification dans Git
  2. Commit et push
  3. ArgoCD synchronise automatiquement (polling 3 min)
  4. Kubernetes applique les changements
  5. Zéro commande manuelle

Sécurité :

  • Clé SSH dédiée pour ArgoCD (lecture seule)
  • Secrets chiffrés avec SealedSecrets
  • Certificats SSL automatiques (Let's Encrypt)
  • Accès HTTPS uniquement
!

argocd_5applis.png

Leçons apprises

GitOps change tout

Avant GitOps :

  • Commandes kubectl et helm manuelles
  • Pas d'historique
  • Dérive de configuration inévitable
  • Rollback compliqué

Après GitOps :

  • Git = source de vérité unique
  • Historique complet (qui, quoi, quand, pourquoi)
  • Pas de dérive (auto-correction)
  • Rollback = git revert

Le pattern App of Apps est puissant

L'App of Apps transforme ArgoCD en un système complètement déclaratif :

  • Ajout d'application = fichier YAML dans Git
  • Suppression d'application = suppression du fichier
  • Modification d'application = modification du fichier

Aucune commande manuelle nécessaire.

SealedSecrets : Sécurité sans compromis

SealedSecrets permet de stocker des secrets dans Git en toute sécurité :

  • Secrets chiffrés avec une clé publique
  • Seul le contrôleur K8s a la clé privée
  • Commit et push sans risque

IMPORTANT : Backup de la clé privée SealedSecrets indispensable !

Reflector : Partage de secrets entre namespaces

Reflector simplifie le partage de secrets entre namespaces :

  • Certificats SSL automatiquement répliqués
  • Mise à jour automatique lors du renouvellement
  • Annotations simples à configurer

Alternative : Kubed (plus de features, plus complexe)

ArgoCD UI : Visibilité totale

L'interface ArgoCD donne une visibilité complète sur l'état du cluster :

  • Applications synchronisées ou non
  • Santé des ressources
  • Historique des synchronisations
  • Diff entre Git et K8s
  • Logs et événements

Métriques de succès

Métrique Avant Après
Déploiement d'une app 10 commandes manuelles 1 commit Git
Rollback 5-10 min (recherche commandes) 1 min (git revert)
Visibilité de l'état kubectl get everything ArgoCD UI
Historique des changements Aucun Git log complet
Dérive de configuration Inévitable Impossible (auto-heal)
Secrets dans Git Dangereux Sécurisé (SealedSecrets)

Prochaines étapes

L'infrastructure GitOps est en place, mais il reste encore des améliorations possibles :

Phase 3 - CI/CD :

  • GitHub Actions pour build automatique des images Docker
  • Tests automatiques avant déploiement
  • Déploiement automatique sur merge dans main

Phase 4 - Monitoring :

  • Prometheus + Grafana pour les métriques
  • Loki pour les logs centralisés
  • Alertmanager pour les alertes

Phase 5 - Haute disponibilité :

  • Résolution du problème CNI (pods sur différents nodes)
  • Load balancing intelligent
  • Backup automatique des données

Le voyage DevOps continue...


Fin de la série ArgoCD. Prochaine partie : CI/CD avec GitHub Actions

Read more