DevOpsIntermediate

What Is GitOps? Principles, Workflow, and Why Teams Adopt It

GitOps means Git is the single source of truth for your infrastructure and deployments, reconciled continuously by an automated controller. Here's what that means in practice.

DevFieldGuideJuly 31, 20268 min read
Share:

GitOps is a specific, fairly narrow idea that gets used loosely: it means the desired state of your infrastructure and applications lives in Git, and a controller continuously makes the real system match it — rather than a person or a CI pipeline pushing changes directly.

The core idea in one sentence

Instead of a pipeline running kubectl apply (a push — something external reaches into the cluster and changes it), a controller running inside the cluster continuously pulls the desired state from a Git repository and reconciles reality to match it.

That's the entire mechanical difference. Everything else attributed to GitOps — auditability, easy rollback, drift detection — follows from that one architectural choice.

Push-based deployment vs. GitOps

AspectPush-based CI/CDGitOps
Who applies changesThe CI pipeline, using cluster credentialsA controller running inside the cluster, pulling from Git
Cluster credentialsMust be exposed to CI/external systemsNever leave the cluster — the controller already has them
Source of truthWhatever was last applied — pipeline logs, if you're luckyThe Git repository, always, by construction
Drift detectionNot automatic — nothing notices a manual changeAutomatic — the controller diffs desired vs. actual continuously
RollbackRe-run an old pipeline, or manually revertgit revert — the controller reconciles the old state back automatically

The credentials point is the one people underrate. In a push model, your CI system needs write access to production — a compromised CI pipeline is a compromised production cluster. In a GitOps model, nothing outside the cluster ever holds a production credential; the in-cluster controller pulls, it doesn't grant outside-in access.

The reconciliation loop

Developer opens a PRChanges a manifest, Helm values, or Kustomize overlay
PR reviewed and mergedSame review process as application code
Controller detects the diffPolls or gets notified of the new commit
ReconciliationController applies changes to match Git
Continuous drift checkAny manual cluster change gets reverted or flagged

That last step is what makes GitOps genuinely different from "we also keep our YAML in a repo." A GitOps controller doesn't just apply Git state once — it keeps checking. If someone runs kubectl edit directly against a GitOps-managed resource, the controller notices the live state no longer matches Git and either reverts it automatically or raises the diff as "OutOfSync," depending on configuration.

The four GitOps principles, as OpenGitOps actually defines them

The CNCF's OpenGitOps working group formalized this into four principles, worth knowing precisely rather than paraphrasing loosely:

  1. Declarative. The system's desired state is expressed declaratively — what the end state should look like, not a sequence of imperative steps to get there.
  2. Versioned and immutable. Desired state is stored in a way that enforces immutability, versioning, and a complete history — in practice, Git.
  3. Pulled automatically. Software agents automatically pull the desired state from the source, not a step where something external pushes changes in.
  4. Continuously reconciled. Agents continuously observe actual system state and attempt to apply the desired state, correcting drift as it's detected.

A repo full of YAML that a Jenkins job applies on merge satisfies principles 1 and 2 but fails 3 and 4 — that's push-based CI/CD with declarative config, not GitOps. The distinction matters because principles 3 and 4 are what give you automatic drift correction and a genuinely reliable audit trail; without them you just have version-controlled YAML that someone still has to remember to apply correctly.

Repository structure: app config vs. app source

Most real GitOps setups split application source code from deployment configuration into separate repositories (or at minimum, separate top-level directories with different access controls):

my-app/ ← application source repo src/ Dockerfile .github/workflows/ci.yml → builds + pushes an image, tags it my-app-config/ ← GitOps config repo environments/ staging/ deployment.yaml kustomization.yaml production/ deployment.yaml kustomization.yaml

The CI pipeline's actual job in a GitOps setup is narrower than in push-based CI/CD: build, test, produce an image, and open a PR against the config repo bumping the image tag. It never touches the cluster directly. The merge of that PR — a reviewable, auditable event — is what actually triggers a deployment, once the GitOps controller notices it.

Environment promotion becomes a Git operation

Promoting a build from staging to production is just merging (or cherry-picking) a config change from one environment's directory to another's — usually a PR from environments/staging to environments/production, or a tag/branch promotion pattern. The entire promotion history is Git history: who approved it, when, and exactly what changed, for free, without a separate deployment-tracking system.

