Control Repositories

What a Control Repository Is

A control repository is a git repository whose contents declare the desired state of a system — not how the system is built.

Why the Name Matters

“Source repository” usually implies source code — something that compiles, runs, serves requests, has bugs. The engineering discipline around source code is correspondingly heavy: automated tests, code review focused on correctness, CI pipelines, release engineering, semantic versioning.

A control repository is different. Its contents are not a program, though they might be generated by programs. They are instructions to a reconciliation controller about the desired state of some target system. The file format is usually YAML or JSON — data, not code. What matters is not whether the YAML “runs correctly,” because YAML doesn’t run. What matters is whether the resulting cluster state is correct.

Calling it a control repository makes the distinction explicit. Reviewers know they are evaluating configuration changes, not code changes. The standards and workflows adjust accordingly.

What a Control Repository Provides

From the git repository itself, without any additional tooling:

  • What changed — the diff
  • Who changed it — the commit author
  • When it changed — the commit timestamp
  • Why it changed — the commit message
  • How to undo itgit revert

This is the foundation of every operational property GitOps delivers. Every deploy is a commit. Every rollback is a revert. Every question about “what was running last Tuesday at 3pm” is a git log away. The repository is the audit trail.

Different Discipline Than Source Code

Source code repositories enforce discipline about how the code works — correctness, idiomatic style, test coverage, performance. A control repository enforces discipline about what the system should be — which workloads, which configurations, which versions.

A few specific differences:

Testing. Source code is tested with unit tests, integration tests, and mocks. Control repository changes are tested by rendering (for Helm, Jsonnet, or Kustomize) and running validators like kubeconform. “Does this YAML describe a valid Kubernetes resource?” is the extent of automated testing. The real test is whether the cluster converges successfully, which happens after merge. The results of the merge and its application to an environment might be tested by integration tests, functional tests, acceptance tests, audits, etc. That is beyond the control repository itself.

Review focus. In source code review, reviewers look for bugs, performance, and style. In control repository review, reviewers look at the diff and ask: “do I want this in production?” Style issues in YAML do not matter. Correctness of the desired state — RBAC grants, resource limits, security contexts — matters enormously.

Release cadence. Source code has releases. In a control repository, the merge commit to main is the release artifact; the act of committing to main is the release process. Every merge is a deployment. Tagging the merge commit with a semver label is a good idea — it gives the release a stable, human-readable name for incident reports, rollbacks, and audit trails — but the commit is the artifact, not the tag. The tag is a pointer. The commit is what ships. The resources the repository declares may also have their own versions (image tags, chart versions); those are separate concerns from the repository’s own release tags.

Tolerance for duplication. DRY is a principle that makes source code maintainable — duplication is a source of inconsistency bugs. In a control repository, duplication is often intentional. Two environment overlays may be 90% identical because they describe two distinct, independently managed systems. Deduplicating them couples the two environments, which is the opposite of what you want during a phased rollout.

The Head of Main Is the State of Things Now

The head of the tracked branch — whatever you call it, main is typical — is not a proposal, a draft, a snapshot, or “what we intend to deploy someday.” It is the state of things now. Right this second. What the controller sees, what the cluster is reconciling toward, what is running in production. Proposed changes live in branches, not on main.

This is the single most important invariant of a control repository. Every operational property depends on it:

  • git log on main is the history of the platform
  • git blame on main tells you who put each line into production
  • git revert on main rolls the platform back
  • git diff between two commits on main is the change that happened
  • A new engineer reading main understands what is running, today, without asking anyone

If any of these statements become untrue, the repository has stopped being a control repository. It has become a historical artifact or a proposal document — neither of which has operational value.

If you look at main and it does not reflect what is actually deployed, something is wrong: either the controller is stuck, or someone applied a change out-of-band, or changes are accumulating on a branch that has not merged. All three are incidents. The invariant is that the head of main describes reality, authoritatively, as of the last reconciliation interval.

Anything that weakens this invariant weakens the whole system.

Why Trunk-Based Development Is Required for Control Repositories

Branching and merging are not a stylistic choice in a control repository. They are a structural one. A control repository can only deliver its value if the head of main describes current reality — and long-running branches make that impossible.

In a source code repository, a long-running branch is expensive. Merge costs compound, diffs stop being informative, design incompatibilities surface late. But the repository still functions. The source code on main still compiles, still passes tests, still produces a runnable artifact. The branch is a liability on the shipping pipeline, not on the coherence of the repository itself.

