GitOps Explained: Principles and a Complete Argo CD Tutorial
GitOps Explained: Principles and a Complete Argo CD Tutorial
GitOps manages infrastructure and deployment pipelines the same way we manage source code: Git becomes the single source of truth for declarative infrastructure and applications. It's an extension of DevOps that makes operations developer-centric — pull requests are your change-management system, and an agent inside the cluster does the deploying. This guide covers the four GitOps principles, why the pull model beats push, and a complete hands-on setup with Argo CD — the declarative, GitOps continuous delivery tool for Kubernetes.
Table of Contents
- The four GitOps principles
- Why Argo CD?
- Getting started: Argo CD in 7 steps
- Reading application state: sync and health status
- Repository patterns that scale
- Scaling to hundreds of apps: ApplicationSet
- Ordering and orchestrating syncs: hooks and waves
- Multi-tenancy: AppProjects and RBAC
- Multiple sources: charts plus external config
- Day-2 operations
- Argo CD vs Flux
- The payoff
The four GitOps principles
1. Declarative
You describe the desired state of the system — in YAML, JSON, Helm, or Kustomize — and the system is responsible for achieving it. That's the opposite of imperative scripts that spell out every step. The entire desired state lives in Git, and the GitOps tool's job is making actual state match it.
2. Versioned and immutable
Every change is a commit: tracked, attributed, and reversible. Git history is your audit log — what changed, who changed it, and why. Immutability means a deployed version is never patched in place; changes ship by deploying a new version, which eliminates drift by construction.
3. Pulled automatically
Traditional CI/CD pushes changes: the pipeline holds cluster credentials and runs kubectl/helm against the target. GitOps inverts this — an agent inside the environment pulls desired state from Git and applies it. That decouples CI from deployment (each scales independently) and tightens security: your CI system never needs production access, and cluster credentials never leave the cluster.
4. Continuously reconciled
The agent continuously compares live state against Git. Drift — say, a manual kubectl edit — is detected and corrected automatically. Rollback is equally simple: git revert the commit and the system converges back to the previous state.
Why Argo CD?
Argo CD implements these principles as a Kubernetes controller. It watches your running applications and continuously compares live state to the desired state in Git. An app whose live state differs is flagged OutOfSync — Argo CD visualizes the diff and can sync it automatically or on demand.
Key capabilities:
- Automated deployment to specified target environments, including multiple clusters
- Supports Kustomize, Helm charts, Jsonnet, plain YAML, and custom config-management plugins
- Tracking strategies: follow a branch, a tag, or pin to an exact Git commit
- Rollback / roll-anywhere to any configuration ever committed
- Health status analysis of resources, automated drift detection and visualization
- SSO integration (OIDC, OAuth2, LDAP, SAML, GitHub, GitLab) and multi-tenant RBAC
- Web UI with a real-time view, plus a CLI and access tokens for CI automation
- Webhook integration (GitHub, GitLab, Bitbucket) for instant sync on push
- PreSync/Sync/PostSync hooks for blue/green and canary rollouts
- Audit trails for events and API calls, plus Prometheus metrics
Getting started: Argo CD in 7 steps
Step 1 — Install
kubectl create namespace argocd
kubectl apply -n argocd --server-side --force-conflicts -f \
https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
This deploys the API server, repo server, application controller, and UI. The --server-side --force-conflicts flags are required because Argo CD's CRDs exceed the client-side apply size limit.
Step 2 — Install the CLI
curl -sSL -o argocd \
https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
chmod +x argocd && sudo mv argocd /usr/local/bin/
Step 3 — Expose the API server
kubectl port-forward svc/argocd-server -n argocd 8080:443
The UI is now at https://localhost:8080. In production, front it with an Ingress and SSO instead.
Step 4 — Log in
The initial admin password lives in a generated secret:
kubectl get secret argocd-initial-admin-secret -n argocd \
-o jsonpath="{.data.password}" | base64 -d
argocd login localhost:8080 # user: admin
Step 5 — Register target clusters
Argo CD manages its own cluster out of the box. To deploy elsewhere:
argocd cluster add <CONTEXT_NAME>
Step 6 — Create an application
For demos, the CLI is quickest:
argocd app create my-app \
--repo https://github.com/your-org/your-app.git \
--path ./k8s-manifests \
--dest-server https://kubernetes.default.svc \
--dest-namespace default
For production, declare the app as YAML so the GitOps setup is itself GitOps-managed:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: web
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/your-org/deploy.git
targetRevision: HEAD
path: overlays/prod
destination:
server: https://kubernetes.default.svc
namespace: web
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Step 7 — Sync
argocd app sync my-app
Argo CD reconciles the live state with Git. With automated sync policy, merges to the tracked branch deploy themselves — prune removes resources deleted from Git, and selfHeal reverts manual drift.
Reading application state: sync and health status
Every Application carries two independent status signals, and confusing them is the most common beginner mistake:
Sync status — does the live state match Git?
Synced— live state matches the tracked revisionOutOfSync— Git and the cluster disagree; something needs syncingUnknown— comparison failed (usually a manifest-generation error)
Health status — is the deployed workload actually working?
Healthy— resource is fully operationalProgressing— rollout still in flight (pods not ready, replicas scaling)Degraded— something failed (CrashLoopBackOff, unschedulable pods)Suspended— resource is deliberately paused (e.g., a suspended CronJob)Missing/Unknown— resource absent from the cluster or health can't be assessed
An app can be Synced but Degraded (the bad YAML applied perfectly) — or OutOfSync but Healthy (you've committed a fix, not yet applied). Argo CD ships built-in health checks for every standard resource type; for CRDs you can declare your own as Lua scripts in the argocd-cm ConfigMap.
Repository patterns that scale
- App repo vs. deploy repo. CI builds and tests images from the app repo, then updates the image tag in the deploy repo (via PR or direct commit). GitOps only ever touches the deploy repo — keeping CI credentials out of production entirely.
- Environment overlays. Kustomize base/ plus overlays/dev, overlays/staging, overlays/prod. Promotion is a pull request bumping an overlay.
- App of Apps. One root Application that creates child Applications — your entire platform, declared in one repo.
- Pin production to commits or tags, let dev track HEAD. You get reproducibility where it counts and speed where it doesn't.
Scaling to hundreds of apps: ApplicationSet
App of Apps works, but the modern, officially-recommended pattern for many applications is the ApplicationSet controller (bundled in the install manifest). An ApplicationSet is a template plus a generator — it stamps out one Application per matched item. Generators include list, cluster, git (directories or files), pullRequest, matrix, merge, scmProvider, and plugin.
The git directories generator is the workhorse: point it at a repo and every folder becomes an Application. Add a clusters/prod-eu directory → a new prod-eu app appears automatically:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: cluster-addons
namespace: argocd
spec:
goTemplate: true
generators:
- git:
repoURL: https://github.com/your-org/deploy.git
revision: HEAD
directories:
- path: addons/*
template:
metadata:
name: '{{.path.basename}}'
spec:
project: default
source:
repoURL: https://github.com/your-org/deploy.git
targetRevision: HEAD
path: '{{.path.path}}'
destination:
server: https://kubernetes.default.svc
namespace: '{{.path.basename}}'
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Pair the matrix generator (git × cluster) to deploy the same addon set to every registered cluster — one ApplicationSet, fleet-wide coverage.
Ordering and orchestrating syncs: hooks and waves
Sync hooks run workloads at specific points in the sync lifecycle via the argocd.argoproj.io/hook annotation:
PreSync— before resources apply (database migrations)Sync— alongside the main sync (rarely needed; normal resources suffice)PostSync— after sync completes and resources are healthy (smoke tests, notifications)SyncFail— only when the sync fails (rollback jobs, alerting)PostDelete/PreDelete— run when an Application is deleted (cleanup)
apiVersion: batch/v1
kind: Job
metadata:
name: db-migrate
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
hook-delete-policy controls hook cleanup — HookSucceeded deletes the Job after it succeeds so the next sync can rerun it.
Sync waves order everything else. Annotate resources with argocd.argoproj.io/sync-wave — lower numbers apply first, and Argo CD waits for each wave to report healthy before starting the next. Wave -1 runs before the default wave 0:
metadata:
annotations:
argocd.argoproj.io/sync-wave: "-1"
Classic use: namespaces and CRDs at negative waves, control plane at 0, app deployments at 1+. For real blue/green or canary analysis, layer in Argo Rollouts — Argo CD manages the Rollout resource, Rollouts manages the traffic shift.
Multi-tenancy: AppProjects and RBAC
The default project permits everything — fine for demos, wrong for teams. AppProjects are the tenancy boundary: they whitelist which source repos, target clusters/namespaces, and resource kinds a group of Applications may use:
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: payments
namespace: argocd
spec:
sourceRepos:
- https://github.com/your-org/payments-*.git
destinations:
- server: https://kubernetes.default.svc
namespace: payments-*
clusterResourceWhitelist:
- group: ''
kind: Namespace
An app in the payments project can only pull from payments-* repos and only land in payments-* namespaces — blast radius contained at the API level.
RBAC lives in the argocd-rbac-cm ConfigMap as a CSV policy. Map users/groups (from your OIDC SSO integration) to roles:
data:
policy.csv: |
p, role:payments-dev, applications, get, payments/*, allow
p, role:payments-dev, applications, sync, payments/*, allow
g, your-org:payments-team, role:payments-dev
The resource/action vocabulary is granular — applications, clusters, repositories, logs, exec — and scoped per project with the payments/* object pattern.
Multiple sources: charts plus external config
spec.source accepts a plural sources list — most useful for consuming a published Helm chart while keeping your values in Git, without vendoring the chart:
spec:
sources:
- repoURL: https://charts.bitnami.com/bitnami
chart: redis
targetRevision: 19.0.0
helm:
valueFiles:
- $values/deploy/values-prod.yaml
- repoURL: https://github.com/your-org/deploy.git
targetRevision: HEAD
ref: values
The $values ref points the chart's valueFiles at the second source's repo. Chart version pinned in Git, values reviewed by PR, no chart copying.
Day-2 operations
argocd app list # all apps, sync + health at a glance
argocd app get web # detail: status, resources, events
argocd app diff web # live-vs-Git diff before you sync
argocd app history web # every revision deployed
argocd app rollback web 3 # roll back to history ID 3
argocd app delete web # cascade-deletes managed resources
argocd app wait web --health --sync # block until synced + healthy (CI-friendly)
Worth knowing:
- Webhooks. By default Argo CD polls Git every 3 minutes. Configure a webhook (GitHub/GitLab/Bitbucket →
/api/webhook) and syncs become instant. The payload is validated viawebhook.*.secretinargocd-secret. - Refresh.
argocd app get web --refreshforces a re-compare instead of waiting for the poll. - Cascade deletes. The
resources-finalizer.argocd.argoproj.iofinalizer deletes all managed resources when an Application is deleted. Remove the finalizer first if you want to orphan the workloads. - Notifications. The bundled
argocd-notifications-controllersends sync/health events to Slack, email, webhooks, and more — subscribe per-app with thenotifications.argoproj.io/subscribe.<trigger>.<service>annotation. - Ignorable differences.
spec.ignoreDifferencessuppresses fields that controllers mutate in-cluster (HPA-managedreplicas, admission-injected labels) so they stop churningOutOfSync.
Argo CD vs Flux
Flux is the other major GitOps engine — lighter, more composable, excellent for platform teams who prefer Git-driven everything including the tooling itself. Argo CD's UI, RBAC, and multi-tenancy make it friendlier for teams and app developers. Both implement the same four principles; you can't go far wrong either way.
The payoff
Once every production resource traces to a commit, operations stops being archaeology. Audits become git log. Disaster recovery becomes git clone + argocd app sync. Onboarding a cluster is pointing an agent at a repo. That's the real promise of GitOps: operations with the same rigor — and the same tools — as the code itself.
Related Articles
- CI/CD Pipelines Explained: From Commit to Production
- GitHub Actions vs GitLab CI vs Jenkins: How to Choose
Last Updated: October 2026 Author: CloudOpsGuide Team Difficulty: Advanced Estimated Reading Time: 25 minutes