Back to Blog
DevOps15 min readJun 2026

GitOps: Declarative Delivery with ArgoCD & Flux

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.

GitOpsArgoCDFluxKubernetesDevOps
SB

Sri Balaji

Founder · TheSimplifiedTech

On this page

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.

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

merge PRwatch desiredwatch actualapply / healbump tag (CI)
Developers

Open a PR

Config Repo

Desired state (Git)

GitOps Controller

ArgoCD / Flux

Kubernetes

Actual state

Image Registry

New image tags

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

    It reconciles

    The controller applies whatever is needed to make the cluster match Git, creating, updating, or deleting resources.

  5. 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 changesExternal CI pipelineController inside the cluster
Cluster credentials liveIn CI (broad blast radius)Inside the cluster only
Drift detectionNone, fire and forgetContinuous; self-heals
Source of truthWhatever last ranGit, always
RollbackRe-run an old pipelinegit 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.

ArgoCDFlux
UIRich web dashboardCLI-first, minimal UI
Core unitApplication CRDKustomization + sources
Multi-tenancyProjects, RBAC, SSO built inVia namespaces + RBAC
Image automationAdd-onBuilt-in image updater
Feels likeA deploy consoleA 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 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

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

  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:

Want to go deeper?

This article covers concepts taught hands-on in the Cloud Engineer and DevOps career paths, with real terminal labs, production scenarios, and structured lessons.