Trois jours après avoir mis en place le cluster K3s, j’avais déjà fait plusieurs kubectl apply -f manuels pour tester des déploiements. Le problème avec kubectl apply direct, c’est que l’état réel du cluster diverge silencieusement de ce que tu as sur le disque. Tu modifies un manifest localement, tu oublies d’appliquer, ou tu appliques un fix rapide en ligne de commande et tu n’écris jamais la correction dans le fichier. Deux semaines plus tard, kubectl get deployment vaultwarden -o yaml ne ressemble plus à ton deployment.yaml local. GitOps résout ça en faisant du dépôt Git la source de vérité unique.
Prérequis et choix d’outil
Le cluster K3s décrit dans l’article précédent est la base : 1 master (VM 204, 10.10.30.204, 4 vCPU, 4 GB RAM) + 2 workers (VM 205, VM 206, 2 vCPU, 2 GB RAM chacun), Longhorn installé comme StorageClass par défaut.
Pour le GitOps, deux outils dominent : Flux CD et ArgoCD. Flux est plus léger en RAM (~100 MB) et intégralement piloté par la CLI et les CRDs. ArgoCD est plus lourd (~500 MB) mais il embarque une interface web qui permet de visualiser l’état de synchronisation de chaque application – utile pour comprendre ce qui se passe sans avoir à mémoriser les commandes argocd à la main. Pour un premier GitOps, l’UI d’ArgoCD réduit la friction d’apprentissage. J’ai pris ArgoCD.
Le dépôt Git cible est Gitea, auto-hébergé sur LXC 104 (gitea.homelab.local, IP 10.10.30.104). L’avantage de Gitea en local : les secrets que je pousse dans ce dépôt ne transitent pas par GitHub. La configuration ArgoCD et les manifests Kubernetes vivent dans un dépôt privé sur cette instance.
Installation d’ArgoCD
ArgoCD s’installe via ses manifests officiels. À la date de rédaction, la version stable est 2.12.3 :
kubectl create namespace argocd
kubectl apply -n argocd
-f https://raw.githubusercontent.com/argoproj/argo-cd/v2.12.3/manifests/install.yaml
L’installation déploie une dizaine de composants dans le namespace argocd : le serveur API, le controller d’application, le repo-server, le Redis, le Dex (SSO), et l’ApplicationSet controller. Suivi de l’état :
kubectl -n argocd get pods --watch
Tous les pods doivent passer à Running en 3 à 5 minutes. Si argocd-redis reste en Pending, vérifie que Longhorn peut provisionner un PVC – c’est souvent open-iscsi absent d’un nœud qui bloque.
Récupérer le mot de passe admin initial :
kubectl -n argocd get secret argocd-initial-admin-secret
-o jsonpath="{.data.password}" | base64 -d && echo
Ce mot de passe est généré aléatoirement à l’installation. Conserve-le, tu en auras besoin pour la première connexion. Supprime le secret argocd-initial-admin-secret après avoir changé le mot de passe (argocd account update-password).
Accès à l’UI : port-forward puis Ingress Traefik
Pour la première connexion, port-forward direct :
kubectl -n argocd port-forward svc/argocd-server 8443:443
L’UI est accessible sur https://localhost:8443. Login : admin / mot de passe récupéré ci-dessus. ArgoCD présente une interface vide – aucune application n’est encore configurée.
Pour un accès permanent depuis le réseau homelab, un Ingress Traefik. Traefik est installé par défaut dans K3s et gère les Ingress. Le TLS Let’s Encrypt nécessite que l’Ingress soit joignable depuis Internet sur le port 443, ce que je n’expose pas pour ArgoCD – j’utilise un certificat auto-signé généré par cert-manager pour l’accès interne uniquement.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: argocd-server
namespace: argocd
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
spec:
rules:
- host: argocd.homelab.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: argocd-server
port:
number: 443
tls:
- hosts:
- argocd.homelab.local
ArgoCD par défaut refuse les connexions non sécurisées entre lui-même et son Ingress. Il faut lui dire de ne pas insister sur TLS en backend, car Traefik termine le TLS en amont :
kubectl -n argocd patch configmap argocd-cmd-params-cm
--type merge
-p '{"data":{"server.insecure":"true"}}'
kubectl -n argocd rollout restart deployment argocd-server
Après ça, argocd.homelab.local est joignable depuis n’importe quel client du réseau homelab. Pi-hole résout ce nom vers 10.10.30.204 (IP du master K3s qui porte l’Ingress Traefik).
Structure du dépôt GitOps sur Gitea
Sur Gitea, je crée un dépôt privé homelab-gitops. Structure retenue :
homelab-gitops/
├── apps/
│ ├── vaultwarden/
│ │ ├── namespace.yaml
│ │ ├── deployment.yaml
│ │ ├── service.yaml
│ │ ├── ingress.yaml
│ │ └── sealed-secret.yaml
│ ├── uptime-kuma/
│ │ ├── namespace.yaml
│ │ ├── deployment.yaml
│ │ ├── service.yaml
│ │ └── ingress.yaml
│ └── monitoring/
│ └── ...
└── argocd-apps/
├── vaultwarden-app.yaml
└── uptime-kuma-app.yaml
Le dossier apps/ contient les manifests Kubernetes de chaque service. Le dossier argocd-apps/ contient les objets Application ArgoCD qui pointent vers chaque dossier dans apps/. Cette séparation permet de versionner les Applications ArgoCD elles-mêmes dans Git – c’est le pattern « App of Apps ».
Connecter ArgoCD à Gitea
C’est là que ça a coincé. Gitea sur LXC 104 utilise un certificat auto-signé (généré par Caddy en mode HTTPS local). ArgoCD refuse de se connecter à un dépôt dont le certificat n’est pas signé par une CA connue.
La mauvaise solution, parfois suggérée dans des tutoriels rapides, est --insecure-skip-server-verification dans la configuration du dépôt ArgoCD. Elle désactive toute vérification TLS – inutile pour un homelab qui a pris la peine de faire du HTTPS.
La bonne solution : ajouter le certificat CA de Gitea dans le ConfigMap argocd-tls-certs-cm. D’abord, récupérer le certificat :
openssl s_client -connect gitea.homelab.local:443 -showcerts /dev/null
| openssl x509 -outform PEM > gitea-ca.crt
Vérifier que le certificat récupéré correspond bien à la CA (et pas juste au certificat serveur) :
openssl x509 -in gitea-ca.crt -text -noout | grep "CA:TRUE"
Patcher le ConfigMap :
kubectl -n argocd create configmap argocd-tls-certs-cm
--from-file=gitea.homelab.local=gitea-ca.crt
--dry-run=client -o yaml | kubectl apply -f -
Si ton Gitea est derrière Caddy avec un certificat issu d’une CA interne (comme step-ca), c’est le certificat root de ta CA interne qu’il faut ajouter, pas le certificat serveur.
Redémarrer le repo-server pour prendre en compte la nouvelle CA :
kubectl -n argocd rollout restart deployment argocd-repo-server
Ensuite, ajouter le dépôt Gitea via la CLI ArgoCD. Installer la CLI localement d’abord :
curl -sSL -o /usr/local/bin/argocd
https://github.com/argoproj/argo-cd/releases/download/v2.12.3/argocd-linux-amd64
chmod +x /usr/local/bin/argocd
argocd login argocd.homelab.local --username admin --password
argocd repo add https://gitea.homelab.local/pascal/homelab-gitops
--username pascal
--password
Si la connexion réussit, argocd repo list affiche le dépôt avec le statut Successful. Si tu obtiens x509: certificate signed by unknown authority, c’est que le CA n’a pas été pris en compte – vérifie le ConfigMap et le rollout du repo-server.
Première Application ArgoCD : Vaultwarden
Le manifest argocd-apps/vaultwarden-app.yaml dans le dépôt Git :
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: vaultwarden
namespace: argocd
spec:
project: default
source:
repoURL: https://gitea.homelab.local/pascal/homelab-gitops
targetRevision: main
path: apps/vaultwarden
destination:
server: https://kubernetes.default.svc
namespace: vaultwarden
syncPolicy:
syncOptions:
- CreateNamespace=true
Je commence avec une sync policy manuelle (pas d’automated block) pour comprendre avant d’automatiser. Appliquer l’objet Application :
kubectl apply -f argocd-apps/vaultwarden-app.yaml
Dans l’UI ArgoCD, l’application apparaît en statut OutOfSync – le cluster ne correspond pas encore à l’état du dépôt. Cliquer « Sync » ou lancer :
argocd app sync vaultwarden
ArgoCD crée le namespace vaultwarden, applique les manifests dans apps/vaultwarden/, et passe en statut Synced / Healthy. L’UI montre l’arbre des ressources : Deployment, Service, Ingress, PersistentVolumeClaim (Longhorn). Si un pod reste en Pending, le détail des events est visible directement dans l’UI sans kubectl describe.
Gestion des secrets avec Sealed Secrets
Le problème des secrets Kubernetes dans Git : un Secret est encodé en base64, pas chiffré. Si ton dépôt est public (ou si quelqu’un accède à ton Gitea), tous tes secrets sont lisibles. Sealed Secrets résout ça : tu chiffres le secret avec la clé publique du contrôleur dans le cluster, et seul ce contrôleur peut déchiffrer.
Installer le contrôleur Sealed Secrets via Helm :
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm repo update
helm install sealed-secrets sealed-secrets/sealed-secrets
--namespace kube-system
--version 2.16.1
Installer kubeseal localement :
curl -sSL -o /usr/local/bin/kubeseal
https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.27.1/kubeseal-0.27.1-linux-amd64
chmod +x /usr/local/bin/kubeseal
Pour chiffrer un secret, d’abord récupérer la clé publique du contrôleur :
kubeseal --fetch-cert
--controller-name=sealed-secrets
--controller-namespace=kube-system
> sealed-secrets-pub.crt
Créer un secret Kubernetes temporaire (en mémoire, jamais écrit en clair sur disque) et le chiffrer :
kubectl create secret generic vaultwarden-env
--namespace vaultwarden
--from-literal=DATABASE_URL="sqlite:///data/db.sqlite3"
--from-literal=ADMIN_TOKEN="$(openssl rand -base64 48)"
--dry-run=client -o yaml
| kubeseal --cert sealed-secrets-pub.crt
--format yaml > apps/vaultwarden/sealed-secret.yaml
Le fichier sealed-secret.yaml généré contient un objet SealedSecret avec les données chiffrées. Tu peux le committer dans Git en toute sécurité – sans la clé privée du contrôleur dans le cluster, le secret est illisible.
La clé privée Sealed Secrets est dans le secret Kubernetes sealed-secrets-key dans kube-system. Si tu reconstruis le cluster sans restaurer cette clé, tous tes SealedSecrets précédemment commités deviennent indéchiffrables. Sauvegarde ce secret dans un coffre-fort externe (Vaultwarden par exemple, une fois qu’il tourne).
Quand ArgoCD synchronise apps/vaultwarden/, il applique le SealedSecret. Le contrôleur Sealed Secrets le détecte, le déchiffre, et crée le Secret Kubernetes natif correspondant. Le pod Vaultwarden monte ce secret via envFrom. Tout ça sans que le secret en clair n’ait jamais touché le dépôt Git.
Activation de l’auto-sync
Une fois que tu comprends le flux (Git → ArgoCD → cluster), tu peux activer la synchronisation automatique. Modifier argocd-apps/vaultwarden-app.yaml :
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
prune: true supprime les ressources dans le cluster qui ont été supprimées du dépôt Git. selfHeal: true re-synchronise si quelqu’un modifie manuellement une ressource dans le cluster (drift detection). Avec ces deux options, le cluster revient toujours à l’état du dépôt, que ce soit après une modification manuelle accidentelle ou après un redémarrage.
Active selfHeal progressivement. Si tu as des opérateurs qui modifient des ressources à la volée (cert-manager qui ajoute des annotations, Longhorn qui met à jour des labels), ArgoCD va boucler en détectant un drift et en re-synchronisant en permanence. Dans ce cas, configure des ignoreDifferences dans la spec Application pour ignorer les champs gérés par ces opérateurs.
Résultats
Vaultwarden et Uptime Kuma sont déployés et synchronisés via ArgoCD depuis le dépôt Gitea. L’UI ArgoCD affiche :
- vaultwarden :
Synced / Healthy, 1 replica enRunning, PVC Longhorn de 10 GB attaché sur k3s-worker-1. - uptime-kuma :
Synced / Healthy, 1 replica enRunning, PVC Longhorn de 5 GB attaché sur k3s-worker-2.
Quand je pousse un changement dans apps/vaultwarden/deployment.yaml sur Gitea (par exemple bumper l’image de vaultwarden/server:1.32.0 à 1.32.1), ArgoCD détecte le diff dans les 3 minutes (polling interval par défaut), affiche le statut OutOfSync, et applique automatiquement le changement si l’auto-sync est actif. Le rolling update se fait sans interruption de service grâce aux réplicas et à la probe readiness.
La consommation mémoire d’ArgoCD dans le cluster (namespace argocd, tous pods cumulés) : environ 520 MB idle avec 2 applications gérées. C’est la principale différence avec Flux, qui tiendrait dans 120 MB pour le même résultat. Sur 32 GB disponibles dans heighliner, ce n’est pas un problème, mais il faut en être conscient avant d’installer ArgoCD sur un nœud plus contraint.
Ce que je vais faire ensuite : migrer Nextcloud depuis son LXC Docker vers K3s et GitOps. Nextcloud est plus complexe – base de données séparée, volumes persistants pour les données utilisateurs, configuration Redis pour le cache. C’est exactement le type de cas où la visibilité de l’UI ArgoCD et la reproductibilité du dépôt Git vont aider.
slug: argocd-k3s-gitops
meta: Installer ArgoCD sur K3s, connecter Gitea auto-hébergé, gérer les secrets avec Sealed Secrets et déployer des apps homelab en GitOps. (~155 chars)
tags: argocd, k3s, gitops, gitea, sealed-secrets, kubernetes, traefik