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
| Aspect | Push-based CI/CD | GitOps |
|---|---|---|
| Who applies changes | The CI pipeline, using cluster credentials | A controller running inside the cluster, pulling from Git |
| Cluster credentials | Must be exposed to CI/external systems | Never leave the cluster — the controller already has them |
| Source of truth | Whatever was last applied — pipeline logs, if you're lucky | The Git repository, always, by construction |
| Drift detection | Not automatic — nothing notices a manual change | Automatic — the controller diffs desired vs. actual continuously |
| Rollback | Re-run an old pipeline, or manually revert | git 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
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:
- 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.
- Versioned and immutable. Desired state is stored in a way that enforces immutability, versioning, and a complete history — in practice, Git.
- Pulled automatically. Software agents automatically pull the desired state from the source, not a step where something external pushes changes in.
- 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
SealedSecretcustom resource that's safe to commit (only the in-cluster controller can decrypt it), and the controller decrypts it into a normal KubernetesSecretat 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
Secretat runtime. The actual secret value never touches Git at all, not even encrypted.
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/passwordExternal 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.
- 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
- 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
- Audit trail for free. "Who changed this and why" is answered by
git logand PR history — no separate deployment-tracking tool to maintain. - Faster, safer rollback.
git revertplus automatic reconciliation is materially faster than re-running an old pipeline or manually undoing a change under pressure. - No standing production credentials outside the cluster. The single biggest security improvement — nothing external needs write access to production.
- Drift stops being invisible. A manual
kubectl editused 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.
Related reading
- Argo CD and GitOps: Continuous Delivery for Kubernetes — shares tags: devops, kubernetes (same category).
- CI/CD Pipelines Explained: From Commit to Production — shares tags: devops, git.
- Git Rebase vs. Merge: When to Actually Use Each — shares tags: git, devops.
- Infrastructure as Code: Why Terraform Won — shares tags: devops.
- Kubernetes ConfigMaps and Secrets: A Practical Guide — shares tags: kubernetes, devops.