Stop deploying by running commands at your cluster. GitOps makes Git the single source of truth and lets a controller continuously reconcile reality to match it, so rollback is a git revert, every change is audited, and drift heals itself. Here's the model, the trade-offs, and a real Argo Application.
Someone ran a kubectl apply at 11pm to fix an incident. Nobody wrote it down. Three weeks later the cluster behaves differently than the YAML in your repo says it should, and no one can explain why. This is configuration drift, and it's the slow rot at the heart of imperative deployment, every kubectl edit, every manual scale, every quick fix moves the live system away from any written record of it.
GitOps fixes this by inverting the flow. Instead of *pushing* changes to the cluster, you commit the desired state to Git and let a controller *pull* it, continuously reconciling the live cluster to match. Git becomes the single source of truth, your deploy history becomes your git log, and rollback becomes git revert. This article builds that model and shows it with a real ArgoCD Application.
Who this is for
Engineers running things on Kubernetes who deploy with scripts or CI that runs kubectl, and want auditable, self-healing delivery. Comfort with Git and basic k8s manifests is assumed; we explain the GitOps parts from scratch.
The one-sentence definition
GitOps is operating your infrastructure by declaring its desired state in Git and letting an automated controller continuously make reality match that declaration.
Two words carry the weight. Declarative: you describe the end state you want (10 replicas of v2), not the steps to get there. Reconciliation: a controller constantly compares desired (Git) to actual (cluster) and closes the gap, forever. It's a thermostat, not a light switch.
🌡️ You set the target temperatureCommit desired state to Git
🔁 The thermostat reads the room, constantlyController diffs Git vs cluster
🔥 It heats or cools to close the gapReconcile: apply changes to match Git
🪟 Someone opens a window (drift)Manual kubectl edit
♨️ It corrects automaticallySelf-heal back to Git's state
GitOps is a thermostat for your cluster.
The reconciliation loop
The GitOps loop. Developers never touch the cluster directly, they open a pull request against the config repo. The controller (ArgoCD or Flux) lives inside the cluster, watches Git for the desired state and the cluster for the actual state, and reconciles any difference. The dashed arrows are the continuous watch; the solid arrow is the apply that closes drift.
1
A change starts as a pull request
You don't run kubectl. You edit a manifest in the config repo and open a PR, the same review, approval, and audit trail as any code change.
2
The merge is the deploy
When the PR merges to the main branch, Git's desired state has changed. That's the only action a human takes.
3
The controller notices the diff
Living inside the cluster, the controller polls or is webhook-notified that Git now differs from the running cluster.
4
It reconciles
The controller applies whatever is needed to make the cluster match Git, creating, updating, or deleting resources.
5
It keeps watching, forever
If anyone hand-edits the cluster, the controller sees the drift and pulls it back to Git's truth (self-heal). The repo always wins.
Pull vs push: why the direction matters
Traditional CI/CD pushes: your pipeline holds cluster credentials and runs kubectl apply from the outside. GitOps pulls: a controller inside the cluster reaches out to Git and applies changes itself. That flip sounds small but it changes your security posture and your guarantees.
Push (CI runs kubectl)
Pull (GitOps controller)
Who applies changes
External CI pipeline
Controller inside the cluster
Cluster credentials live
In CI (broad blast radius)
Inside the cluster only
Drift detection
None, fire and forget
Continuous; self-heals
Source of truth
Whatever last ran
Git, always
Rollback
Re-run an old pipeline
git revert
The pull model keeps cluster credentials inside the cluster and makes drift detectable.
Pro tip
The credentials point is underrated. With push, every CI runner needs cluster-admin-ish access, so a compromised pipeline is a compromised cluster. With pull, the cluster credentials never leave the cluster, CI only ever needs write access to a Git repo.
ArgoCD vs Flux
The two dominant controllers do the same job with different personalities. ArgoCD ships a polished web UI and an explicit Application resource, great for teams who want to *see* sync status and drift. Flux is leaner, more composable, and Git-native to a fault, great for teams who want everything, including Flux's own config, to live in YAML with no dashboard.
ArgoCD
Flux
UI
Rich web dashboard
CLI-first, minimal UI
Core unit
Application CRD
Kustomization + sources
Multi-tenancy
Projects, RBAC, SSO built in
Via namespaces + RBAC
Image automation
Add-on
Built-in image updater
Feels like
A deploy console
A set of Unix tools
Both are CNCF graduated and production-proven. Pick on UI preference and how composable you want it.
A real ArgoCD Application
Here's the resource that wires it all together. This Application tells ArgoCD: watch this repo path, keep this namespace in sync with it, and heal any drift automatically. Once this exists, you never deploy this app by hand again, you change the repo.
application.yaml
yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: web
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/acme/cluster-config.git
targetRevision: main # the branch that is 'production'path: apps/web/overlays/prod # where this app's manifests livedestination:
server: https://kubernetes.default.svc
namespace: web
syncPolicy:
automated:
prune: true# delete resources removed from GitselfHeal: true# revert manual cluster edits back to GitsyncOptions:
- CreateNamespace=true
Read syncPolicy carefully, it's where GitOps gets its teeth. prune: true means deleting a manifest from Git deletes it from the cluster (Git is *complete* truth, not just additive). selfHeal: true means a hand-edit to the live cluster gets reverted on the next reconcile. Together they guarantee the cluster can never silently diverge from the repo.
verify.sh
bash
# See what Argo thinks: is the app in sync with Git?
argocd app get web
# Watch a reconcile happen live after you merge a PR
argocd app sync web --watch
# Roll back to any previous Git state, this is your 'undo'
argocd app history web
argocd app rollback web <revision>
Turn on selfHeal deliberately
selfHeal is wonderful until an incident, when someone urgently scales a deployment by hand and the controller helpfully reverts it 30 seconds later. The fix isn't to disable selfHeal; it's culture: in a GitOps world, the emergency fix is also a fast PR. Make merging trivial so nobody is tempted to fight the controller.
What GitOps actually buys you
Audit for free. Every change to production is a git commit, author, timestamp, diff, review, all in your history. No separate change log to maintain.
Rollback = git revert. Bad release? Revert the commit; the controller reconciles the cluster back. No special tooling, no remembering which pipeline run was good.
Self-healing. Drift from manual edits or partial failures is detected and corrected automatically. The cluster trends toward the repo, always.
Disaster recovery is reproducible. Lost the cluster? Point a fresh controller at the same repo and it rebuilds the declared state.
Least-privilege CI. Your pipeline only needs to write to Git, not to the cluster, shrinking the blast radius of a compromised runner.
Common mistakes that cost hours
Putting app code and config in the same repo. Mixing them means every code commit churns the deploy controller. Keep a separate config repo (or at least a separate path) as the source of truth.
Committing plaintext secrets to Git. Git is now your source of truth, including your secrets if you're careless. Use Sealed Secrets, SOPS, or an external secrets operator. Never raw base64.
Forgetting prune, then wondering why deleted manifests linger. Without prune, removing a file from Git leaves the resource orphaned in the cluster. Git stops being complete truth.
Fighting selfHeal during incidents. Hand-editing a self-healing app starts a tug-of-war with the controller. Make emergency PRs fast instead.
No environment promotion strategy. Pointing dev and prod at the same branch means every merge hits production. Use separate paths, overlays, or branches per environment.
Treating the controller as fire-and-forget. GitOps controllers need monitoring too, a wedged ArgoCD silently stops reconciling, and drift creeps back in unnoticed.
Takeaways
GitOps in six lines
Declare desired state in Git; a controller continuously reconciles the cluster to match.
Pull beats push: cluster credentials stay in the cluster, and drift self-heals.
Every production change is a reviewed, audited git commit, for free.
Rollback is git revert; disaster recovery is point-a-controller-at-the-repo.
prune + selfHeal make Git the complete, authoritative truth, use them on purpose.
Keep config separate from app code, and never commit raw secrets.
Where to go next
GitOps sits on top of Kubernetes and Git workflows, strengthen both, then go deep on the controller:
This article covers concepts taught hands-on in the Cloud Engineer and DevOps career paths, with real terminal labs, production scenarios, and structured lessons.