ArgoCD and GitOps: Continuous Delivery for Kubernetes (2026)
GitOps has become the de facto deployment model for Kubernetes in production. Rather than pushing changes imperatively via kubectl apply, GitOps treats your Git repository as the single source of truth — a reconciliation loop continuously compares desired state (Git) against actual state (cluster) and corrects any drift. ArgoCD is the most widely deployed GitOps controller, used by organizations ranging from startups to Fortune 500 companies. This guide covers everything from first principles to production-hardened patterns including multi-cluster ApplicationSets, RBAC, and automated rollback strategies.
Table of Contents
GitOps Principles and Why They Matter
GitOps rests on four foundational principles defined by the OpenGitOps project:
- Declarative — The entire system is described declaratively (Kubernetes manifests, Helm values, Kustomize overlays).
- Versioned and immutable — The desired state is stored in a version-controlled system (Git), providing history and auditability.
- Pulled automatically — Approved changes are applied automatically by a software agent running inside the cluster.
- Continuously reconciled — Software agents continuously observe actual state and attempt to match the desired state.
The practical benefits over push-based CI/CD are significant. Drift detection catches manual kubectl changes that bypass your pipeline. Rollback is a git revert. Audit logs come free from git history. Cluster credentials never leave the cluster boundary — your CI system only writes to Git, not to Kubernetes.
ArgoCD Architecture Deep Dive
ArgoCD runs as several components inside your cluster:
- argocd-server — The API server and web UI. Handles all user interactions via gRPC and REST.
- argocd-repo-server — Clones Git repositories and renders manifests (Helm, Kustomize, plain YAML). Isolated for security — does not have cluster access.
- argocd-application-controller — The heart of ArgoCD. Watches Application resources, calls repo-server for desired state, calls Kubernetes API for live state, compares them, and triggers syncs.
- argocd-dex-server — OpenID Connect provider for SSO integration (GitHub, Okta, LDAP).
- argocd-redis — Caches rendered manifests and live cluster state.
- applicationset-controller — Generates Application resources from templates based on generators (Git, cluster list, matrix, etc.).
Install ArgoCD into its own namespace:
kubectl create namespace argocd
kubectl apply -n argocd -f \
https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# Wait for all pods to be ready
kubectl wait --for=condition=Available deployment --all -n argocd --timeout=300s
# Get the initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath="{.data.password}" | base64 -d
The Application CRD: Core Configuration
Every deployment in ArgoCD is represented by an Application custom resource. Here is a production-ready example deploying a Helm chart:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: payments-service
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io # cascade delete
annotations:
notifications.argoproj.io/subscribe.on-sync-succeeded.slack: deployments
notifications.argoproj.io/subscribe.on-sync-failed.slack: alerts
spec:
project: production
source:
repoURL: https://github.com/myorg/k8s-config
targetRevision: main
path: apps/payments-service/overlays/production
# For Helm charts:
# chart: payments-service
# helm:
# releaseName: payments
# values: |
# replicaCount: 3
# image:
# tag: "v2.4.1"
destination:
server: https://kubernetes.default.svc
namespace: payments
syncPolicy:
automated:
prune: true # Delete resources removed from Git
selfHeal: true # Revert manual kubectl changes
allowEmpty: false # Never sync to empty state
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
- ApplyOutOfSyncOnly=true # Only apply changed resources
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
revisionHistoryLimit: 10
resources-finalizer.argocd.argoproj.io finalizer ensures that when you delete an Application resource, ArgoCD also deletes all the Kubernetes resources it created. Omit it if you want to delete the Application without affecting the deployed resources (useful for migrating apps between ArgoCD instances).
Sync Policies: Auto vs Manual
The choice between automated and manual sync involves trade-offs that depend on your deployment risk tolerance:
| Feature | Manual Sync | Auto Sync |
|---|---|---|
| Deploy trigger | Human approval in UI/CLI | Any Git commit to tracked path |
| Drift correction | Shows diff, requires manual action | Automatically reverts in seconds |
| Safe for production | Yes (more control) | Yes with proper Git branch protection |
| Emergency rollback | Manual sync to previous revision | git revert + auto-applied |
| Recommended for | Stateful apps, databases | Stateless services, microservices |
A common pattern is to use auto-sync for staging and manual sync for production, with Git branch protection rules requiring PR reviews before merging to the production branch. Configure this per project:
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: production
namespace: argocd
spec:
description: Production workloads
sourceRepos:
- 'https://github.com/myorg/k8s-config'
destinations:
- namespace: '*'
server: https://prod-cluster.example.com
clusterResourceWhitelist:
- group: ''
kind: Namespace
namespaceResourceBlacklist:
- group: ''
kind: ResourceQuota # Prevent apps from modifying quotas
roles:
- name: deployer
description: Can sync but not delete apps
policies:
- p, proj:production:deployer, applications, sync, production/*, allow
- p, proj:production:deployer, applications, get, production/*, allow
groups:
- myorg:sre-team
App of Apps Pattern
Managing dozens or hundreds of Application resources manually does not scale. The App of Apps pattern solves this: one root Application watches a directory of other Application manifests, bootstrapping the entire cluster from a single command.
# Root application — points to a directory of Application CRDs
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: cluster-bootstrap
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/k8s-config
targetRevision: main
path: clusters/production/apps # contains Application YAML files
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
The clusters/production/apps/ directory then contains individual Application manifests:
clusters/production/apps/
├── ingress-nginx.yaml
├── cert-manager.yaml
├── prometheus-stack.yaml
├── payments-service.yaml
├── user-service.yaml
└── api-gateway.yaml
base/ directory holds common Application templates, and each cluster's overlay patches in cluster-specific values (server URL, namespace, image tags). This avoids copy-paste across staging and production.
ApplicationSet for Multi-Cluster Deployments
ApplicationSet is the evolution of App of Apps for multi-cluster and multi-tenant scenarios. A single ApplicationSet resource generates many Application resources using generators:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: payments-service-all-clusters
namespace: argocd
spec:
generators:
- matrix:
generators:
- clusters:
selector:
matchLabels:
environment: production
- git:
repoURL: https://github.com/myorg/k8s-config
revision: main
directories:
- path: apps/payments-service/overlays/*
template:
metadata:
name: '{{name}}-payments'
annotations:
notifications.argoproj.io/subscribe.on-sync-failed.pagerduty: ""
spec:
project: production
source:
repoURL: https://github.com/myorg/k8s-config
targetRevision: main
path: '{{path}}'
kustomize:
images:
- 'payments:{{metadata.annotations.image-tag}}'
destination:
server: '{{server}}'
namespace: payments
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
The Matrix generator combines the cluster list with the Git directory list, producing one Application per cluster per environment overlay. When you add a new cluster with the environment: production label, ApplicationSet automatically creates all the Application resources for it.
RBAC and SSO in ArgoCD
ArgoCD's RBAC is configured via ConfigMaps in the argocd namespace. The default policy uses Casbin syntax:
# kubectl edit configmap argocd-rbac-cm -n argocd
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-rbac-cm
namespace: argocd
data:
policy.default: role:readonly
policy.csv: |
# Admins can do everything
g, myorg:platform-team, role:admin
# SRE can sync and view but not create/delete apps
p, role:sre, applications, sync, */*, allow
p, role:sre, applications, get, */*, allow
p, role:sre, applications, action/*, */*, allow
g, myorg:sre-team, role:sre
# Developers can only view their own project
p, role:dev-payments, applications, get, payments/*, allow
p, role:dev-payments, applications, sync, payments/staging-*, allow
g, myorg:payments-team, role:dev-payments
scopes: '[groups]'
For GitHub SSO via Dex, add to argocd-cm:
data:
url: https://argocd.example.com
dex.config: |
connectors:
- type: github
id: github
name: GitHub
config:
clientID: $dex.github.clientID
clientSecret: $dex.github.clientSecret
orgs:
- name: myorg
teams:
- platform-team
- sre-team
- payments-team
Rollback Strategy and Notifications
ArgoCD maintains a revision history for each Application. Rolling back is straightforward:
# Via CLI — list history
argocd app history payments-service
ID DATE REVISION
0 2026-06-01 10:00:00 +0000 UTC abc1234
1 2026-06-03 14:30:00 +0000 UTC def5678
2 2026-06-05 09:15:00 +0000 UTC ghi9012
# Roll back to previous revision
argocd app rollback payments-service 1
# Roll back and disable auto-sync temporarily
argocd app rollback payments-service 1 --prune
argocd app rollback puts the application into a degraded state relative to HEAD. Auto-sync will try to re-apply the latest Git state. Always fix the root cause in Git and merge a fix commit rather than relying on rollback as a permanent solution.
Configure notifications using the ArgoCD Notifications controller:
# argocd-notifications-cm ConfigMap trigger configuration
data:
trigger.on-sync-failed: |
- when: app.status.operationState.phase in ['Error', 'Failed']
send: [slack-message, pagerduty-create-incident]
trigger.on-health-degraded: |
- when: app.status.health.status == 'Degraded'
oncePer: app.status.sync.revision
send: [slack-message]
template.slack-message: |
message: |
Application {{.app.metadata.name}} sync {{.app.status.operationState.phase}}
Revision: {{.app.status.sync.revision}}
Details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}
service.slack: |
token: $slack-token
username: ArgoCD
Frequently Asked Questions
What is the difference between ArgoCD and Flux?
Both are CNCF-graduated GitOps controllers. ArgoCD provides a rich web UI, multi-cluster management from a single control plane, and fine-grained RBAC out of the box. Flux is more lightweight and follows a purely controller-per-concern architecture (source controller, kustomize controller, helm controller). ArgoCD is generally preferred when you need a centralized multi-cluster dashboard; Flux is preferred when running decentralized GitOps where each cluster manages itself independently.
How do I handle secrets in GitOps without committing them to Git?
The standard approaches are: (1) Sealed Secrets — encrypt secrets with a cluster-specific public key so only your cluster can decrypt them; safe to commit to Git. (2) External Secrets Operator — reference secrets from AWS Secrets Manager, Vault, or GCP Secret Manager; the operator syncs them into Kubernetes Secrets. (3) SOPS + age/GPG — encrypt secret values in YAML files using Mozilla SOPS; ArgoCD has a SOPS plugin. Most enterprises use External Secrets Operator for centralized secret governance.
Can ArgoCD deploy to clusters outside the one it's installed in?
Yes. ArgoCD supports managing external clusters by registering them with argocd cluster add <context-name>. This creates a ServiceAccount in the target cluster and stores the kubeconfig in an ArgoCD Secret. The Application destination server field then references the external cluster URL. This is the standard multi-cluster model — one ArgoCD instance (often in a management cluster) controls all application clusters.
How do I prevent ArgoCD from overwriting manual hotfixes during an incident?
The cleanest approach is to temporarily disable auto-sync on the affected Application: argocd app set payments-service --sync-policy none. Apply your hotfix manually. Then create a Git commit with the same change, merge it, re-enable auto-sync, and let ArgoCD reconcile. Disabling auto-sync ensures the reconciliation loop does not undo your emergency change during the incident window.
What is the recommended resource structure for a large organization with 50+ microservices?
Use one AppProject per team, one ApplicationSet per service family, and the App of Apps pattern per cluster. Store all configuration in a monorepo under clusters/<env>/apps/ with Kustomize overlays per environment. Apply branch protection on the production branch requiring two reviewers. Use ArgoCD's syncWindows in AppProject to restrict auto-sync to business hours for production, giving your on-call team control during off-hours.