A Practical Guide to GitOps for Kubernetes Users

When it comes to GitOps, teams often reduce it to “dumping YAML into Git” or “running kubectl apply inside a CI pipeline.” Worse yet, some teams prematurely build complex internal platforms—only to find that delivery hasn’t accelerated, while operational overhead has doubled.

To unlock the real value of GitOps, the key is embracing KISS (Keep It Simple, Stupid), YAGNI (You Aren’t Gonna Need It), and the Boring Technology principle, stripping delivery workflows down to their essentials.


What Is GitOps

The term GitOps was coined by Weaveworks in 2017 and later codified into four core principles by the CNCF OpenGitOps Working Group.

At its core, GitOps requires declaring a system’s desired state declaratively, treating Git as the single source of truth. An in-cluster agent (such as Argo CD) continuously pulls the latest configurations and uses a reconciliation control loop to compare and adjust state, ensuring that the live cluster perpetually matches Git.

This continuous reconciliation loop delivers tangible improvements to everyday engineering workflows:


Common Misconceptions: What You Think Is GitOps Might Not Be

The most widespread misconception is confusing version-controlled Infrastructure as Code (IaC) with GitOps. Checking Dockerfiles and Kubernetes YAML manifests into a repository is merely the first step.

If engineers still run kubectl edit to hotfix production during an incident, or tweak environment variables in a cloud console, the live state has already drifted away from Git. As long as Git is not the sole legitimate path for state changes, it remains nothing more than a static backup of past intentions.

Another common anti-pattern is executing kubectl apply -f k8s/ directly inside CI scripts. This remains fundamentally push-based external automation: once the CI pipeline finishes executing, it disconnects. It cannot detect subsequent out-of-band manual changes in the cluster, nor does it establish a continuous feedback loop to remediate configuration drift.


Architecture Mechanics: Push-Based Pain Points vs. Pull-Based Defense

To appreciate the engineering value of GitOps, one must examine the architectural divergence between the traditional push-based CI model and the pull-based agent model.

Push-Based Shortcomings in Security and Operations

In a push-based model, the CI system acts as the active executor. After code is merged, the CI runner builds the container image, retrieves a kubeconfig stored in pipeline secrets, and directly calls the remote Kubernetes API server.

This topology introduces severe, unavoidable security and operational liabilities:

  1. High blast radius for credential leaks: The CI environment must hold highly privileged production credentials. A compromise in a third-party CI Action or build runner grants attackers direct keys to the cluster.
  2. Network boundary penetration costs: Production Kubernetes API servers typically reside within private VPC subnets. Allowing SaaS CI platforms to push deployments requires punching holes in firewalls or managing self-hosted runners inside the VPC.
  3. Blindness to configuration drift: CI runs are ephemeral and trigger-based; once the deployment step completes, the process exits. If an operator subsequently applies manual changes directly to the cluster, CI remains unaware until a future deployment overwrites or conflicts with those changes.

Pull-Based Advantages and Defense-in-Depth

The pull-based model deploys a persistent reconciliation operator (such as Argo CD or Flux) inside the Kubernetes cluster, continuously comparing Git declarations against the live state.

This paradigm offers substantial architectural benefits:


Tooling Landscape: Argo CD vs. Flux v2 Head-to-Head

Within the CNCF GitOps ecosystem, Argo CD and Flux (Flux v2) stand as the two premier Graduated projects. They embody distinctly different architectural philosophies.

Contrasting Architectural Philosophies

Argo CD follows the path of a centralized visual dashboard and collaboration hub. With an intuitive web UI, live resource dependency trees, built-in SSO integrations, and native diff and log viewers, it dramatically lowers the barrier to cross-functional collaboration.

Flux v2, by contrast, embraces Kubernetes-native APIs and the Unix philosophy. Rather than providing a monolithic dashboard, it decomposes functionality into focused micro-controllers (Source, Kustomize, Helm, etc.). Its minimal memory footprint makes it ideal for edge computing or veteran teams who favor CLI-driven workflows.

Feature Comparison and Practical Selection

Evaluation DimensionArgo CDFlux v2Practical Verdict
Web DashboardFeature-rich UI with real-time diffs and logsNo native UI; relies on third-party community toolsArgo CD drastically cuts on-call and cross-team communication overhead
Architecture & CRD DesignCustom Application and AppProject modelsAligns strictly with K8s primitives; highly modularFlux v2 is lightweight and flexible: micro-controller architecture facilitates lean customization
Learning CurveGentle and intuitive; what you see is what you getModerate; requires mastering multiple controllers and CLI commandsArgo CD drives easier adoption; product engineers can inspect sync status autonomously
Memory FootprintModerate (~500 MB to 1.5 GB)Minimal (~100 MB to 300 MB)In typical cloud clusters, this difference is generally negligible
Multi-Tenancy & RBACBuilt-in SSO with granular UI/API permissionsRelies on native K8s RBAC and ServiceAccountsArgo CD is better suited for mid-to-large teams and complex access models

Under the Boring Technology principle, Argo CD is the recommended choice for 90% of standard engineering organizations. A unified visual interface allows developers, QA, and operations to align on a single pane of glass; the collaboration friction it eliminates far outweighs its modest resource footprint. Reserve Flux v2 for resource-constrained edge environments or teams where every engineer already possesses deep Kubernetes operational fluency.


