Blog
October 14, 2020 Marie H.

GitOps in Practice with ArgoCD

GitOps in Practice with ArgoCD

Photo by <a href="https://unsplash.com/@andreasfelske?utm_source=cloudista&utm_medium=referral" target="_blank" rel="noopener">Andreas Felske</a> on <a href="https://unsplash.com/?utm_source=cloudista&utm_medium=referral" target="_blank" rel="noopener">Unsplash</a>

GitOps in Practice with ArgoCD

GitOps is one of those terms that gets explained badly. Let me give you the one-sentence version: Git is the source of truth for your infrastructure state, and a controller in the cluster continuously reconciles the live state to match what's in Git. That's it. Everything else is implementation detail.

ArgoCD is currently the best implementation of that idea for Kubernetes. Here's how I use it.

Updated March 2026: ArgoCD has matured significantly. ApplicationSet (now stable in ArgoCD v2.3+) is the standard way to manage many applications with templated definitions — it largely replaces the manual App of Apps pattern for most use cases. The ArgoCD v2.x line also brought improvements to notifications, multi-source applications, and the UI. The core concepts in this post remain accurate; syntax details may vary on current versions.

Pull vs Push

Most CI/CD systems use a push model: a pipeline runs, it authenticates to the cluster, and it applies changes. The cluster is a target that the pipeline writes to. The problem with push is that there's no reconciliation — if someone applies a manual change to the cluster (kubectl apply in a panic at 2am), your pipeline doesn't know. Your Git repo says one thing, the cluster is running another.

GitOps is a pull model. A controller running inside the cluster watches a Git repository. When the repo changes, the controller pulls the new desired state and applies it. When someone makes a manual change to the cluster, the controller detects drift and either alerts or automatically reverts it, depending on your sync policy.

This shift in direction matters operationally. The cluster is always moving toward the state described in Git. Git is auditable, diffable, and reviewable. Production changes go through pull requests.

ArgoCD Architecture

ArgoCD has three main components:

  • Application Controller: The heart of the system. Watches Kubernetes resources and compares live state to desired state from Git. Performs sync operations.
  • Repo Server: Clones and caches Git repositories, renders Helm charts or Kustomize manifests, and serves the rendered output to the application controller.
  • API Server: The gRPC/HTTP API that the CLI and UI talk to. Handles auth, RBAC, and webhooks.

Installing ArgoCD:

kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

For production, use the Helm chart and configure HA replicas for the application controller and API server. The default install is fine for evaluation.

Defining an Application

The core ArgoCD primitive is the Application CRD. It says: watch this Git repo at this path, and keep the cluster in sync with it.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/my-org/my-app-config
    targetRevision: main
    path: kubernetes/overlays/production
  destination:
    server: https://kubernetes.default.svc
    namespace: my-app
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
    - CreateNamespace=true

repoURL and path point to where your Kubernetes manifests live. targetRevision can be a branch, tag, or commit SHA. Using a branch means the cluster tracks it continuously; using a SHA means the cluster is pinned to a specific version until you update the Application.

prune: true means resources deleted from Git get deleted from the cluster. Without it, ArgoCD will add resources but never remove them.

selfHeal: true means if someone manually changes a resource in the cluster, ArgoCD will revert it within a few minutes. This is the "enforcement" half of GitOps.

Sync Policies: Manual vs Automated

Automated sync is what you want for most cases, but there's a valid argument for manual sync on production for the first time you adopt GitOps. Manual sync means a human triggers each deployment — ArgoCD detects the diff and shows you what will change, and you click sync (or run argocd app sync my-app). Good for building trust in the tooling.

Once you trust the system, automated sync is the right default. The whole point is that Git being updated is the deployment trigger. If you still need a human to approve every sync, you're adding back the manual step you were trying to eliminate.

App of Apps Pattern

When you have many services, you don't want to create each Application object by hand. The App of Apps pattern solves this: you create one top-level ArgoCD Application that points to a directory of other Application manifests.

# root-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/my-org/platform-config
    targetRevision: main
    path: argocd/applications
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

The argocd/applications directory contains Application manifests for each service. Adding a new application is a Git commit. Removing one is a Git commit. ArgoCD manages ArgoCD.

RBAC and SSO

ArgoCD has built-in RBAC, and you should configure SSO rather than handing out local accounts. It supports OIDC, so you can wire it to your existing identity provider (Okta, Dex, GitHub OAuth, etc.).

In the ArgoCD ConfigMap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
  namespace: argocd
data:
  url: https://argocd.internal.example.com
  oidc.config: |
    name: Okta
    issuer: https://my-org.okta.com
    clientID: <client-id>
    clientSecret: $oidc.okta.clientSecret
    requestedScopes:
    - openid
    - profile
    - email
    - groups

Then in argocd-rbac-cm, map groups to ArgoCD roles:

data:
  policy.csv: |
    p, role:developer, applications, get, */*, allow
    p, role:developer, applications, sync, */*, allow
    g, my-org:platform-team, role:admin
    g, my-org:developers, role:developer

Developers can view and sync applications. Platform team gets admin. Nobody shares credentials.

Secrets in GitOps

This is the part that trips people up. You can't store plaintext secrets in Git — that defeats the purpose of having secrets. Two common approaches:

Sealed Secrets: Encrypt secrets with a public key using the kubeseal CLI. The encrypted SealedSecret CRD is safe to commit. The Sealed Secrets controller in the cluster decrypts it. Simple setup, but the decryption key lives in the cluster — protect it.

Vault with ArgoCD: Use the Vault Agent Injector (or the ArgoVault plugin) to inject secrets at runtime. The Git repo contains references to Vault paths, not secret values. This is what I prefer for anything sensitive — secrets never touch Git at all.

Either way, the pattern is the same: Git contains either encrypted secret values or references to secrets stored elsewhere. Never plaintext.

Why GitOps Matters for Audit

For compliance and incident response, the Git history is invaluable. Every change to production is a commit with an author, a timestamp, and a diff. "What changed between Tuesday and Thursday?" is a git log command. "Who approved this deployment?" is the pull request. This is genuinely useful — not just a checkbox for compliance, but a real operational advantage when you're debugging.