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 !

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 !

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

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 :
- Sync Status passe à
OutOfSync(ArgoCD a détecté le changement) - Puis automatiquement : ArgoCD lance la synchronisation
- 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 :
- Modification du code dans Git
- Commit et push
- ArgoCD détecte automatiquement (polling 3 min)
- ArgoCD synchronise Kubernetes avec Git
- Kubernetes déploie les changements
- 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 :
- Je chiffre le Secret avec
kubeseal(clé publique) - Je commit le Secret chiffré dans Git (sans risque)
- 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 :
- Je crée une Application spéciale qui pointe vers
argocd-apps/applications/ - Cette Application déploie automatiquement toutes les Applications définies dans ce dossier
- 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 !

Avantage de l'App of Apps
Maintenant, pour ajouter une nouvelle application :
- Je crée un fichier YAML dans
argocd-apps/applications/ - Je commit et push
- L'App of Apps détecte le nouveau fichier
- L'App of Apps crée automatiquement la nouvelle Application
- 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 :
- Modification dans Git
- Commit et push
- ArgoCD synchronise automatiquement (polling 3 min)
- Kubernetes applique les changements
- 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

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