Skip to main content
GitOps Explained — What It Is and Why Teams Adopt It

GitOps Explained — What It Is and Why Teams Adopt It

KU
• 7 min read•22 views

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.

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:

  1. No clicking around. All cluster state lives in YAML files in a repo, not in a console.
  2. Drift is detected automatically. If someone changes a Deployment manually, the agent notices and reverts (or alerts) — Git is supposed to be the truth.
  3. Rollbacks are trivial. git revert rolls 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 edit during 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

bash.bash
kind create cluster --name gitops-tutorial
kubectl get nodes  # should show one Ready node

Step 2: Install Argo CD

bash.bash
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:

bash.bash
kubectl get pods -n argocd

You should see ~7 pods, all Running once they're ready.

Step 3: Access the Argo CD UI

bash.bash
kubectl port-forward -n argocd svc/argocd-server 8080:443

In another terminal, get the initial admin password:

bash.bash
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:

bash.bash
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:

yaml.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:

bash.bash
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:

  1. Click "+ NEW APP"
  2. Application Name: hello
  3. Project: default
  4. Sync Policy: Automatic (check both prune and self-heal)
  5. Repository URL: your GitHub URL (e.g. https://github.com/yourname/gitops-demo)
  6. Path: . (the repo root)
  7. Cluster: in-cluster
  8. Namespace: default
  9. Click "CREATE"

Argo CD reads the repo, sees the Deployment + Service, applies them to the cluster. Within ~30 seconds:

bash.bash
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:

bash.bash
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:

bash.bash
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:

bash.bash
kubectl scale deployment/hello --replicas=1
kubectl get pods -l app=hello   # 1 pod (briefly)

Wait ~10 seconds and check again:

bash.bash
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:

bash.bash
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

bash.bash
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.

You've seen the model. The next levels:

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.

React
Share this post
KU

Kiril Urbonas

AI Engineer

560 articles
View all articles by Kiril Urbonas

You might have missed

Evergreen posts worth revisiting.