feature/gitops-migration git push Build & Test lint · unit · scan ✓ passed Container Registry sha256:a3f9… Update Git Manifests image: v2.4.1 ArgoCD Sync ⟳ watching… prod continuous reconciliation loop drift detected → auto-correct desired state always wins GitOps · Thought Parameters
Back to Blog
DevOps 7 min read

GitOps in Practice: Moving Beyond CI/CD to True Continuous Delivery

Most teams that say they're doing "continuous delivery" are actually doing continuous integration with a deployment step bolted on at the end. A pipeline runs, tests pass, an artifact gets pushed to production. That's better than nothing. But it's not what Jez Humble and Dave Farley meant when they wrote the book on the subject, and it's missing properties that matter a lot at scale.

GitOps is a different model. It's not a replacement for CI — you still need to build and test code before it ships. But it changes how the deployment side of that equation works, in ways that solve real problems that push-based pipelines can't.

The Core Idea

GitOps has a deceptively simple premise: your Git repository is the single source of truth for what should be running in your infrastructure. Not what a pipeline last pushed. Not what someone kubectl apply'd manually during an incident. Not what you think is running based on a status dashboard. What is committed to Git.

A GitOps operator — ArgoCD and Flux are the two dominant implementations in the Kubernetes ecosystem — runs inside your cluster and continuously compares the desired state (what's in Git) against the actual state (what's running). Any divergence triggers a reconciliation. The cluster converges back to what Git says it should be.

This is a pull-based model rather than push-based. Instead of a pipeline reaching out to your cluster and applying changes, the cluster reaches into your Git repository and pulls them. That inversion has significant consequences.

What Push-Based CD Gets Wrong

The problems with traditional push-based pipelines aren't obvious until you've operated one at scale for a while. Here's what you run into:

Drift Goes Undetected

In a push-based model, the pipeline runs when code changes. Between pipeline runs, nothing checks whether the running state still matches what was deployed. A developer applies a quick config change manually during an incident and forgets to commit it. A Kubernetes admission controller gets updated outside of the deployment process. A node gets recycled and comes back running a slightly different configuration.

None of these trigger a pipeline. None of them show up anywhere until they cause a problem. GitOps eliminates this class of drift because the reconciliation loop runs continuously — typically every few minutes — and will detect and correct any divergence from the desired state.

Cluster Credentials Live in the Pipeline

To push changes to a cluster, a CI/CD pipeline needs credentials to reach it. Those credentials live in your CI system — GitHub Actions secrets, Jenkins credentials, whatever. This creates a large attack surface. If your CI system is compromised, the attacker has credentials to your production cluster. For regulated environments, this is increasingly a hard compliance problem.

In a GitOps model, the operator runs inside the cluster and pulls from Git. The cluster needs read access to the Git repository. The CI system never touches the cluster directly. The credential attack surface is dramatically smaller.

Deployment History Is Fragmented

With push-based pipelines, your deployment history lives in the pipeline tool. Your configuration history lives in Git. These are two separate systems with two separate audit trails, and correlating them when debugging a production incident is painful. In GitOps, every deployment is a Git commit. The entire history of what ran in production, when it changed, and who approved it is in the same place as the code itself.

How It Works in Practice

A typical GitOps setup separates application code from deployment configuration into two repositories:

The separation matters for a few reasons. It means you can roll back a deployment by reverting a commit in the config repo without touching the app repo. It means environment promotion (staging → production) is a pull request against the config repo, with all the review and approval workflows that implies. And it means the config repo becomes a clear audit trail of exactly what was running in each environment at any point in time.

If you can't describe your infrastructure state as code checked into version control, you don't really know what's running in production.

ArgoCD vs. Flux

These are the two mature options in the Kubernetes GitOps space, and the choice between them is less important than it might seem — both are excellent. A few practical differences:

ArgoCD has a better UI out of the box, which is genuinely useful for debugging sync issues and visualizing the application graph. It has a more opinionated app-of-apps pattern for managing multiple applications. If you want non-engineers to be able to see deployment state, ArgoCD's dashboard is the better starting point.

Flux is more Kubernetes-native in its architecture — it's built as a set of controllers that follow the operator pattern closely. It's better suited for multi-tenancy scenarios and has tighter integration with image update automation (automatically opening PRs when new image tags are detected). If you're heavily GitOps-native from the start and want the most composable setup, Flux is worth the steeper initial curve.

For most teams starting out: ArgoCD. The visibility it provides makes debugging the inevitable early sync failures much less painful.

Getting the Config Repo Structure Right

The biggest implementation mistake I see is treating the config repo as an afterthought. Teams dump raw Kubernetes YAML in a flat directory and discover six months later that managing fifty services across three environments is a nightmare.

The structure that holds up well at scale organizes by cluster and environment first, then application:

clusters/
  staging/
    apps/
      api-service/
      worker/
  production/
    apps/
      api-service/
      worker/
base/
  api-service/    # shared manifests, Kustomize base
  worker/

Kustomize overlays handle the differences between environments — resource limits, replica counts, feature flags, image tags. Helm works too; the key is having a consistent layering pattern that makes environment differences explicit and auditable.

Handling Secrets

The obvious question: if everything is in Git, what do we do with secrets? You clearly can't commit raw credentials to a repository.

The answer is sealed secrets or external secrets — two different approaches that both solve the problem without compromising the GitOps model:

For anything beyond a small team, External Secrets Operator is the right answer. The operational cost of Sealed Secrets grows uncomfortable as you scale the number of clusters and secrets.

The Migration Path

Most teams don't start with GitOps — they migrate to it. The migration doesn't have to be big-bang. A workable approach:

  1. Stand up ArgoCD in your cluster and connect it to an app-of-apps Git repository
  2. Pick one low-risk service and move its deployment manifests into the config repo
  3. Point ArgoCD at it and let it take over. Disable or remove the deployment step in that service's CI pipeline
  4. Watch it for two weeks. Debug sync issues. Get comfortable with the model
  5. Migrate remaining services incrementally

The two-week observation period sounds slow but it's worth it. The mental model shift from push to pull is non-trivial, and it's better to internalize it on a low-stakes service than to discover edge cases during a production incident on your core API.

What You Gain

After six months with GitOps fully adopted, here's what teams consistently report:

None of these are hypothetical. They're the practical payoff of a model that treats infrastructure state as version-controlled data, not as an emergent property of the last pipeline run.

CI/CD got us here. GitOps takes us the rest of the way.

Migrating to GitOps or improving your CI/CD setup?

We've built and migrated GitOps setups on GCP and AWS. Let's talk through what your migration looks like.

Get in touch