DEV Community

Cover image for GitOps: Declarative Delivery with ArgoCD & Flux
Sri Balaji
Sri Balaji

Posted on • Originally published at thesimplifiedtech.com

GitOps: Declarative Delivery with ArgoCD & Flux

TL;DR: GitOps makes Git the single source of truth and lets a controller reconcile reality to match it, so rollback is a git revert, every change is audited, and drift heals itself. Includes ArgoCD vs Flux and the trade-offs.

Contents

Who pushed that change to production?

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.

In the real world In tech
🌡️ You set the target temperature Commit desired state to Git
🔁 The thermostat reads the room, constantly Controller diffs Git vs cluster
🔥 It heats or cools to close the gap Reconcile: apply changes to match Git
🪟 Someone opens a window (drift) Manual kubectl edit
♨️ It corrects automatically Self-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 cont

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.

💡 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

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 live
  destination:
    server: https://kubernetes.default.svc
    namespace: web
  syncPolicy:
    automated:
      prune: true       # delete resources removed from Git
      selfHeal: true    # revert manual cluster edits back to Git
    syncOptions:
      - CreateNamespace=true
Enter fullscreen mode Exit fullscreen mode

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

# 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>
Enter fullscreen mode Exit fullscreen mode

⚠️ 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

  1. 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.
  2. 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.
  3. 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.
  4. Fighting selfHeal during incidents. Hand-editing a self-healing app starts a tug-of-war with the controller. Make emergency PRs fast instead.
  5. 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.
  6. 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:


Originally published on TheSimplifiedTech, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.

Top comments (0)