GitOps mit ArgoCD — Ein praktischer Leitfaden für deklarative Deployments
Das Deployment-Problem, über das niemand spricht
Deine CI-Pipeline baut das Image. Und dann? Jemand führt kubectl apply aus. Oder ein Helm-Befehl, der in einem Deploy-Step steckt. Oder ein Skript, das per SSH auf einen Bastion-Host geht. Drei Monate später weiß keiner mehr, was tatsächlich in Production läuft — weil der Cluster-State von dem abgedriftet ist, was in Git steht.
GitOps löst das Problem, indem Git zur einzigen Wahrheitsquelle für den Cluster-State wird. ArgoCD ist der meistgenutzte GitOps-Operator für Kubernetes — deklarativ, auto-sync-fähig, und es zeigt Dir genau, wo Realität und Soll-Zustand auseinanderlaufen.
Dieser Leitfaden zeigt die Einrichtung für echte Workloads, nicht die Hello-World-Version.
Was GitOps tatsächlich bedeutet
GitOps ist ein Deployment-Pattern mit vier Prinzipien, formalisiert vom OpenGitOps-Projekt:
- Deklarativ — der gesamte gewünschte Systemzustand wird deklarativ beschrieben (YAML, Helm Charts, Kustomize Overlays)
- Versioniert und unveränderlich — der Soll-Zustand liegt in Git, Audit-Trail und Rollback gibt’s gratis
- Automatisch gezogen — ein Agent (ArgoCD) gleicht den Cluster-State kontinuierlich mit Git ab
- Kontinuierlich abgeglichen — Drift wird erkannt und korrigiert, ohne manuelles Eingreifen
Der entscheidende Unterschied: Deine CI-Pipeline fasst den Cluster nicht mehr an. Sie baut und pusht ein Image, aktualisiert dann ein Manifest in Git. ArgoCD erledigt den Rest.
ArgoCD installieren
Die empfohlene Installation nutzt das offizielle Helm Chart. Für Production das HA-Manifest verwenden:
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/ha/install.yaml
Oder via Helm für mehr Kontrolle:
helm repo add argo https://argoproj.github.io/argo-helm
helm install argocd argo/argo-cd \
--namespace argocd \
--create-namespace \
--set server.extraArgs={--insecure} \
--set configs.params."server\.insecure"=true
Das --insecure-Flag deaktiviert TLS auf dem ArgoCD-Server selbst — TLS wird am Ingress-Controller terminiert, was dem Standard-Pattern entspricht. ArgoCD niemals ohne TLS-Terminierung irgendwo in der Kette exponieren.
Initiales Admin-Passwort abrufen:
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath="{.data.password}" | base64 -d
Sofort ändern. Noch besser: SSO via OIDC konfigurieren und den Admin-Account komplett deaktivieren.
Repository-Struktur, die skaliert
Der häufigste Fehler: Application-Manifeste im selben Repo wie der Application-Code. Das erzeugt zirkuläre Abhängigkeiten — eine Code-Änderung triggert CI, die Manifeste aktualisiert, was wiederum CI triggert.
Nutze ein dediziertes Config-Repository:
infra-gitops/
├── apps/
│ ├── api/
│ │ ├── base/
│ │ │ ├── deployment.yaml
│ │ │ ├── service.yaml
│ │ │ └── kustomization.yaml
│ │ └── overlays/
│ │ ├── staging/
│ │ │ ├── kustomization.yaml
│ │ │ └── replicas-patch.yaml
│ │ └── production/
│ │ ├── kustomization.yaml
│ │ └── replicas-patch.yaml
│ └── frontend/
│ ├── base/
│ └── overlays/
├── platform/
│ ├── cert-manager/
│ ├── ingress-nginx/
│ └── monitoring/
└── argocd/
├── projects.yaml
└── applicationsets.yaml
Wichtige Entscheidungen:
- Kustomize statt Helm für eigene App-Manifeste — Helm ist großartig für Third-Party-Charts. Für eigene Services sind Kustomize Overlays einfacher im PR zu reviewen und leichter zu diffen.
apps/vonplatform/trennen — Platform-Komponenten (cert-manager, Ingress, Monitoring) haben andere Change-Kadenzen und Approval-Anforderungen.- Ein Verzeichnis pro Environment-Overlay — macht Promotion explizit und nachvollziehbar.
Application-Manifeste
Eine ArgoCD Application-Ressource sagt dem Controller, was wo deployed werden soll:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: api-staging
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/your-org/infra-gitops.git
targetRevision: main
path: apps/api/overlays/staging
destination:
server: https://kubernetes.default.svc
namespace: api
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
Die kritischen Einstellungen:
automated.prune: true— entfernt Ressourcen, die nicht mehr in Git existieren. Ohne das bleiben gelöschte Manifeste als verwaiste Ressourcen im Cluster.automated.selfHeal: true— macht manuellekubectl-Änderungen rückgängig. Das ist der ganze Sinn von GitOps — wenn jemand etwas per Hand patcht, korrigiert ArgoCD es.retry— transiente Fehler passieren (API-Server-Überlastung, Webhook-Timeouts). Exponentielles Backoff verhindert kaskadierende Retries.
ApplicationSets für Multi-Environment
Individuelle Application-Ressourcen pro Service pro Environment skalieren nicht. ApplicationSets generieren Applications aus Templates:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: apps
namespace: argocd
spec:
goTemplate: true
goTemplateOptions: ["missingkey=error"]
generators:
- git:
repoURL: https://github.com/your-org/infra-gitops.git
revision: main
directories:
- path: apps/*/overlays/*
template:
metadata:
name: '{{ index .path.segments 1 }}-{{ index .path.segments 3 }}'
spec:
project: default
source:
repoURL: https://github.com/your-org/infra-gitops.git
targetRevision: main
path: '{{ .path.path }}'
destination:
server: https://kubernetes.default.svc
namespace: '{{ index .path.segments 1 }}'
syncPolicy:
automated:
prune: true
selfHeal: true
Das entdeckt automatisch jedes apps/<service>/overlays/<env>-Verzeichnis und erstellt eine Application dafür. Das goTemplate: true-Flag aktiviert Go-Template-Syntax, den empfohlenen Modus für neue ApplicationSets. Neuen Service hinzufügen? Verzeichnisstruktur anlegen, pushen, fertig.
Secrets-Management
Das Einzige, was nicht im Klartext in Git darf: Secrets. Drei tragfähige Ansätze:
Sealed Secrets
Bitnami Sealed Secrets verschlüsselt Secrets client-seitig mit einem cluster-spezifischen Public Key. Nur der Controller im Cluster kann entschlüsseln.
kubeseal --format yaml < secret.yaml > sealed-secret.yaml
Das Sealed Secret kann sicher committet werden. Einfach, aber Key-Rotation erfordert Neuverschlüsselung aller Secrets.
External Secrets Operator
External Secrets Operator synchronisiert Secrets aus externen Stores (AWS Secrets Manager, Vault, GCP Secret Manager) in Kubernetes Secrets:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: api-secrets
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets
kind: ClusterSecretStore
target:
name: api-secrets
data:
- secretKey: DATABASE_URL
remoteRef:
key: /production/api/database-url
Das ist der empfohlene Ansatz für Production. Secrets liegen in einem dedizierten Secrets Manager mit eigenen Access-Policies, Audit-Logging und Rotation. Das Kubernetes Secret ist abgeleitet, nicht die Quelle der Wahrheit.
SOPS
Mozilla SOPS verschlüsselt einzelne Werte in YAML-Dateien. ArgoCD hat nativen SOPS-Support via Plugin. Guter Mittelweg, wenn kein externer Secrets Manager gewünscht ist.
Environment-Promotion
Der Promotion-Flow in GitOps:
- CI baut Image
api:sha-abc123und pusht in die Registry - CI öffnet einen PR gegen das Config-Repo, der den Image-Tag im Staging-Overlay aktualisiert
- ArgoCD synct Staging automatisch
- Nach Validierung aktualisiert ein zweiter PR (oder manueller Merge) das Production-Overlay
- ArgoCD synct Production
Schritt 2 automatisieren mit Image Updater oder einem einfachen CI-Job:
# In der CI-Pipeline des App-Repos (separater Step nach dem Clonen von infra-gitops)
- name: Update staging manifest
run: |
kustomize edit set image api=$IMAGE_TAG
git commit -am "chore: update api to $IMAGE_TAG"
git push
working-directory: infra-gitops/apps/api/overlays/staging
Für Production-Promotion einen PR mit Approval verlangen. Das liefert:
- Audit-Trail — wer hat das Production-Deploy wann freigegeben
- Rollback —
git revertauf den Merge-Commit - Diff-Review — der PR zeigt exakt, was sich zwischen den Environments geändert hat
Sync Windows und Waves
Nicht alles sollte sofort syncen. ArgoCD unterstützt Sync Windows, um einzuschränken, wann Syncs stattfinden:
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: production
spec:
syncWindows:
- kind: allow
schedule: '0 8-17 * * 1-5'
duration: 9h
applications: ['*']
timeZone: Europe/Berlin
Das beschränkt Production-Syncs auf Geschäftszeiten an Werktagen — wenn jemand da ist, falls etwas kaputtgeht.
Für die Reihenfolge von Abhängigkeiten (Datenbank-Migration vor App-Deployment) Sync Waves nutzen:
metadata:
annotations:
argocd.argoproj.io/sync-wave: "-1" # Läuft vor Wave 0
Negative Waves laufen zuerst. Wave -1 für Migrationen, Wave 0 für die App, Wave 1 für Post-Deploy-Checks.
Monitoring und Alerts
ArgoCD exponiert Prometheus-Metriken auf :8082/metrics. Die wichtigsten Alerts:
- App Sync fehlgeschlagen —
argocd_app_info{sync_status="OutOfSync"}für mehr als 10 Minuten - App Health degradiert —
argocd_app_info{health_status!="Healthy"}für mehr als 5 Minuten - Sync-Operation-Fehler —
argocd_app_sync_total{phase="Error"}Rate-Anstieg
ArgoCD unterstützt auch Notifications via Slack, Teams, Webhooks oder E-Mail. Mindestens konfigurieren: Sync-Fehler und Health-Degradierung für Production-Apps.
Häufige Fallstricke
Secrets unverschlüsselt in Git. Klingt offensichtlich, passiert aber. Pre-commit Hooks mit gitleaks oder detect-secrets nutzen, um das vor dem Commit abzufangen.
Pruning nicht aktivieren. Ohne prune: true lässt das Löschen eines Manifests aus Git die Ressource weiterlaufen. Am Ende hat man Ghost-Services, die niemand wartet.
Resource Hooks ignorieren. ArgoCD respektiert Resource Hooks für Pre-Sync und Post-Sync Jobs. PreSync-Hooks für Datenbank-Migrationen statt Init-Container nutzen — sie sind in der ArgoCD-UI sichtbar und haben ordentliches Error-Handling.
Alles in einen Namespace syncen. Den Destination-Namespace in der Application nutzen, um Namespace-Grenzen durchzusetzen. Mit ArgoCD Projects kombinieren, um einzuschränken, in welche Namespaces ein Team deployen darf.
Das Fazit
GitOps mit ArgoCD eliminiert das Ratespiel „Was läuft gerade in Production?”. Jedes Deployment ist ein Git-Commit. Jedes Rollback ist ein Revert. Jede Änderung ist reviewbar.
Die Setup-Investition ist frontlastig — Repository-Struktur, ApplicationSets, Secrets-Management, Sync-Policies. Sobald es läuft, werden Deployments langweilig. Und langweilige Deployments sind genau das, was man will.
Mit einem Service in Staging anfangen. Die Feedback-Loop richtig hinbekommen. Dann auf Production erweitern und Services inkrementell hinzufügen. Nicht alles auf einmal migrieren.