GitOps with ArgoCD and Flux¶
What You'll Learn¶
- The core GitOps principle — git as the single source of truth, reconciled by a pull-based controller instead of a push-based pipeline
- How to install ArgoCD and define a real
Applicationthat syncs a repo to a cluster - How to install Flux and define a real
KustomizationandHelmRelease - When GitOps is worth adopting over the pipeline model from CI/CD Pipelines for Kubernetes, and when it isn't
Why This Matters¶
Every pipeline example so far ends with the CI runner directly calling kubectl or helm against the cluster, using credentials it holds. That's push-based delivery, and it has two structural weaknesses: the CI system needs standing write access to production, and nothing detects or corrects drift if someone changes the live cluster by hand afterward. GitOps fixes both by moving the "who applies changes" role inside the cluster itself.
Mental Model¶
Push-based CI/CD (file 1 in this section): a pipeline, running outside the cluster, pushes changes into it when a build succeeds. The cluster is passive; the pipeline is the source of action.
GitOps: a controller running inside the cluster continuously watches a Git repository and reconciles the live state to match it — pulling, not being pushed to. Git becomes the literal source of truth: to change what's running, you commit to Git, and the controller notices and applies it. There is no separate "deploy" credential to protect, because the controller already has cluster access and Git access is all that's gated.
flowchart LR
subgraph Push["Push-based (CI/CD)"]
CI[CI runner] -->|kubectl apply, holds cluster creds| K1[Cluster]
end
subgraph Pull["Pull-based (GitOps)"]
Git[(Git repo)] -->|watched by| GC[GitOps controller in-cluster]
GC -->|reconciles| K2[Cluster]
K2 -.->|drift detected & reverted| GC
end
The drift-correction property is what push-based pipelines fundamentally can't offer without extra tooling: if someone runs kubectl edit deployment by hand, a GitOps controller notices the live state no longer matches Git and reverts it on the next reconciliation loop (typically within minutes), whereas a CI pipeline has no way to even know that happened.
ArgoCD: install and a real Application¶
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/v2.12.4/manifests/install.yaml
kubectl -n argocd rollout status deployment/argocd-server --timeout=180s
# Retrieve the initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath='{.data.password}' | base64 -d
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: checkout-api
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/example-org/checkout-api-config.git
targetRevision: main
path: overlays/production
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true # delete resources removed from Git
selfHeal: true # revert manual drift automatically
syncOptions:
- CreateNamespace=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
kubectl apply -f checkout-api-application.yaml -n argocd
argocd app get checkout-api
argocd app sync checkout-api # force an immediate reconciliation
argocd app history checkout-api # every past sync, tied to a Git commit
prune: true and selfHeal: true are what make this genuinely GitOps rather than just "kubectl apply, but triggered by ArgoCD" — without selfHeal, ArgoCD only reports drift; with it, ArgoCD actively reverts it.
Flux: install and a real Kustomization + HelmRelease¶
flux check --pre
flux bootstrap github \
--owner=example-org \
--repository=fleet-infra \
--branch=main \
--path=clusters/production \
--personal
flux bootstrap does something ArgoCD's basic install doesn't: it commits Flux's own manifests into your Git repo and wires the cluster to track that repo going forward — the bootstrap step itself is a GitOps action.
# clusters/production/checkout-api-source.yaml
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: checkout-api-config
namespace: flux-system
spec:
interval: 1m
url: https://github.com/example-org/checkout-api-config.git
ref:
branch: main
---
# clusters/production/checkout-api-kustomization.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: checkout-api
namespace: flux-system
spec:
interval: 5m
sourceRef:
kind: GitRepository
name: checkout-api-config
path: "./overlays/production"
prune: true
targetNamespace: production
For a chart-based deployment instead of raw manifests, Flux's HelmRelease points at a chart source and drives helm upgrade --install for you on every reconciliation:
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: bitnami
namespace: flux-system
spec:
interval: 1h
url: https://charts.bitnami.com/bitnami
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: checkout-redis
namespace: production
spec:
interval: 10m
chart:
spec:
chart: redis
version: "20.x"
sourceRef:
kind: HelmRepository
name: bitnami
namespace: flux-system
values:
architecture: standalone
auth:
enabled: true
existingSecret: checkout-redis-auth
flux get kustomizations
flux get helmreleases -n production
flux reconcile kustomization checkout-api --with-source
ArgoCD vs. Flux¶
| ArgoCD | Flux | |
|---|---|---|
| Interface | Full web UI plus CLI, app-centric view | CLI/CRD-first; UI available via Weave GitOps or ArgoCD-style add-ons |
| Core unit | Application |
Kustomization and/or HelmRelease |
| Helm support | Renders Helm as a source type inside Application |
Native HelmRelease CRD with its own reconciliation |
| Multi-cluster fleet management | ApplicationSet generators |
Flux's Kustomization/GitRepository composition, or Flux's multi-tenancy features |
| Ecosystem fit | Pairs naturally with Argo Rollouts (same project, same CRDs) | Pairs naturally with Flagger (both CNCF, commonly deployed together) |
Both are graduated CNCF projects solving the same reconciliation problem; the choice usually comes down to whether your team wants a UI-centric app view (ArgoCD) or a more Kubernetes-native, CLI/CRD-composable toolkit that leans on existing HelmRelease/Kustomization primitives (Flux) — and which progressive-delivery tool from Progressive Delivery you're already leaning toward, since Argo Rollouts and Flagger pair naturally with their respective GitOps sibling.
When to Pick GitOps Over a Traditional Pipeline¶
| Signal | Favors |
|---|---|
| Single cluster, small team, deploys are infrequent | Traditional CI/CD pipeline (file 1) — GitOps overhead may not pay for itself yet |
| Many clusters/environments that must stay consistent | GitOps — one Git repo per environment, reconciled identically everywhere |
| Compliance requires an audit trail of exactly what changed and when | GitOps — Git history is the audit trail |
Manual kubectl drift is a recurring incident cause |
GitOps — selfHeal/Flux reconciliation actively corrects it |
| CI system holding standing cluster credentials is a security concern | GitOps — the in-cluster controller holds credentials, CI only needs Git write access |
| Deploys need complex pre-deploy steps (schema migrations, external API calls) that don't map cleanly to "apply this manifest" | Traditional pipeline, or a hybrid: CI still handles migrations, GitOps handles manifest application |
Most mature setups end up hybrid: CI still owns build/test/scan/push (file 1), while the final "apply this to the cluster" step is a Git commit that a GitOps controller picks up, rather than a kubectl apply the pipeline runs directly.
Common Mistakes¶
- Running ArgoCD or Flux without
selfHeal/enforced reconciliation and then still allowing routine manualkubectlchanges — you get GitOps's dashboard without its drift protection. - Pointing a GitOps controller at the same repo that holds application source code without separating "code" from "desired cluster state," making it hard to review infrastructure changes independently of app changes.
- Forgetting that a GitOps controller reconciling on an interval (e.g., Flux's
interval: 5m) means a bad commit stays live until the nextgit revertand reconciliation — GitOps doesn't remove the need for review before merge. - Granting the GitOps controller's ServiceAccount cluster-admin instead of scoping it to only the namespaces/resources it should manage.
- Treating ArgoCD/Flux adoption as a replacement for CI entirely — they reconcile manifests, they don't build, test, or scan images.
Interview Questions¶
- What specifically makes GitOps "pull-based" as opposed to a CI pipeline running
kubectl apply? - What does
selfHeal: truedo in ArgoCD, and what's the operational risk of running without it? - Compare ArgoCD's
Applicationmodel to Flux'sKustomization/HelmReleasemodel — what's structurally different? - Under what circumstances would you keep a traditional push-based pipeline instead of moving to GitOps?
See Interview Prep for full answers.
Next¶
Continue to Production Engineering for the reliability practices — SLOs, capacity planning, disaster recovery — that build on top of a working, automated delivery pipeline.