Handling secrets in a GitOps repo

The four principles say desired state lives in Git — but real applications need secrets (database passwords, API keys) that genuinely shouldn't sit in plaintext in a version-controlled repository, even a private one. Two patterns solve this without breaking the "everything reconciles from Git" model:

  • Sealed Secrets (Bitnami) — a controller runs in-cluster with a private key; you encrypt a secret client-side into a SealedSecret custom resource that's safe to commit (only the in-cluster controller can decrypt it), and the controller decrypts it into a normal Kubernetes Secret at apply time.
  • External Secrets Operator — the Git repo stores only a reference (a name/path in AWS Secrets Manager, HashiCorp Vault, or similar), and a controller syncs the real value from the external store into a Kubernetes Secret at runtime. The actual secret value never touches Git at all, not even encrypted.
yaml
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: db-credentials
spec:
  secretStoreRef:
    name: aws-secrets-manager
    kind: ClusterSecretStore
  target:
    name: db-credentials
  data:
    - secretKey: password
      remoteRef:
        key: prod/db/password

External Secrets Operator is generally the safer default for teams already using a real secrets manager — it keeps one source of truth for the actual value (the secrets manager) rather than distributing encrypted copies across every environment's Git history.

Multi-tenancy: one controller, many teams

A single GitOps controller instance commonly manages applications for many different teams, which raises a real access-control question: should every team be able to see (and potentially sync) every other team's Application? Both major GitOps tools solve this with namespace- and project-scoped RBAC — Argo CD's "AppProjects" restrict which repos, clusters, and namespaces a given Application is even allowed to reference, so a team's config repo genuinely can't accidentally (or maliciously) target another team's namespace, regardless of what someone commits to it.

Key takeaways
  • Git is the single source of truth; a controller reconciles reality to match it — not a pipeline pushing changes on your behalf.
  • Real GitOps requires both automatic pulling and continuous reconciliation — declarative YAML applied by CI alone is not GitOps by the OpenGitOps definition.
  • Secrets need a dedicated pattern (Sealed Secrets or External Secrets Operator) — never commit plaintext values, even to a private repo.
  • Multi-team GitOps needs project-level RBAC scoping from day one, not retrofitted after the first cross-team incident.

What GitOps doesn't solve

Common mistakes
  • Treating GitOps as a replacement for CI. It replaces the deployment/apply step specifically — you still need a real CI pipeline to build, test, and produce artifacts before GitOps ever gets involved.
  • Storing secrets in plaintext in the GitOps config repo because "it's just internal." Git history is effectively permanent; use a sealed-secrets approach or an external secrets operator that syncs from a real secret store, never raw values in the manifests themselves.
  • Assuming automatic reconciliation means automatic safety. A bad commit merged to the config repo gets reconciled just as faithfully as a good one — GitOps gives you a fast, auditable rollback (git revert), not immunity from deploying bad configuration in the first place.
  • Splitting config across too many repos too early. A small team with one or two services usually doesn't need the same multi-repo structure a platform team managing dozens of services does — start with a structure you can actually maintain.

Why teams actually adopt it

Best practices
  1. Audit trail for free. "Who changed this and why" is answered by git log and PR history — no separate deployment-tracking tool to maintain.
  2. Faster, safer rollback. git revert plus automatic reconciliation is materially faster than re-running an old pipeline or manually undoing a change under pressure.
  3. No standing production credentials outside the cluster. The single biggest security improvement — nothing external needs write access to production.
  4. Drift stops being invisible. A manual kubectl edit used to be silent. With GitOps, it's either auto-corrected or surfaced as an explicit out-of-sync state.

GitOps is a pattern, not a specific product — Argo CD and Flux are the two dominant implementations of it on Kubernetes, and the next article covers Argo CD specifically: how it implements these same four principles as a real, running controller.

Frequently Asked Questions

DevFieldGuide
DevFieldGuide

Editorial Team

Practical tutorials and developer tools, written and maintained by the DevFieldGuide team.

Enjoyed this article?

Get the next one straight to your inbox, along with the best of what we publish each week.

Related Articles

More in DevOps

View all