A control repository has no such slack. If main declares the cluster runs v1.2.3 and a long-running branch declares it runs v1.2.4, neither statement is true about the cluster. The cluster runs whichever one the controller last applied. The repository has stopped being a source of truth for the system it controls. The invariant is broken.

The argument is structural:

  1. The head of main must describe the state of the system, authoritatively, right now.
  2. If work accumulates on branches that are not on main, the head of main is describing something that is not what is running, or not all of what is running, or is a stale version of what is running.
  3. Therefore, work must land on main continuously. Branches exist only long enough to be reviewed and merged.

That is trunk-based development. Not as an optimization, not as a best practice, not as a cultural preference — as a direct consequence of what a control repository is.

Environment-specific variation is expressed in the repository structure: overlays, values files, per-cluster directories. Promoting a change from staging to prod is an edit to a file on main, maybe a copy from one dir to another, not a merge from a long-running branch. Feature flags, phased rollouts, canary deployments — all solved at the configuration layer, not at the VCS layer. The trunk is always the truth.

The general case for trunk-based development is a companion piece, with citations and the usual objections. The case for control repositories specifically is simpler: you either have the head-of-main invariant or you don’t. If you do, you’re trunk-based. If you’re not trunk-based, you don’t have a control repository — you have configuration files in git with a broken relationship to what’s running and no clear overview to what is running right now.

What Control Repositories Are Not

Not a substitute for discipline about the system being controlled. The repository records what was requested. It does not validate that what was requested is wise. A change committing an overly permissive RBAC grant will be applied just as faithfully as a correct one. Review discipline on the diff is what prevents this — the same review discipline that prevents bad source code from shipping, applied to the shape of the cluster rather than the shape of a function.

Not self-documenting. A control repository describes what the cluster should be. It does not describe why the choices were made. Architectural decisions, design rationale, and operational runbooks live in separate documentation. The commit messages and PR descriptions carry the rationale for individual changes; they are not a substitute for documentation of the overall architecture.

Not a substitute for observability. The repository declares intent. Observability tells you whether reality matches. Metrics, logs, and traces are how you know the cluster is behaving as declared. A control repository in git plus a cluster you cannot observe is a dangerous combination — you know what you asked for, but not whether you got it. Observability that clearly shows when — and for how long — reality has diverged from the control repository is what closes the loop.

The Reconciliation Loop

A control repository alone is not GitOps. It is the input. You can push from your control repository to your live system. This is part of the solution, but not enough by itself.

You need a reconciliation loop to have a complete system: a control repository holding desired state, plus a controller inside the target system that continuously reconciles actual state against what the repository declares.

The repository is the declaration. The controller is the enforcement. Neither is sufficient on its own.

Without the controller, the repository is just configuration files in git — better than nothing, but subject to drift, stale applies, inconsistent state across environments, and all the problems that come from imperative deployment mechanisms. Without the repository, the controller has nothing to reconcile toward.

The value of GitOps — audit trail, reproducibility, drift correction, safe rollback — emerges from the combination. The control repository makes the first three possible. The controller makes them automatic, self-correcting, and measurable.

Practical Implications

If you are building or adopting a control repository for the first time, the operational model shifts in three important ways:

  1. No one applies changes directly. kubectl apply from laptops becomes forbidden. helm install or helm upgrade from CI pipelines becomes forbidden. Every change is a commit. The controller reconciles all changes. The controller might effectively apply, install or upgrade, but no one else does.

  2. Drift is a repository problem, not a cluster problem. If the cluster does not match the repository, the controller will correct it — unless the correction itself would break something, in which case the fix is a commit to the repository, not a manual edit to the cluster.

  3. Documentation lives alongside the config. READMEs, BOOTSTRAP guides, rationale for non-obvious choices — commit them in the repository. The next person to look at the repository should be able to understand the system from the repository alone.

The Point

A control repository is a git repository whose contents declare the desired state of some system, intended to be reconciled by an automated controller. Its discipline differs from source code repositories — reviews focus on what the system should be rather than whether the code works, duplication is often intentional, the state of main is the release artifact, and the act of changing main is the release process. Combined with a reconciliation controller, it provides audit trail, reproducibility, drift correction, and safe rollback as properties of the system, rather than as operational practices that people have to remember to follow.

The head of main is the platform. Keep it that way — see Trunk-Based Development for how.