ArgoCD Explained, Part 1: The Theory Behind GitOps
Everything you need to know before you type your first kubectl apply architecture, Applications, scaling patterns, ordering, multi-cluster RBAC, and production hygiene.
Part 1 of 2 · Theory · Part 2 will cover the hands-on, practical walkthrough
If you’ve worked with Kubernetes for any length of time, you’ve probably run into the same recurring headache: keeping what’s declared in your manifests in sync with what’s actually running in your cluster. Someone runs a one-off kubectl edit to fix something urgently, a deploy script half-completes, and suddenly nobody is quite sure what’s really live. This is the exact problem ArgoCD was built to solve, and it does so by borrowing a simple but powerful idea from software engineering: treat your infrastructure the same way you treat your code.
This is Part 1 of a two-part series. Here, we’re covering the theory what ArgoCD is, how it’s built, and the concepts you need internalized before touching a real cluster. Part 2 will be the hands-on companion: installing ArgoCD, deploying a real application, and walking through the workflows in practice.
1. What Is ArgoCD, and Why Do We Need It?
The Tool, in One Line
ArgoCD is a GitOps continuous delivery tool for Kubernetes. It sits between your Git repository where your YAML manifests, Helm charts, or Kustomize configurations live and your Kubernetes cluster, and it continuously checks whether the two agree.
The Problem Before ArgoCD
Before tools like ArgoCD became standard, most teams deployed to Kubernetes through a fairly familiar pattern: a CI pipeline (Jenkins, GitLab CI, etc.) would run kubectl apply or helm upgrade as its final step. This worked, but it came with real costs:
- The CI system needed direct credentials to the production cluster a meaningful security exposure.
- Manual or scripted deploys were prone to human error and inconsistent execution.
- There was no reliable way to know whether the cluster’s actual state matched what was declared in Git, or whether someone had quietly patched something by hand.
The GitOps Idea
GitOps reframes the problem. Instead of pushing changes into the cluster, you declare the desired state in Git, and a controller running inside the cluster pulls that state and reconciles it continuously. A few principles fall out of this:
- Git is the single source of truth. Whatever is committed to the repo is what should be running.
- ArgoCD runs as a controller inside the cluster and continuously watches the connected Git repository.
- Drift is detected automatically. If Git changes, ArgoCD notices and can reconcile the cluster to match (a Sync).
- Manual changes are treated as drift. If someone edits a live resource directly, ArgoCD marks the app OutOfSync, and if auto-sync is enabled, reverts it back. This behavior is called self-healing.
Where This Actually Matters
- Organizations running many microservices across multiple environments (dev/staging/prod), where manual deployment coordination doesn’t scale.
- Teams that need a clean audit trail - every change is a Git commit, with an author, a timestamp, and a diff.
- Setups that span multiple Kubernetes clusters, managed from a single control plane.
Traditional CI/CD vs. GitOps
2. ArgoCD Architecture
ArgoCD deploys inside your Kubernetes cluster, typically in its own argocd namespace, and it’s built as a set of small, independent components rather than one monolithic service. Understanding these pieces individually makes debugging dramatically easier later on.
API Server
The single entry point for everything. The CLI, the Web UI, and any CI/CD integration all talk to the API Server over gRPC/REST. It handles application management (create, update, delete, sync), authentication, RBAC enforcement, and incoming webhook events from Git providers.
Repository Server
This component talks directly to Git. When your repo contains a Helm chart, Kustomize overlay, or plain manifests, the Repo Server renders them down into plain Kubernetes YAML. It also caches the repository locally so it isn’t re-cloning on every reconciliation loop.
Application Controller the Brain
This is the component doing the real work. It continuously compares two things:
- Desired State: what the Repo Server produced from Git.
- Live State: what’s actually running, queried directly from the Kubernetes API.
When these diverge, the app is marked OutOfSync. If automated sync is enabled, the controller applies the changes itself. It also continuously evaluates application health (Healthy, Progressing, Degraded, and so on).
Redis
A caching layer for application states, resource trees, and other computed data. Without it, ArgoCD would be re-querying the Kubernetes API and Git far more often than necessary Redis keeps things responsive.
Dex (Optional)
If you need single sign-on Google, GitHub, LDAP, or a generic OIDC provider. Dex is the component that bridges ArgoCD’s authentication to those external identity providers.
The Full Reconciliation Flow
- A change lands in the Git repo say, an image tag bump to v2.
- The Application Controller checks for changes, either on a polling interval or instantly via a webhook.
- The Repo Server renders the latest manifests from that commit.
- The Controller compares live state (v1) against desired state (v2) and marks the app OutOfSync.
- If auto-sync is on, the Controller calls the Kubernetes API to apply the update.
- Kubernetes rolls out the new pod (v2) and terminates the old one.
- The API Server surfaces this status change in real time to the UI and CLI.
Architecture at a Glance
Why this matters in practice
When something breaks, this map tells you where to look. A sync that won’t trigger points to the Application Controller. A Helm chart that won’t render points to the Repo Server. A UI or access problem points to the API Server.
3. The Application CRD
In ArgoCD, an Application is a Custom Resource — a Kubernetes-native object that ArgoCD understands and reconciles. It’s the fundamental unit you’ll interact with, and it defines four things: which Git repo to pull from, which path within that repo to use, where in the cluster to deploy, and how syncing should happen.
A Basic Application
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/my-app-manifests.git
targetRevision: main
path: k8s/production
destination:
server: https://kubernetes.default.svc
namespace: my-app-namespace
syncPolicy:
automated:
prune: true
selfHeal: trueThe source block
- repoURL: the Git repository.
- targetRevision: the branch, tag, or commit to track.
- path: the folder within the repo containing manifests.
The destination block
- server: the target cluster (use https://kubernetes.default.svc for the same cluster ArgoCD runs on).
- namespace: the target namespace.
The syncPolicy block the most consequential setting
Manual Sync (the default) means ArgoCD will only report OutOfSync a human has to click Sync in the UI or CLI.
syncPolicy:
automated:
prune: true
selfHeal: true- prune: true — if a resource is removed from Git, ArgoCD deletes it from the cluster too.
- selfHeal: true — manual cluster changes are reverted automatically to match Git.
Sync Status
Health Status
Creating an Application
Via the CLI:
argocd app create my-app \
- repo https://github.com/myorg/my-app-manifests.git \
- path k8s/production \
- dest-server https://kubernetes.default.svc \
- dest-namespace my-app-namespaceOr directly with a manifest, since an Application is just a Kubernetes object:
kubectl apply -f application.yaml4. Scaling Up: App of Apps and ApplicationSet
A single Application object is manageable. Twenty of them, each hand-maintained, is where things start to hurt — new environments become tedious to stand up, and consistency across apps starts slipping.
App of Apps Pattern
The core idea: create one parent Application whose entire job is to create other Application objects. Imagine a repo that contains nothing but Application manifests:
apps-repo/
├── app-frontend.yaml
├── app-backend.yaml
├── app-database.yaml
└── app-redis.yaml
A root Application points at that repo:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root-app
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/apps-repo.git
targetRevision: main
path: .
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: trueWhen ArgoCD syncs root-app, it discovers every Application file in that folder and deploys all of the child applications automatically.
The payoff
Adding a new microservice becomes as simple as dropping a new YAML file into the repo. The root app detects it and creates it one place, full control.
ApplicationSet — Templating It Further
Get Pawan Sharma’s stories in your inbox
Join Medium for free to get updates from this writer.
App of Apps still means writing a YAML file per application by hand. ApplicationSet automates that with a template-and-generator model.
Example: the List generator
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: my-app-set
namespace: argocd
spec:
generators:
- list:
elements:
- env: dev
replicas: "1"
- env: staging
replicas: "2"
- env: prod
replicas: "5"
template:
metadata:
name: 'my-app-{{env}}'
spec:
project: default
source:
repoURL: https://github.com/myorg/my-app.git
targetRevision: main
path: 'k8s/{{env}}'
destination:
server: https://kubernetes.default.svc
namespace: 'my-app-{{env}}'This one definition produces three Applications — my-app-dev, my-app-staging, my-app-prod — automatically.
Common Generators
Choosing Between Them
Most production environments today lean toward ApplicationSet it scales better and keeps configuration DRY.
5. Sync Waves, Hooks, and Resource Ordering
By default, ArgoCD applies all resources in a sync in parallel. That’s fine until ordering actually matters for instance, a database migration needs to complete before the backend starts, and the backend needs to be healthy before the frontend goes live.
Sync Waves
An annotation on any resource tells ArgoCD which ‘wave’ it belongs to:
apiVersion: apps/v1
kind: Deployment
metadata:
name: database
annotations:
argocd.argoproj.io/sync-wave: "0"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
annotations:
argocd.argoproj.io/sync-wave: "1"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
annotations:
argocd.argoproj.io/sync-wave: "2"- The default wave value is 0 when no annotation is present.
- Resources apply in ascending order — 0, then 1, then 2.
- The next wave only begins once every resource in the current wave reports Healthy.
- Negative numbers are valid too, for anything that needs to run before everything else.
Resource Hooks
Hooks trigger an action at a specific point in the sync lifecycle, rather than as part of the ordinary resource set.
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration
annotations:
argocd.argoproj.io/hook: PreSyncHook Types
A Real Example: DB Migration Job
apiVersion: batch/v1
kind: Job
metadata:
name: db-migrate
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
template:
spec:
containers:
- name: migrate
image: myapp/migration:latest
command: ["python", "manage.py", "migrate"]
restartPolicy: NeverHook Delete Policies
Putting It Together
Use Sync Waves when you need dependency ordering across multiple resources. Use Hooks when you need a one-time task — a migration, backup, notification, or cleanup step — tied to a specific point in the sync.
6. Multi-Cluster Management and AppProjects
Part A — Managing Multiple Clusters
ArgoCD isn’t limited to the cluster it runs in. A single ArgoCD instance — often called a ‘hub’ can register and manage any number of external Kubernetes clusters.
Registering a cluster via the CLI:
argocd cluster add prod-cluster-context-nameThis creates a Secret in the argocd namespace holding that cluster’s connection details:
apiVersion: v1
kind: Secret
metadata:
name: prod-cluster-secret
namespace: argocd
labels:
argocd.argoproj.io/secret-type: cluster
stringData:
name: prod-cluster
server: https://prod-cluster-api.example.com
config: |
{
"bearerToken": "...",
"tlsClientConfig": {...}
}Applications can then target that cluster directly:
spec:
destination:
server: https://prod-cluster-api.example.com
namespace: my-appThis gives you a single control plane for dev, staging, and production alike, and it’s exactly what the ApplicationSet Cluster generator (from Section 4) uses under the hood to fan a single app definition out across every registered cluster.
Part B — AppProject: RBAC and Isolation
Once multiple teams share an ArgoCD instance, you need boundaries — Team A shouldn’t be able to touch Team B’s applications, intentionally or otherwise. AppProject provides that boundary.
An AppProject restricts:
- Which Git repositories can be used as a source.
- Which cluster/namespace combinations are valid destinations.
- Which Kubernetes resource kinds are permitted.
- Which users or groups can perform which actions (RBAC roles).
Example AppProject
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: team-frontend
namespace: argocd
spec:
description: "Frontend team's project"
sourceRepos:
- https://github.com/myorg/frontend-*.git
destinations:
- server: https://kubernetes.default.svc
namespace: frontend-*
clusterResourceWhitelist: []
namespaceResourceWhitelist:
- group: 'apps'
kind: Deployment
- group: ''
kind: Service
roles:
- name: developer
policies:
- p, proj:team-frontend:developer, applications, sync, team-frontend/*, allow
groups:
- frontend-team-groupsourceRepos restricts this project to repos matching frontend-*.
- destinations limits deployment to namespaces matching frontend-*.
- clusterResourceWhitelist: [] blocks creation of any cluster-scoped resource — an important safety rail.
- roles define exactly what the developer role can do — here, sync only, not delete.
Linking an Application to a Project
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: frontend-app
spec:
project: team-frontend
source:
repoURL: https://github.com/myorg/frontend-web.git
...If this Application’s repo or destination falls outside team-frontend’s rules, ArgoCD rejects it outright.
About the default project
If you don’t set a project field, ArgoCD falls back to the default project, which has no restrictions at all. In production, always define custom AppProjects rather than relying on default.
Quick Summary
7. Notifications, Rollback, and Production Hygiene
Rollback
ArgoCD keeps a history of every sync automatically, so returning to a known-good state is always available.
Viewing history:
argocd app history my-appID DATE REVISION
Rolling back:
argocd app rollback my-app 1The right way to roll back
The better practice is a Git revert — a new commit restoring the previous state — rather than only rolling back through ArgoCD. GitOps’ whole premise is that Git is the source of truth; if you roll back in ArgoCD without updating Git, the broken version will simply come back the next time anyone pushes.
Notifications
A separate Notifications Controller can send alerts to Slack, email, webhooks, or Microsoft Teams based on application events.
Defining a Slack service in the notifications ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-notifications-cm
namespace: argocd
data:
service.slack: |
token: $slack-token
template.app-deployed: |
message: "App {{.app.metadata.name}} deployed successfully!"
trigger.on-deployed: |
- when: app.status.sync.status == 'Synced'
send: [app-deployed]Subscribing an Application to it:
metadata:
annotations:
notifications.argoproj.io/subscribe.on-deployed.slack: my-channelSimilar triggers — on-sync-failed, on-health-degraded — cover failure scenarios too.
Secrets Management
A common trap
ArgoCD does not encrypt secrets on its own. Committing plain-text secrets to Git is a serious risk, since they persist in the repo’s history indefinitely.
a. Sealed Secrets (Bitnami)
Secrets are encrypted locally with the kubeseal CLI. The encrypted SealedSecret is safe to commit; only a controller inside the target cluster, holding the matching private key, can decrypt it.
b. External Secrets Operator (ESO)
Actual secret values live in Vault, AWS Secrets Manager, or Azure Key Vault. Git holds only a reference, and ESO materializes the real Kubernetes Secret from the external source at runtime.
c. HashiCorp Vault + ArgoCD Vault Plugin
Manifests contain placeholders like <path:secret/data/myapp#password>, which the plugin resolves against Vault at sync time.
Golden rule
Never commit raw secrets to a Git repository — private repos included.
Production Checklist
What’s Next
That’s the full theoretical foundation: why ArgoCD exists, how its components fit together, how Applications are defined, how to scale to dozens of them, how to control ordering, how to manage multiple clusters safely, and how to run all of this in production without shooting yourself in the foot.
Stay tuned! Part 2 picks up from here with the hands-on side: installing ArgoCD on a real cluster, connecting a Git repo, deploying a working application end to end, triggering syncs, and watching the self-healing behavior in action.










