InfraRunBook
    Back to articles

    GitOps and ArgoCD Explained

    CI/CD
    Published: Apr 8, 2026
    Updated: Apr 8, 2026

    A practical deep-dive into GitOps principles and how ArgoCD implements continuous reconciliation for Kubernetes clusters, covering architecture, repository patterns, and real-world operational workflows.

    GitOps and ArgoCD Explained

    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

    Application
    resource, 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

    Application
    manifest 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: true
    flag 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: true
    flag 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

    Application
    that points to a directory containing nothing but other
    Application
    manifests. 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 apply
    or
    helm upgrade
    against 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 revert
    followed 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

    SealedSecret
    resource 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

    Application
    resources 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
    Application
    manifest per app per cluster becomes a maintenance burden that defeats the whole point.

    This is the problem

    ApplicationSet
    was built to solve. It's a higher-order resource that generates
    Application
    objects 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

    ApplicationSet
    creates one
    Application
    for every cluster labeled
    env: production
    . Add a new cluster, label it correctly, and ArgoCD automatically starts managing
    platform-api
    on it. Remove the label and the
    Application
    is 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

    selfHeal
    is 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.

    Frequently Asked Questions

    What is the difference between GitOps and traditional CI/CD pipelines?

    Traditional CI/CD pipelines push changes into a cluster using outbound credentials stored in the CI system. GitOps inverts this model: a reconciliation agent running inside the cluster pulls desired state from Git and enforces it continuously. This means no cluster credentials in CI, a complete audit trail via Git history, and automatic drift correction when live state diverges from declared state.

    Does ArgoCD replace Helm or Kustomize?

    No. ArgoCD is a deployment and reconciliation engine that natively understands Helm charts, Kustomize overlays, and raw YAML. Helm and Kustomize handle manifest templating and patching; ArgoCD handles the deployment lifecycle, sync status, and rollback capability on top of whatever manifest format you're already using.

    How do you handle secrets in a GitOps workflow without committing them to Git?

    The two most common approaches are Sealed Secrets, which encrypts secrets with a cluster-specific public key so the encrypted form is safe to commit, and External Secrets Operator, which reads secret references from Git and fetches the actual values at runtime from an external store like HashiCorp Vault or AWS Secrets Manager. Sealed Secrets is simpler; External Secrets is better suited for multi-cluster environments with a centralized secret store.

    What is the app-of-apps pattern in ArgoCD?

    The app-of-apps pattern is a bootstrapping strategy where a single root ArgoCD Application points to a Git directory containing only other Application manifests. ArgoCD syncs the root app, which creates all child apps, which then sync their own workloads. This gives you a single entry point for provisioning an entire cluster from scratch — critical for disaster recovery and new environment onboarding.

    Can ArgoCD manage multiple Kubernetes clusters?

    Yes. ArgoCD supports multi-cluster management from a single control plane. You register external clusters and target them in Application resources. For fleet-scale management across many clusters, ApplicationSet generates Application objects dynamically based on cluster labels or other generators, eliminating the need to maintain one manifest per app per cluster manually.

    Related Articles