GitOps Explained: Why Your Git Repository Should Be Your Single Source of Truth
In our previous posts, we covered the case for infrastructure automation, how Terraform and Ansible work together, and lessons from running Kubernetes in production. All of those practices converge on a single idea: your infrastructure and application state should be declared in code, versioned in git, and automatically reconciled.
That idea has a name. It is called GitOps.
What Is GitOps?
GitOps is an operational model where git is the single source of truth for declarative infrastructure and application configuration. A GitOps agent running in your environment continuously compares the desired state (what is in git) with the actual state (what is running) and reconciles any differences.
The term was coined by Weaveworks in 2017, and while it originated in the Kubernetes ecosystem, the principles apply broadly.
The four core principles:
- Declarative configuration. The entire system is described declaratively — Kubernetes manifests, Helm charts, Kustomize overlays, Terraform HCL.
- Versioned and immutable. The desired state is stored in git, giving you a full history of every change, who made it, and why.
- Pulled automatically. An agent in the target environment pulls the desired state and applies it. Changes are not pushed from CI.
- Continuously reconciled. The agent does not just apply once — it runs a reconciliation loop, detecting and correcting drift.
graph LR
subgraph Push Model
D1[Developer] -->|push| CI1[CI Server]
CI1 -->|kubectl apply| K1[Cluster]
end
subgraph Pull Model - GitOps
D2[Developer] -->|push| G[Git Repo]
A[ArgoCD] -->|watch| G
A -->|sync| K2[Cluster]
end
Push vs. Pull: Why It Matters
Traditional CI/CD follows a push model: a CI pipeline builds your code, runs tests, and then pushes the result to the target environment (via kubectl apply, helm upgrade, ssh, etc.).
GitOps follows a pull model: a controller running inside the target environment pulls the desired state from git and applies it locally.
This distinction has profound implications:
Security
In the push model, your CI system needs credentials to access your production cluster. That means your CI runners — which also run untrusted build steps, third-party test frameworks, and dependency installations — hold the keys to production. A compromised CI runner is a compromised production environment.
In the pull model, the GitOps agent runs inside the cluster and only needs read access to your git repository. Your CI system never touches production. The blast radius of a CI compromise is limited to the build environment.
# Push model (traditional CI/CD)
Developer -> Git -> CI Pipeline -> [kubectl apply] -> Production Cluster
^
CI needs cluster credentials (risky)
# Pull model (GitOps)
Developer -> Git <- GitOps Agent (ArgoCD/Flux) -> Production Cluster
^
Agent only needs git read access
Auditability
Every change to production is a git commit. You can answer “what changed at 3 AM on Saturday?” by looking at git log. You can answer “who approved this change?” by looking at the pull request. You can answer “what was running in production last Thursday?” by checking out the commit from that date.
This is not just good practice — it is a compliance requirement in many industries. In our first post about the cost of not automating, we discussed how manual changes create audit gaps. GitOps closes them entirely.
Rollback
In traditional CI/CD, rolling back means re-running an old pipeline, or manually patching the deployment, or hoping your container registry still has that old image tag.
In GitOps, rolling back is git revert. The GitOps agent sees the new commit (which restores the previous state) and reconciles. Your cluster returns to its prior configuration. It is simple, fast, and uses the same mechanism as every other change.
GitOps in Practice: A Concrete Example
Let us walk through how a typical GitOps workflow looks with ArgoCD (the most widely adopted GitOps controller for Kubernetes).
Repository Structure
gitops-repo/
├── apps/
│ ├── production/
│ │ ├── api/
│ │ │ ├── kustomization.yaml
│ │ │ ├── deployment.yaml
│ │ │ ├── service.yaml
│ │ │ └── ingress.yaml
│ │ └── web/
│ │ ├── kustomization.yaml
│ │ ├── deployment.yaml
│ │ ├── service.yaml
│ │ └── ingress.yaml
│ └── staging/
│ ├── api/
│ │ └── kustomization.yaml
│ └── web/
│ └── kustomization.yaml
├── platform/
│ ├── cert-manager/
│ ├── ingress-nginx/
│ ├── monitoring/
│ └── external-secrets/
└── clusters/
├── production.yaml
└── staging.yaml
The repository is organized by concern: apps/ for application workloads, platform/ for shared infrastructure components, and clusters/ for per-cluster ArgoCD Application definitions.
The Deployment Flow
- A developer opens a pull request that updates the image tag in
apps/production/api/deployment.yaml:
# Before
image: registry.example.com/api:v1.4.2
# After
image: registry.example.com/api:v1.5.0
-
The PR is reviewed and approved. Reviewers can see exactly what will change in production — it is a readable YAML diff.
-
The PR is merged to the main branch.
-
ArgoCD detects the new commit within seconds (via webhook or polling).
-
ArgoCD computes the diff between the desired state (git) and the actual state (cluster).
-
ArgoCD applies the changes. The deployment rolls out with the standard Kubernetes rolling update strategy.
-
If the rollout fails (health checks fail, pods crash-loop), ArgoCD marks the application as “Degraded” and alerts fire.
-
To roll back, someone opens a PR that reverts the image tag change. Same process, same review, same audit trail.
What CI Still Does
GitOps does not eliminate CI. It redefines its scope:
- CI is responsible for: building container images, running tests, scanning for vulnerabilities, pushing images to the registry, and — optionally — updating the image tag in the GitOps repository.
- CD is handled by the GitOps agent. CI no longer runs
kubectl applyorhelm upgrade.
A typical CI pipeline ends with a commit to the GitOps repo:
# GitHub Actions example — final CI step
- name: Update image tag in GitOps repo
run: |
git clone https://github.com/org/gitops-repo.git
cd gitops-repo
yq -i '.spec.template.spec.containers[0].image = "registry.example.com/api:${{ github.sha }}"' \
apps/production/api/deployment.yaml
git add .
git commit -m "deploy: api ${{ github.sha }}"
git push
This “image updater” step can also be handled automatically by tools like ArgoCD Image Updater, which watches your container registry and updates the GitOps repo when new images appear.
GitOps vs. Traditional CI/CD: Summary
| Aspect | Traditional CI/CD | GitOps |
|---|---|---|
| Deployment trigger | CI pipeline push | Git commit (pull) |
| Source of truth | CI pipeline config | Git repository |
| Drift detection | None (fire and forget) | Continuous reconciliation |
| Rollback mechanism | Re-run old pipeline | git revert |
| Production credentials | Held by CI system | Held by in-cluster agent |
| Audit trail | CI logs (often ephemeral) | Git history (permanent) |
| Multi-cluster | Complex pipeline logic | Agent per cluster, same repo |
Common Concerns
“Won’t this slow us down?” No. The review process adds a few minutes, but the deployment itself is faster because the agent is always running and applies changes immediately on merge. More importantly, the time saved on incident response and debugging far outweighs the review overhead.
“What about secrets?” Secrets should never be stored in plain text in git. Use Sealed Secrets, SOPS, or (our preference) the External Secrets Operator, which syncs secrets from Vault, AWS Secrets Manager, or similar backends into Kubernetes.
“What about database migrations?” Migrations are a special case — they are imperative (run once, in order) rather than declarative. We handle them as Kubernetes Jobs triggered by Helm hooks or ArgoCD sync waves, which we cover in our next post about building a production ArgoCD setup.
Getting Started
If you are already running Kubernetes, adding GitOps is straightforward:
- Create a dedicated git repository for your cluster state.
- Install ArgoCD or Flux in your cluster.
- Move one application’s manifests into the git repo and create an ArgoCD Application for it.
- Observe, iterate, and expand.
Start with a non-critical service. Get comfortable with the workflow. Then expand to cover all applications and platform components.
The Bigger Picture
GitOps is not just a deployment technique. It is a way of thinking about operations. When git is the source of truth, you gain version history, code review, branch-based workflows, and a clear contract between “what should exist” and “what does exist.”
Combined with the infrastructure automation we discussed in our earlier posts — Terraform for provisioning, Ansible for configuration, Kubernetes for orchestration — GitOps completes the picture. Everything is code. Everything is reviewed. Everything is auditable.
In our next post, we will go deep on ArgoCD: multi-cluster management, ApplicationSets, RBAC, SSO integration, sync waves, and the operational patterns that make GitOps work at scale.
At robto, we implement GitOps workflows that give teams confidence in every deployment. From initial setup to enterprise-scale multi-cluster architectures, we help you make git the source of truth for your entire infrastructure.