Skip to content
GitOps

GitOps: governing environments and deploys without dark rituals 🧭

GitOps is one of those words people often use as an elegant synonym for “deploying through Git”. Unfortunately, committing a YAML file is not enough to make something GitOps. The real point is simpler and stricter: Git becomes the declarative source of truth for environments and configuration, while a controller continuously reconciles real state with desired state.

If you only need a pipeline that deploys software, start from CI/CD. If you want the environment to be described, reviewable, auditable, and automatically brought back to the expected state, then you are in GitOps territory.

What GitOps really is 🤔

In practice, GitOps means this:

  • the desired state of the environment lives in Git
  • every change goes through commit, review, and audit trail
  • an operator in the cluster or target environment reads the repository and applies the change
  • reconciliation is continuous, not “something that happened once during deployment”

The important difference from many classic pipelines is that you do not ask CI to push configuration everywhere. You ask a controller to observe the repository and converge toward the declared state.

When it makes sense ✅

GitOps works especially well when you have:

  • Kubernetes environments or other declarative platforms
  • multiple environments that must stay consistent
  • a need for a clear audit trail on configuration and deploys
  • teams that prefer pull requests and disciplined rollback over creative SSH sessions in production
  • a real need to detect and correct infrastructure or application drift

It makes much less sense when:

  • the environment is deeply manual or imperative
  • the team still lacks basic discipline around repositories, review, and branching
  • the target platform does not lend itself well to declarative state and reconciliation

Typical operating flow 🔄

A healthy flow, simplified, looks like this:

    flowchart LR
  A[Application commit] --> B[CI build and test]
  B --> C[Immutable artifact publication]
  C --> D[Manifest or Helm values update in Git]
  D --> E[Pull request and review]
  E --> F[Merge to main]
  F --> G[GitOps controller detects the delta]
  G --> H[Environment reconciliation]
  H --> I[Observability and verification]
  

Here CI produces artifacts, but it does not remain the owner of runtime state. Responsibility for desired state moves to the GitOps repository and the controller that watches it.

GitOps reconciliation model 🧠

The most important part of GitOps is not the commit. It is continuous reconciliation. In other words, the system does not “apply once and hope”. It keeps comparing desired state and real state, reports divergence, and, when configured to do so, corrects it automatically.

    stateDiagram-v2
  [*] --> DesiredInGit
  DesiredInGit --> Reconciling: new commit or refresh
  Reconciling --> Healthy: real state aligned
  Healthy --> Drifted: manual change / divergent state
  Drifted --> Reconciling: drift detected
  Reconciling --> Degraded: sync failed / blocking policy
  Degraded --> Reconciling: fix and retry
  Healthy --> [*]
  

That is why GitOps makes the most sense on declarative platforms: the controller must be able to observe, compare, and converge without inventing half of the logic at runtime.

Typical tools and components 🛠️

The most common building blocks are these:

  • Argo CD: widely used in Kubernetes, excellent for application delivery, sync policies, and state visibility.
  • Flux CD: a solid and modular approach, often chosen by teams wanting strong cloud-native integration.
  • Helm: useful as a packaging layer, but not equivalent to GitOps; by itself it is not enough.
  • Kustomize: great for environment overlays without turning values into a scavenger hunt.
  • External Secrets / Vault / SOPS: essential if you want to avoid storing plaintext secrets in Git like it is still 2009.

A minimal, very common stack looks like this:

  • CI that builds and publishes an immutable artifact
  • a registry for images or packages
  • a separate GitOps repository for manifests and environment values
  • a controller in the cluster with sync policy, health checks, and visible diff
  • observability on sync, drift, rollout, and rollback

Best practices that reduce unnecessary pain 🧱

1. Separate application code from runtime configuration 📦

Keeping everything in one repository can work for small teams, but in many cases it is better to distinguish between:

  • application source repositories
  • manifest or deployment configuration repositories
  • shared platform repositories where appropriate

The goal is not to create more repositories for sport. The goal is to avoid useless coupling and make ownership, review, and promotion across environments clearer.

2. Promote artifacts, do not rebuild 🚚

GitOps does not cancel a core CI/CD best practice: the artifact should be built once. Promotion across environments should change references, versions, or parameters, not recompile the world as if nothing had happened.

3. Treat secrets seriously 🔐

GitOps loves Git, but Git is not a secret manager. Use encryption tooling or vault integration and keep access tightly controlled. A repository full of credentials is simply a very efficient future incident report.

4. Make drift visible and governed 🧪

Drift should not only be corrected. It should first be detected. If someone changes a production resource by hand, you need to know immediately. Decide explicitly when self-healing is allowed and when human intervention is required.

5. Use policy and guardrails 🚧

GitOps without policy risks becoming nothing more than “fast automation of versioned mistakes”. Integrate:

  • admission policy
  • manifest validation
  • security checks
  • naming, tagging, and resource limit rules

6. Define a clear repository and promotion strategy 🗂️

One of the first decisions that ages badly when taken too quickly is repository structure. Some teams use a mono-repo with environment overlays, others separate application code, deployment configuration, and platform concerns. Neither choice is magical. What matters is that it remains coherent and governable.

Useful questions to settle early:

  • who is allowed to change production manifests?
  • does promotion across environments happen through merge, cherry-pick, or automated version bumping?
  • are environment-specific values few and readable, or are they already mutating into YAML folklore?

If every environment has its own special logic, GitOps stops simplifying things and starts collecting exceptions.

7. Do not turn GitOps into a dumping ground for responsibilities 🧯

GitOps is excellent for describing and reconciling desired state. It is terrible if you use it as a container for every automation idea that crosses your mind. Some concerns should stay outside the GitOps controller or be handled very carefully:

  • destructive one-off jobs
  • non-idempotent database migrations
  • heavy infrastructure provisioning in the same application layer
  • business logic disguised as configuration

The more the GitOps repository becomes a suitcase where everything gets stuffed, the more you lose operational clarity and reliable rollback.

Relationship with IaC and CI/CD 🔗

GitOps, CI/CD, and IaC are not competitors. They are different layers of the same story:

  • CI/CD builds, tests, and publishes artifacts
  • IaC defines infrastructure and services as code
  • GitOps uses Git as the source of truth and automatically reconciles desired state

A healthy combination looks like this: IaC creates clusters, network, storage, and identity; CI/CD builds artifacts; GitOps governs application configuration and continuous deployment behavior.

An easy way to read that split is:

  • IaC prepares the ground
  • CI/CD builds the package
  • GitOps decides which version should live in each environment and watches its state

Common mistakes to avoid 🚫

Mistaking GitOps for “kubectl apply from a pipeline”: if CI pushes manifests directly into the cluster, you are much closer to a traditional deployment pipeline than to GitOps.

Keeping secrets in plaintext in the repo: convenient until the day it turns into a very expensive lesson.

Unmanageable environment overlays: if every environment has 14 special cases, you do not have a declarative model. You have a hostile puzzle.

Automatic sync everywhere, all the time: auto-sync is powerful, but it must be governed. In production without guardrails, it can turn a mistake into a very efficient outage.

No observability on the controller: if you do not know why a sync failed or what was corrected, you are driving blind with a dashboard that is merely “very pretty”.

In short 🧾

GitOps works well when you want environments described in Git, continuous reconciliation, clear audit trails, and disciplined rollback. It is not a religion and it does not replace CI/CD or IaC. It completes them. Implemented well, it reduces manual configuration, silent drift, and those handcrafted deploy patterns that age badly.

Last updated on