What Is GitOps, Really?
GitOps is not a tool. It's not a product you install. It's a set of practices that treat Git as the single source of truth for your entire system state — infrastructure, application configs, everything. The idea is deceptively simple: if it's not in Git, it doesn't exist. If something in your running environment doesn't match what's declared in Git, that's a drift problem, and something needs to reconcile it back.
The term was coined by Alexis Richardson of Weaveworks in 2017, but the underlying principles have roots in infrastructure-as-code and the immutable infrastructure movement. What GitOps adds on top of those ideas is the operational loop — a continuous reconciliation agent that watches your Git repository and drives your cluster toward the declared state, automatically and continuously.
ArgoCD is one of the most widely adopted tools that implements GitOps for Kubernetes. It runs inside your cluster, watches one or more Git repositories, and continuously compares the desired state (what's in Git) against the live state (what's actually running in the cluster). When there's a diff, ArgoCD can either alert you or automatically sync — depending on how you've configured it. That continuous comparison is the heartbeat of the whole model.
How ArgoCD Works Under the Hood
When you deploy ArgoCD into a Kubernetes cluster and create an
Applicationresource, you're essentially telling it: watch this Git repo, at this path, and make sure this cluster namespace reflects exactly what's declared there. Simple contract, surprisingly powerful consequences.
ArgoCD runs three core components. The Application Controller is the brain — it polls your Git repository at a configurable interval (default is three minutes, though webhooks make it near-instant) and compares the manifests in Git against the live cluster state using a three-way diff. The API Server exposes the ArgoCD API consumed by the UI, CLI, and any CI/CD integrations. The Repo Server handles cloning and rendering your manifests, with support for Helm, Kustomize, Jsonnet, and raw YAML.
The sync process itself is surgical. ArgoCD doesn't blow away your namespace and recreate everything from scratch. It generates a diff between the desired state rendered from Git and the observed state reported by the Kubernetes API, then applies only the delta. This matters enormously for stateful workloads — you don't want a database pod cycling just because ArgoCD ran a full reconciliation pass.
Here's a minimal ArgoCD
Applicationmanifest pointing at a realistic Git structure:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: platform-api
namespace: argocd
spec:
project: default
source:
repoURL: https://git.solvethenetwork.com/infra/k8s-manifests.git
targetRevision: main
path: apps/platform-api/overlays/production
destination:
server: https://kubernetes.default.svc
namespace: platform-api
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
A few things worth calling out. The
prune: trueflag means ArgoCD will delete resources from the cluster that no longer exist in Git. Without this, you end up with zombie resources drifting around in your namespace indefinitely — deployments no one manages, services pointing at nothing. The
selfHeal: trueflag is what gives ArgoCD its continuous reconciliation property: if someone manually edits a deployment in the cluster (a cardinal sin in GitOps land), ArgoCD will detect the drift and revert it within minutes. That's the enforcement mechanism. Git wins, always.
The Repository Structure That Actually Works
One of the most common questions I get from teams adopting ArgoCD is: how should I structure my Git repositories? There's no single right answer, but after seeing several teams go through painful migrations and hard-learned restructuring, one pattern consistently comes out ahead — the app-of-apps pattern.
The idea is that you have one root ArgoCD
Applicationthat points to a directory containing nothing but other
Applicationmanifests. ArgoCD syncs the root app, which creates all the child apps, which then sync their own workloads. It sounds recursive because it is. But it gives you a single entry point for bootstrapping an entire cluster from nothing — critical for disaster recovery and new environment provisioning.
k8s-manifests/
├── bootstrap/
│ └── root-app.yaml # The app-of-apps entry point
├── argocd-apps/
│ ├── platform-api.yaml
│ ├── monitoring.yaml
│ └── ingress-controller.yaml
└── apps/
├── platform-api/
│ ├── base/
│ │ ├── deployment.yaml
│ │ ├── service.yaml
│ │ └── kustomization.yaml
│ └── overlays/
│ ├── staging/
│ │ └── kustomization.yaml
│ └── production/
│ └── kustomization.yaml
└── monitoring/
└── ...
With Kustomize overlays, your staging and production environments share a common base but diverge on replica counts, resource limits, and environment-specific configuration references. This keeps your manifests DRY while making environment differences explicit, reviewable, and auditable through normal pull request workflows. Every change to production is a merge commit. Every rollback is a revert.
Why This Approach Matters Operationally
Here's where GitOps earns its keep. Think about a traditional deployment pipeline: a developer merges code, CI builds an image, a pipeline runs
kubectl applyor
helm upgradeagainst a cluster — often with long-lived credentials baked into the CI system. That cluster could be in any state. It may have been manually edited by someone debugging at 2am. The pipeline doesn't know, and it doesn't care. It applies its changes on top of whatever is there and moves on.
In a GitOps model, the cluster is always driving toward a declared state. No pipeline reaches into your cluster with credentials. ArgoCD, running inside the cluster, pulls from Git — not the other way around. Your CI system only needs write access to Git. It never touches Kubernetes directly. This is a fundamentally more secure architecture. The blast radius of a compromised CI credential is limited to the Git repository, not direct cluster access.
The operational benefits compound quickly. Every change to your cluster passed through a pull request, giving you a complete, reviewable history with author, timestamp, and diff. Reverting a bad deployment is a
git revertfollowed by a push, or clicking Rollback in the ArgoCD UI — ArgoCD syncs the previous commit's manifests back into the cluster within seconds. Losing a cluster is painful but no longer catastrophic: spin up a new one, point ArgoCD at the same repo, and within minutes everything is reconciled back to the desired state.
In my experience, the moment teams really internalize the value of this model is after their first major incident recovery. I've seen a botched Helm upgrade corrupt the state of three deployments in a production namespace. Because everything was in Git and managed by ArgoCD, the remediation was simple: revert the offending commit, push to main, watch ArgoCD sync. Total time from problem identification to cluster restored was under ten minutes. Without GitOps, that same recovery would have involved hand-editing YAML in a live cluster, cross-checking running configs against documentation, and hoping no one had made an undocumented change in the preceding weeks.
Connecting Repositories and Managing Secrets
For private repositories, ArgoCD stores credentials as Kubernetes secrets in its own namespace. You can add a repo via the CLI or — more in the spirit of GitOps — via a declarative secret:
apiVersion: v1
kind: Secret
metadata:
name: platform-manifests-repo
namespace: argocd
labels:
argocd.argoproj.io/secret-type: repository
stringData:
type: git
url: git@git.solvethenetwork.com:infra/k8s-manifests.git
sshPrivateKey: |
-----BEGIN OPENSSH PRIVATE KEY-----
... key material managed by external-secrets or sealed-secrets ...
-----END OPENSSH PRIVATE KEY-----
Secrets are where most teams hit their first real friction with GitOps. You obviously can't commit plaintext secrets to Git. The two most common solutions are Sealed Secrets and External Secrets Operator. Sealed Secrets encrypts a secret with a cluster-specific public key, producing a
SealedSecretresource that's safe to commit — only that cluster's controller can decrypt it. External Secrets Operator takes a different approach: it reads secret references from Git and fetches the actual values at runtime from an external store like HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault. I've used both in production. Sealed Secrets is simpler to operate and has fewer moving parts; External Secrets is the right call if you already have a centralized secret store and need to share secrets across multiple clusters.
Multi-Cluster Management with ApplicationSets
Single-cluster ArgoCD is useful. Multi-cluster ArgoCD is where the real operational leverage appears.
ArgoCD can manage multiple clusters from a single control plane. You register external clusters via CLI or declarative config, then target them in
Applicationresources using the cluster API URL instead of
https://kubernetes.default.svc. This works fine at small scale. But when you're managing ten, twenty, or fifty clusters — common in organizations running dev, staging, and multiple regional production environments — maintaining one
Applicationmanifest per app per cluster becomes a maintenance burden that defeats the whole point.
This is the problem
ApplicationSetwas built to solve. It's a higher-order resource that generates
Applicationobjects dynamically based on a generator — a list, a Git directory, or cluster labels:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: platform-api-all-clusters
namespace: argocd
spec:
generators:
- clusters:
selector:
matchLabels:
env: production
template:
metadata:
name: '{{name}}-platform-api'
spec:
project: default
source:
repoURL: https://git.solvethenetwork.com/infra/k8s-manifests.git
targetRevision: main
path: apps/platform-api/overlays/production
destination:
server: '{{server}}'
namespace: platform-api
syncPolicy:
automated:
prune: true
selfHeal: true
This single
ApplicationSetcreates one
Applicationfor every cluster labeled
env: production. Add a new cluster, label it correctly, and ArgoCD automatically starts managing
platform-apion it. Remove the label and the
Applicationis cleaned up. The operational overhead of fleet management drops dramatically. What previously required manual manifest creation per cluster per app becomes a label operation.
Closing the Loop with Image Updater
A question that comes up early in most GitOps adoptions: how does a new container image actually get deployed if ArgoCD only watches Git? Someone, or something, still needs to update the image tag in the manifests.
The Argo CD Image Updater extension handles this cleanly. It watches your container registry, detects new tags matching a semver constraint or a regex pattern, and commits the updated image tag back to your GitOps repository automatically. The Git history stays authoritative — every image rollout is a commit — but you don't need a human or a custom CI step to write the commit.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: platform-api
namespace: argocd
annotations:
argocd-image-updater.argoproj.io/image-list: api=registry.solvethenetwork.com/platform/api
argocd-image-updater.argoproj.io/api.update-strategy: semver
argocd-image-updater.argoproj.io/api.allow-tags: "^v[0-9]+\\.[0-9]+\\.[0-9]+$"
argocd-image-updater.argoproj.io/write-back-method: git
spec:
source:
repoURL: https://git.solvethenetwork.com/infra/k8s-manifests.git
targetRevision: main
path: apps/platform-api/overlays/production
With these annotations, Image Updater commits the new image tag back to Git every time a semver-compatible tag appears in the registry. ArgoCD detects the new commit and syncs the rollout. CI builds the image. Image Updater writes the tag to Git. ArgoCD deploys it. Each system does one job.
Common Misconceptions Worth Addressing
The first misconception I run into almost universally is that GitOps means everything has to be on Kubernetes. It doesn't. GitOps is a set of principles. You can apply them to Terraform state, Ansible playbooks, or Flux managing raw VM configurations. ArgoCD specifically is Kubernetes-native, but GitOps as a practice is not tied to any particular runtime.
The second misconception: automated sync means losing control. In my experience, this fear keeps teams running in manual sync mode indefinitely, which defeats most of the point. Automated sync with
selfHealis not ArgoCD doing whatever it wants. It means ArgoCD enforces what's declared in Git. You still control what goes into Git. The process is: open PR, review, merge, ArgoCD syncs. That's more control and more visibility than ad-hoc
kubectl apply, not less.
Third: ArgoCD replaces CI. It absolutely does not. CI handles building, testing, and publishing artifacts — container images, Helm chart versions, compiled binaries. ArgoCD handles deployment. They're complementary systems. A typical workflow is: CI builds and pushes an image tagged with the commit SHA, then either Image Updater or a CI step updates the image tag in the GitOps repo. ArgoCD detects the manifest change and rolls out the new image. Neither system knows or cares about the other's internals.
Fourth, and maybe most damaging in the long run: GitOps is only for mature teams. I'd argue the opposite is true. It's particularly valuable for teams that are still establishing operational discipline. Enforcing the Git-as-source-of-truth model early prevents the accumulation of undocumented snowflake configurations that turn into technical debt no one can unwind. Starting with good habits is always cheaper than cleaning up bad ones.
GitOps with ArgoCD is one of those operational patterns that, once you've built a real workflow around it, is genuinely hard to go back from. The combination of an immutable audit trail, instant rollback, continuous drift correction, and push-based security architecture makes cluster operations predictable in a way that imperative pipelines can't match. The learning curve is real — mostly concentrated around repository structure decisions and the secrets management problem — but the operational leverage makes it worthwhile. Get the repo structure right early, solve secrets once, and the rest largely takes care of itself.