Configuration Architecture and Environment Isolation: Trunk-Based Development with Kustomize

When teams struggle with GitOps, the primary stumbling block is rarely installing the tools. Far more often, it stems from flawed repository layouts and problematic branching models.

Core Principle: Strictly Decouple App Repos from Config Repos

Separating application source code from Kubernetes deployment configurations into distinct repositories is your first defense against delivery headaches:

Do Not Use Git Branches for Environments

Historically, many teams maintained long-lived branches like dev, staging, and prod, configuring their GitOps operator to track each branch separately. This is a hazardous anti-pattern.

Environments naturally have configuration divergences, such as replica counts and domain names. Merging across branches forces teams to continually resolve painful merge conflicts over environment-specific settings.

Worse, when an urgent hotfix is merged directly into production and cherry-picking across lower branches is overlooked, environment baselines drift apart, permanently eroding configuration integrity.

Best Practice: Trunk-Based Development with Kustomize Directory Isolation

The pragmatic architecture is simple: maintain a single main branch in your configuration repository, isolating environments into distinct directories using Kustomize’s base/ and overlays/ structure.

Kustomize embodies the KISS principle: it avoids complex templating engines, relying entirely on plain declarative patches to override specific fields. Because it is built directly into kubectl, it requires no package registries or artifact servers.

A canonical directory layout looks like this:

 k8s-config-repo/
 +-- apps/
   +-- payment-service/
     +-- base/
     |   +-- deployment.yaml     # Common base definition
     |   +-- service.yaml        # Common service definition
     |   +-- kustomization.yaml  # Declares base resources
     +-- overlays/
       +-- dev/
       |   +-- kustomization.yaml  # Overrides replicas/tags
       +-- staging/
       |   +-- kustomization.yaml  # Applies staging configs
       +-- prod/
           +-- kustomization.yaml  # Pins prod limits
           +-- patches/
               +-- replicas.yaml   # Prod replica count

Once the CI build produces a new container image, it updates the target environment tag using kustomize edit set image inside the pipeline.

To balance automation with security guardrails, production pipelines typically use a scoped GitHub App or Deploy Key to open a pull request—or automatically merge it into main after passing automated pre-checks. The in-cluster operator then detects and reconciles the new release.

Secrets (passwords, API tokens, certificates) must never enter Git in plaintext. Native Kubernetes Secrets are merely Base64-encoded strings, not encrypted data.

The industry-standard pattern is storing secrets in dedicated vaults (such as AWS Secrets Manager or HashiCorp Vault) and synchronizing them into the cluster via the External Secrets Operator (ESO). Git stores only remote key references, keeping plaintext credentials completely out of version control.


Avoiding Pitfalls: Five Common Anti-Patterns

When adopting GitOps, teams frequently stumble into traps that look appealing on the surface but prove costly in production.

  1. Unreconciled “break-glass” emergency interventions: During an outage, engineers manually patch live resources, only for auto-sync to overwrite their fixes and trigger cyclical flapping—or they disable auto-sync and forget to re-enable it. The Remedy: Establish a strict break-glass protocol. Any manual hotfix must be followed by a matching Git PR merged within 30 minutes.
  2. Committing dynamic runtime state back to Git: Writing Horizontal Pod Autoscaler (HPA) replica counts back to Git creates an infinite sync loop. The Remedy: Git stores only desired state, not runtime metrics. When using HPA, omit the replicas field from deployment manifests, or configure Argo CD’s ignoreDifferences with RespectIgnoreDifferences=true.
  3. Over-engineering dependency DAGs: Packaging CRDs, infrastructure operators, and business microservices into a single monolithic GitOps application leads to race conditions and ordering headaches. The Remedy: Decouple infrastructure dependencies from application workloads into layered tiers, keeping dependency chains linear and simple.
  4. Prematurely adopting automated canary deployments and rollbacks: Forcing complex progressive delivery operators onto small-scale services with low traffic often stalls rollouts over minor statistical noise. The Remedy: Follow YAGNI. Kubernetes’ native RollingUpdate combined with well-tuned readinessProbe health checks handles the vast majority of production workloads effortlessly.
  5. Jumping on the platform engineering bandwagon: Building proprietary developer portals or home-grown abstraction layers before mastering the basics. The Remedy: Git itself is already the ideal interface. A mature Pull Request workflow delivers battle-tested reliability without unnecessary layers of indirection.

Conclusion: Making Production Releases Boring and Reliable

The ultimate goal of continuous delivery is to make production releases mundane and predictable. Deployments should no longer rely on engineers hand-typing commands late into the night. Instead, every change becomes a standard Pull Request during normal working hours, verified through peer review and seamlessly synchronized by an in-cluster agent.

Adopting GitOps does not require building an elaborate internal developer platform on day one. Start with a single stateless service, isolate configuration into a dedicated repository, organize environments cleanly with Kustomize, and place your delivery pipeline on transparent, reversible rails. Doing this unlocks eighty percent of GitOps’ real value. Return to common sense and stick to KISS, YAGNI, and Boring Technology—your system will prove far more resilient than any over-engineered alternative.