GitOps Explained — What It Is and Why Teams Adopt It
GitOps in plain words — what it actually is, the workflow it enables, and a hands-on demo using Argo CD on a local Kubernetes cluster.
On this page
By the end of this post you'll have a clear definition of GitOps, a working mental model of how it changes deployment workflows, and a hands-on demo where you push a commit and watch a Kubernetes cluster reconcile to match. About 30 minutes.
You'll need: Docker installed, plus kubectl, kind, and a free GitHub account.
What GitOps is, in one paragraph
GitOps is a model for managing infrastructure where a Git repository is the source of truth for the desired state, and an automated agent continuously reconciles the live system to match. To change something, you commit to the repo. To roll back, you git revert. To audit who changed what when, you read the git log.
Three concrete properties fall out of this:
- No clicking around. All cluster state lives in YAML files in a repo, not in a console.
- Drift is detected automatically. If someone changes a Deployment manually, the agent notices and reverts (or alerts) — Git is supposed to be the truth.
- Rollbacks are trivial.
git revertrolls back the change; the agent picks it up and reverts the cluster.
That's GitOps. The rest is implementation.
Why teams adopt it
A few problems GitOps solves:
- Drift between Git and cluster. Engineers
kubectl editduring an incident to fix something. Next CI deploy reverts their change — sometimes during the next incident. - No audit trail. "Who changed this Deployment last and why?" requires cross-referencing CI logs, kubectl audit, and Git. None alone is enough.
- Painful rollbacks. Rolling back means running an old version of CI, which sometimes doesn't work because the CI tooling moved on.
GitOps gives you: cluster state always matches Git, every change is a Git commit (audit trail by default), rollback is git revert + sync. The trade is operational discipline (no more click-fixes) and an extra moving piece (the agent).
The two big GitOps tools
Both watch a Git repo and reconcile a Kubernetes cluster:
- Argo CD — UI-driven, easier for newcomers, the more popular choice
- Flux — CLI-first, lighter, deeper integration with Kubernetes API
We'll use Argo CD. The model is identical for Flux.
Step 1: Spin up a local cluster
kind create cluster --name gitops-tutorial
kubectl get nodes # should show one Ready node
Step 2: Install Argo CD
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
This drops Argo CD's deployments and services into the argocd namespace. Wait ~60 seconds for everything to start:
kubectl get pods -n argocd
You should see ~7 pods, all Running once they're ready.
Step 3: Access the Argo CD UI
kubectl port-forward -n argocd svc/argocd-server 8080:443
In another terminal, get the initial admin password:
kubectl get secret -n argocd argocd-initial-admin-secret \
-o jsonpath="{.data.password}" | base64 -d; echo
Open https://localhost:8080 (accept the self-signed cert warning), log in as admin with that password.
You should see Argo CD's dashboard with no applications yet.
Step 4: Create a Git repo for cluster state
We need a Git repo Argo CD can watch. Create a public repo on GitHub called gitops-demo (no README, no gitignore — keep it empty). Then locally:
mkdir gitops-demo && cd gitops-demo
git init
git branch -M main
git remote add origin https://github.com/<your-username>/gitops-demo.git
Create app.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: hello
template:
metadata:
labels:
app: hello
spec:
containers:
- name: app
image: nginx:1.27-alpine
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: hello
namespace: default
spec:
selector:
app: hello
ports:
- port: 80
Commit and push:
git add app.yaml
git commit -m "initial nginx deployment"
git push -u origin main
Step 5: Tell Argo CD about the repo
In the Argo CD UI:
- Click "+ NEW APP"
- Application Name:
hello - Project:
default - Sync Policy:
Automatic(check both prune and self-heal) - Repository URL: your GitHub URL (e.g.
https://github.com/yourname/gitops-demo) - Path:
.(the repo root) - Cluster: in-cluster
- Namespace:
default - Click "CREATE"
Argo CD reads the repo, sees the Deployment + Service, applies them to the cluster. Within ~30 seconds:
kubectl get pods -l app=hello
You should see two hello-... pods running. Argo CD did this — you didn't kubectl apply anything.
Step 6: Make a change via Git
Edit app.yaml locally — change replicas: 2 to replicas: 5. Commit and push:
git commit -am "scale to 5 replicas"
git push
Within ~3 minutes (Argo's default polling interval; you can also click "REFRESH" in the UI), Argo CD picks up the change and applies it. Check:
kubectl get pods -l app=hello
You should now see five pods. The cluster reconciled to match the Git state — automatically. No kubectl apply from you.
Step 7: Watch self-heal in action
Try to bypass GitOps:
kubectl scale deployment/hello --replicas=1
kubectl get pods -l app=hello # 1 pod (briefly)
Wait ~10 seconds and check again:
kubectl get pods -l app=hello
Five pods. Argo CD detected the drift (cluster says 1, Git says 5) and reverted. That's the self-heal mechanism — it's the GitOps promise made operational.
Step 8: Roll back
Decide that 5 replicas was too many:
git revert HEAD --no-edit
git push
Argo CD picks up the revert, scales back to 2.
That's the complete GitOps loop: edit, commit, push, watch the cluster converge. Same flow whether you're tweaking replicas or rolling out a new image tag.
Step 9: Clean up
kind delete cluster --name gitops-tutorial
When GitOps fits, when it doesn't
GitOps shines when:
- You have multiple services (~5+) on Kubernetes
- Multiple people deploy to the cluster
- Audit trail and rollback matter
- "Who changed what" is a recurring question
GitOps adds friction when:
- You have one service, one deployer
- You're still iterating on architecture daily
- Your cluster is short-lived dev/CI
For most production K8s teams above a small size, the friction is worth it.
Common mistakes
Trying to GitOps everything. Cluster bootstrap (Argo CD itself, CNI, etc.) doesn't fit cleanly — that's the chicken-and-egg problem. Use Terraform for the cluster shell, GitOps for what runs inside.
Keeping secrets in Git. Don't. Use sealed-secrets, External Secrets Operator, or Vault Agent — anything that keeps actual secret material out of the repo.
Auto-syncing everything to prod. A bug in the repo becomes a prod outage automatically. Production apps benefit from manual sync (PR auto-merges in Git, but a human clicks "Sync" to actually deploy).
Skipping the self-heal verification. "GitOps revert worked" should be tested before you trust it in an incident.
What to read next
You've seen the model. The next levels:
- GitOps with Argo CD: automating Kubernetes deployments — what changes after running this for two years across 40+ services
- Kubernetes 101: pods, deployments, and services — the K8s fundamentals GitOps builds on
- Terraform tutorial: your first IaC project — for the cluster-bootstrap layer beneath GitOps
GitOps isn't magic. It's a disciplined version of "Git is the truth," automated by a tool that watches and reconciles. Once a team is on it, the operational mode it enables — clean audits, easy rollbacks, no console-clicks — is hard to give up.
Your First CI/CD Pipeline with GitHub Actions
Walk through a working GitHub Actions workflow — install, test, build, deploy — for a tiny Node app. Every line explained.
Kubernetes 101 — Pods, Deployments, and Services Explained
Run your first three Kubernetes objects — Pod, Deployment, Service — on a local cluster, then understand why each one exists and how they fit together.
More from Infrastructure
Explore more articles in this category
AWS EU-SOUTH-2 Packet Loss: The Single-AZ Lesson Nobody Learned
A single Availability Zone in AWS's Spain region degraded for roughly 2.5 hours, and most "multi-AZ" architectures would have failed anyway.
Cloudflare's Clef: When a Decision Model Beats an LLM Call
Most moderation, routing, and triage calls aren't generation problems. Clef proves it by answering in 38.8 milliseconds instead of seconds.
Perplexity Left DynamoDB for CobbleDB: When Should You?
Perplexity built its own key-value store because DynamoDB's read path and bill stopped fitting 50 KB search items. Here is the checklist for when leaving is justified.
You might have missed
Evergreen posts worth revisiting.