Ask ten teams whether they do GitOps and eight will say yes. Ask what happens if somebody changes a replica count directly in the cluster, and the answers separate immediately.
Most of them mean: the config lives in git, and merging runs a pipeline that applies it. That is a perfectly good way to work. It is also not what the term means, and the difference shows up on exactly the day you needed it to.
What is the actual definition?
The term came out of Weaveworks around 2017 and was later formalised by the OpenGitOps project into four principles. Paraphrased:
Declarative. The system is described by its desired state, not by the steps to get there. A manifest saying "three replicas", not a script that scales to three.
Versioned and immutable. That desired state lives somewhere with history, where each version is a fixed artefact. Git is the obvious choice and the one in the name, but the principle is versioned storage, not git specifically.
Pulled automatically. Software agents pull the desired state, rather than a pipeline pushing it in.
Continuously reconciled. Agents keep observing actual state and keep correcting it toward desired state, on a loop, forever.
Three of those four describe what most teams already do. The fourth is where almost everyone stops, and it is the one that changes the properties of the system.
Why does the reconciliation loop matter so much?
Because a one-shot apply is only true at the moment it runs.
A pipeline that applies on merge is making a statement about that instant. Ten minutes later somebody scales a deployment by hand, and nothing anywhere notices. Git says three replicas. The cluster runs seven. Both are confident. The gap survives until the next unrelated deploy re-applies the manifest, which might be next Tuesday, and then it corrects silently in the middle of somebody else's change.
A reconciler runs continuously. It sees seven, wants three, and acts. Drift has a lifetime measured in the reconcile interval rather than in weeks.
That is the same class of problem as Terraform configuration drift, with one important difference: Terraform tells you drift happened and waits for a human. A GitOps reconciler fixes it without asking. Which is the point, and also the hazard.
What breaks when the reconciler fixes things you wanted broken?
Two in the morning. Something is failing. An engineer scales a deployment up by hand to absorb the load, and it works.
Ninety seconds later the reconciler scales it back down, because git still says three.
This is the most common first bad experience with GitOps, and it is not a bug. The system did exactly what it was configured to do. What was missing was a way to say "not right now".
Three things make this survivable:
A documented suspend. Argo CD and Flux both let you pause reconciliation for a specific application. Everyone on call has to know the command before the night they need it, not during.
Emergency changes go through git anyway. A one line commit on a branch, merged fast, is often quicker than people expect and leaves the record intact. This works when the pipeline is fast; it does not when merging takes twenty minutes of CI.
A shorter path than you think. If the emergency workflow is genuinely too slow, the honest fix is making the normal path faster, not disabling the reconciler.
Push or pull: does it actually matter?
The principle says pull. The practical difference is where credentials live, and that is worth being concrete about.
Push. CI runs the apply. Your CI system holds credentials that can change production. Every workflow file in the repository is potentially a path to those credentials, which is why a pull request that edits a workflow is a different kind of change from one that edits application code.
Pull. An agent runs inside the target environment, watches the repository, and applies what it finds. CI builds artefacts and updates manifests. It never holds production credentials, because it never talks to production.
For Kubernetes the pull model is well served: Argo CD and Flux both do this and both are mature.
For cloud infrastructure it is messier. Terraform and Terragrunt are not naturally pull-based, which is why almost every Terraform pipeline is push, including the one this blog describes. Various tools try to close this and none of them is the obvious default yet.
So a mixed setup is normal and not a failure. Pull for what runs in the cluster, push with tightly scoped short-lived credentials for the cloud resources underneath. Worth being honest that half of it is not GitOps by the strict definition.
What does the repository layout have to be?
One decision matters more than the rest: do application code and deployment manifests live together or separately?
Together. One repository, one pull request changes code and its deployment. Simple, and the reason most teams start here.
The problem arrives with the reconciler. When a build produces a new image tag and writes it back to the manifest in the same repository, that commit triggers CI, which builds again, which writes again. You end up adding a skip marker to commit messages, which works and is a smell.
Separately. Application repository builds and pushes an image. A separate config repository holds manifests. The build updates the config repository. The reconciler watches only that.
More moving parts, and it removes the loop. It also means the config repository's history is a clean deployment log, which is genuinely useful during an incident: one place to read, showing what was deployed when.
At one service, together. At twenty, separate.
How do secrets work if everything is in git?
They cannot be in git in plain text, and this is where a first GitOps attempt usually stalls.
Three approaches, in rough order of how much infrastructure they need:
Encrypted in the repository. Tools like SOPS or Sealed Secrets encrypt values so the ciphertext can be committed safely and only the cluster can decrypt. Everything stays in git, which keeps the model intact. The cost is key management, which is now yours.
References, not values. The manifest names a secret; an operator fetches the actual value from a real secret store at apply time. Git holds a pointer. Same shape as keeping environment variables as references rather than values.
Out of band entirely. Secrets are created by a separate privileged process and the manifests assume they exist. Simplest, and it puts a hole in the "git is the source of truth" claim that you should at least be honest about.
None is wrong. The wrong answer is not deciding, which usually ends with a base64 blob in a commit and a rotation nobody wants to do.
Do you actually need this?
Reconciliation earns its complexity when drift is likely and expensive.
Worth it when: several people can change the environment, environments are supposed to be identical and are not, you need to prove what was running last Thursday, or a rebuild from scratch has to be routine rather than heroic.
Not worth it yet when: one team, one environment, deploys are infrequent and everybody knows what is running. A reconciler here adds a component to operate and solves a problem you do not have.
The honest middle: adopt the first three principles now, because declarative config in version control is good practice regardless. Add the reconciler when somebody has been surprised by drift more than twice.
What does an audit actually see?
This is the part that sells GitOps internally, and it is a real property rather than a marketing one.
Every change is a commit. Every commit has an author, a timestamp, a diff and, if you require review, an approver. "Who changed this and when" is a git log rather than an investigation. That is the same question an approval gate exists to answer, answered by the storage layer instead of a process.
Two caveats worth stating before you promise this to anyone:
It only holds if nothing bypasses it. One person with direct cluster access and a habit of using it turns the audit trail into a partial record, which is worse than a known-incomplete one because it looks complete.
A reconciler that silently reverts is not the same as one that reports. If drift is corrected with no alert, the audit trail records the correction and not the fact that somebody made the change. You want both. Alert on reconciliation events, not just on failures.
The short version
Git plus a pipeline is version-controlled deployment, and that is already most of the value. GitOps adds a loop that keeps checking, and the loop is the whole difference.
Take the loop when drift is a real problem you can name. Skip it while it is not, and do not let the vocabulary decide the architecture.
DevLift generates infrastructure as code and opens a pull request rather than applying, so the change that ships is the change that was reviewed and git stays the record. Book a walkthrough, or read where the human gate belongs.