In a nutshell
If you are new to GitOps, here is the whole lesson in one breath: Argo CD and Flux are two tools that do the same job — keep your Kubernetes cluster matching what is written in Git — and they do it equally well. The question is never “which is better”, only “which one fits my team better”. One is not a beginner tool and the other an expert tool; they simply made different design choices, and this lesson names the handful of choices that actually matter.
The analogy that sticks: imagine two removal companies that both move your belongings from the same old house into the same new house, and both do it flawlessly. Argo CD is the full-service firm — it sends a supervisor, hands you an app to watch the truck on a map, staffs a concierge desk, and gives you one login and one point of contact. Flux is the specialist-crew model — you hire a packing crew, a driving crew, and an unpacking crew, coordinate them yourself with a checklist, and there is no app to stare at, just quiet crews that get it done. Your furniture ends up in exactly the same rooms either way. What differs is how much dashboard and how much do-it-yourself you want.
Level: Intermediate · Time: ~55 min
Prerequisites: you should already know what GitOps is — a controller inside the cluster pulls desired state from Git — from GitOps principles: push vs pull, and be comfortable reading a Kubernetes YAML manifest. After this you can: say in one sentence what each tool is, name the five axes where they genuinely diverge (UI, Helm semantics, image automation, multi-tenancy, multi-cluster), and defend a tool choice — or a decision to run both — to your team without hand-waving.
Sooner or later every team standardising on GitOps asks the same question: Argo CD or Flux? It is almost always asked as though one answer must be wrong. It is not. Both are Cloud Native Computing Foundation graduated projects — the CNCF’s highest maturity tier, the same shelf as Kubernetes, Prometheus, and Envoy — and both solve the identical core problem in the identical core way: a controller runs inside your cluster and continuously pulls desired state from Git, reconciling the live cluster toward it. Deploy the same application with each and the cluster ends up, object for object, the same.
This lesson sits inside a course about Argo CD, which is exactly why it leans the other way. A comparison you can trust has to be fair to the tool it is not selling — so we will be scrupulous about Flux’s real strengths, and it has several that Argo CD genuinely does not match. The point is not to crown a winner (there isn’t one) but to hand you a decision you can defend to your team: what each tool actually is, where the two diverge in ways that have consequences, and the small set of questions that should settle it.
We target Argo CD 2.13+/3.x and Flux 2.x on Kubernetes 1.29+. This machine has no cluster attached, so every command output below is representative and labelled as such — the manifests, CRDs, and commands are real and current, but the outputs illustrate shape, not a live run. If you have already read GitOps principles: push vs pull, the shared foundation here will feel familiar; this lesson is about what the two leading implementations of that idea do differently.
Why this matters
You will make this decision once and live with it for years. GitOps tooling is sticky: it wires into your repos, your RBAC, your CI hand-off, your on-call runbooks, and the muscle memory of every engineer who deploys. Picking on vibes — “Argo has a nice UI”, “Flux feels more Kubernetes-native” — and discovering the mismatch two years later is expensive. Picking on the right axes is cheap insurance.
The trap is treating this as a quality contest. It is not. Neither tool is buggier, slower, or “less GitOps” than the other; both are battle-tested at enormous scale (Flux underpins large fleets at cloud providers and enterprises; Argo CD runs some of the biggest single-pane GitOps deployments in existence). If someone tells you one is simply better, they are selling something. The honest framing is that they made different architectural bets, and those bets suit different teams.
Here is the one-sentence mental model to anchor everything: Argo CD is an application-centric platform you log into; Flux is a toolkit of composable controllers you assemble. Argo bundles a UI, an API, SSO, and multi-tenancy into one product; Flux gives you small Unix-philosophy controllers and delegates the UI and access control to Kubernetes and third parties. Almost every difference in this lesson is a downstream consequence of that single split.
So before the detail, the honest short answer — the things that, in practice, actually tip the decision:
| If this is your reality… | It tends to point to | Because |
|---|---|---|
| Developers need to see apps, diffs, sync status in a browser | Argo CD | First-class built-in UI; Flux ships none |
| You want the tightest possible Kubernetes-native, CLI/Git-only workflow | Flux | No server/UI to run; controllers + flux CLI + Git |
| Strong built-in multi-tenancy, RBAC, and SSO out of the box | Argo CD | AppProjects + argocd-rbac-cm + Dex/OIDC |
| Best-in-class automated image updates written back to Git | Flux | Dedicated image-reflector + image-automation controllers |
You want real Helm release semantics (helm list, hooks, rollback) |
Flux | helm-controller performs an actual Helm release |
| Fan-out to many clusters from one pane, with a UI | Argo CD | Hub-spoke + ApplicationSet + the web UI |
| Smallest attack surface / fewest moving server components | Flux | No API server or UI daemon to expose |
| Less-experienced team that benefits from guardrails and visibility | Argo CD | The UI lowers the GitOps learning curve |
Read those as tendencies, not laws — the rest of the lesson earns each row. And keep in mind the ending we are building toward: many mature orgs run both, because the two tools are good at different things.
Two tools, one job: pull-based GitOps
Start with what is identical, because it is most of what matters and it is easy to forget under the noise of the differences. Both tools implement the GitOps operating model: Git is the single source of truth for desired state, a controller in the cluster continuously reconciles live state toward Git, and drift is detected and (optionally) corrected automatically. Neither tool is a CI system; neither builds images. Both pull — nothing outside the cluster needs cluster credentials to deploy, which is the security property that makes GitOps attractive in the first place.
The guarantees they share are the whole reason GitOps works, and they are genuinely the same on both sides:
| GitOps guarantee | Argo CD | Flux | Notes |
|---|---|---|---|
| Git as source of truth | Yes | Yes | Desired state lives in Git, not in the cluster |
| Pull-based reconciliation | Yes (application-controller) | Yes (kustomize/helm-controller) | No external system holds cluster creds |
| Continuous drift detection | Yes | Yes | Both re-diff on an interval and on webhook |
| Automatic drift correction | Yes (selfHeal) |
Yes (default on Kustomization/HelmRelease) | Revert kubectl edit back to Git |
| Prune deleted resources | Yes (prune) |
Yes (prune: true) |
Remove-from-Git deletes from cluster |
| Rollback = revert Git | Yes | Yes | History is the Git log in both |
| Kustomize + Helm + plain YAML | Yes | Yes | Both render all three |
| Health assessment | Yes (built-in + Lua) | Yes (readiness / wait) |
Argo’s is richer out of the box |
| Not a CI system | Correct | Correct | Neither builds or pushes images |
That table is the point of this section: on the fundamentals, they are peers. If your worry is “will Flux reconcile as reliably as Argo?” or vice versa, stop worrying — both are CNCF-graduated precisely because they cleared the bar for security, governance, and production maturity.
A word on that graduation, because it matters for a decision you will defend to leadership:
| Governance fact | Argo CD | Flux |
|---|---|---|
| CNCF maturity | Graduated (the Argo project, late 2022) | Graduated (late 2022) |
| Original author / steward | Intuit, then a broad maintainer community | Weaveworks, then a broad maintainer community |
| Sibling projects under the umbrella | Argo Workflows, Rollouts, Events | Flagger, the GitOps Toolkit controllers |
| Corporate-shutdown risk | N/A — vendor-neutral, multi-company maintainers | Weaveworks shut down in early 2024; the project did not — it continued under CNCF with community + multiple companies maintaining it |
| Release cadence | Regular minor + patch releases | Regular minor + patch releases |
The Weaveworks shutdown is worth naming directly, because it gets used unfairly as a knock against Flux. The company that created Flux closed; the project is a CNCF-graduated, community-governed effort that kept shipping releases straight through and after the shutdown, with maintainers from several organisations. Graduated CNCF projects are explicitly structured to outlive any single vendor — that is what graduation means. Treat “is it maintained?” as answered for both.
Where they differ even in the shared loop is the reconcile trigger and cadence, and it is a real operational distinction:
| Reconcile behaviour | Argo CD | Flux |
|---|---|---|
| Default poll interval | ~3 min app resync (cluster-wide setting) | Per-resource spec.interval (e.g. 1m, 10m) |
| Where interval is set | Global (timeout.reconciliation) |
On each GitRepository/Kustomization/HelmRelease |
| Instant sync on push | Webhook to the API server | Webhook via notification-controller Receiver |
| Manual kick | argocd app sync |
flux reconcile kustomization <n> --with-source |
| Suspend reconciliation | argocd app set --sync-policy none / UI |
flux suspend kustomization <n> |
Flux’s per-resource interval is more granular — a fast-moving app can poll every minute while a stable one polls hourly — whereas Argo CD’s resync is one global knob (with webhooks for immediacy). Neither is better; it is toolkit-granularity versus platform-simplicity, the theme of the whole comparison.
Architecture, side by side
Now the divergence. Both tools turn Git into cluster state, but the shape of the machine that does it is different, and that shape is what you operate, secure, and debug for years.
Argo CD is an integrated platform. It is several components — but you install, run, upgrade, and reason about them as one product, sharing one UI, one API, and one RBAC model:
| Argo CD component | Role | Notes |
|---|---|---|
| application-controller | The reconcile engine: diff desired vs live, assess health, apply | The heart; scales via sharding |
| repo-server | Clones Git and renders manifests (kustomize build, helm template, plugins) |
Stateless; cache in Redis |
| api-server | Serves the web UI + gRPC/REST API; enforces auth and RBAC | The front door |
| redis | Ephemeral cache of rendered manifests and live state | Lose it → rebuilt from Git + cluster |
| applicationset-controller | Generates Applications from generators (Git, cluster, matrix…) |
Optional but standard |
| notifications-controller | Triggers + templates to Slack/Teams/webhook | Built in |
| dex-server | Bundled OIDC broker for SSO | Optional; skip for direct OIDC |
If you want the deep tour of these, the dedicated architecture lesson on the repo-server and controller covers each in depth. The key framing for this lesson: they ship together and present as a single control plane a team logs into.
Flux is a toolkit — the GitOps Toolkit. It is a set of independent, independently-versioned Kubernetes controllers. There is no server, no UI, no central process. Each controller watches its own CRDs and does one job:
| Flux controller | Role | CRDs it owns |
|---|---|---|
| source-controller | Fetch + verify artifacts from Git/Helm/OCI/Bucket, expose them internally | GitRepository, OCIRepository, HelmRepository, HelmChart, Bucket |
| kustomize-controller | Build + apply Kustomize/plain-YAML, prune, health-check | Kustomization |
| helm-controller | Perform real Helm releases from a source | HelmRelease |
| notification-controller | Emit events out (alerts) and ingest events in (receivers) | Provider, Alert, Receiver |
| image-reflector-controller | Scan registries, evaluate image policies | ImageRepository, ImagePolicy |
| image-automation-controller | Write updated image tags back to Git | ImageUpdateAutomation |
You run only the controllers you need — a minimal Flux is just source + kustomize controllers; add helm-controller for charts, add the image controllers for automation. That à-la-carte quality is the Unix philosophy applied to GitOps.
Line the two up and the mapping is clean — most concepts have a counterpart, they are just packaged differently:
| Concept | Argo CD | Flux |
|---|---|---|
| Fetch + render Git | repo-server | source-controller |
| Diff + apply (Kustomize/YAML) | application-controller | kustomize-controller |
| Helm handling | application-controller (renders template) | helm-controller (real release) |
| The unit you declare | Application (one CRD) |
GitRepository + Kustomization (or HelmRelease) |
| Fan-out / templating | ApplicationSet + generators |
Kustomization composition, flux per-tenant, external tooling |
| Multi-tenancy boundary | AppProject (Argo-internal) |
Namespaces + serviceAccountName impersonation (K8s-native) |
| Access control | argocd-rbac-cm (app-layer) |
Kubernetes RBAC (cluster-native) |
| SSO | Dex / direct OIDC → Argo UI/API | None built in → you use kubectl’s authn |
| UI | First-class, built in | None built in (third-party) |
| Notifications | notifications-controller | notification-controller |
| Image automation | Argo CD Image Updater (separate project) | image-reflector + image-automation controllers |
| Progressive delivery | Argo Rollouts (sibling project) | Flagger (sibling project) |
| Metrics | Prometheus endpoints per component | Prometheus endpoints per controller |
Notice there is a counterpart for nearly everything; the difference is cohesion. Argo pulls these into one product with one login; Flux keeps them as separate controllers wired together by CRDs and Git.
Here is the same idea as one picture. Read it left to right: both tools pull the same Git on the left and converge on the same cluster on the right — the difference is entirely the middle. The top row is Argo CD (repo-server → application-controller → the Application CRD plus a built-in UI); the bottom row is Flux (source-controller → kustomize/helm-controller → Kustomization/HelmRelease, driven by the CLI and Git). The numbered badges mark the real differences, not verdicts.
The philosophical contrast, stated plainly and fairly:
| Dimension | Argo CD (platform) | Flux (toolkit) |
|---|---|---|
| Design philosophy | Application-centric app-delivery platform | Unix-style composable controllers |
| What you operate | One product (several components, one login) | N independent controllers, no server |
| Mental model | “Log into Argo CD to see my apps” | “Commit to Git; the controllers reconcile” |
| Extensibility | Config Management Plugins, generators | Swap/add controllers, postBuild, Kustomize |
| Footprint | Larger (UI, API, Redis, controllers) | Smaller (controllers only) |
| Learning curve | Gentler — the UI teaches you | Steeper — you must understand the CRDs |
| Cultural fit | App teams, platform-as-product | Infra/platform engineers, K8s purists |
Neither column is the “right” one. If your organisation thinks of deployment as a product that developers use, Argo’s platform framing fits. If it thinks of deployment as infrastructure that should be as small and native as possible, Flux’s toolkit framing fits.
The CRDs, side by side
Concepts are easier to trust as YAML. Here is the same application — the podinfo demo app deployed from a Kustomize directory in Git — expressed in each tool.
Argo CD folds everything into one Application:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: podinfo
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/acme/podinfo-config.git
targetRevision: main
path: kustomize
destination:
server: https://kubernetes.default.svc
namespace: podinfo
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Flux splits the source from the application of that source into two objects — a GitRepository (what to fetch) and a Kustomization (what to build and apply from it):
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: podinfo
namespace: flux-system
spec:
interval: 1m # how often to re-fetch the repo
url: https://github.com/acme/podinfo-config.git
ref:
branch: main
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: podinfo
namespace: flux-system
spec:
interval: 10m # how often to re-apply/diff
targetNamespace: podinfo
prune: true # delete-from-Git = delete-from-cluster
wait: true # block until applied resources are healthy
sourceRef:
kind: GitRepository
name: podinfo
path: ./kustomize
That two-object split is the single most visible day-one difference. In Argo, one Application carries both “where’s the source” and “what to do with it”. In Flux, the GitRepository is fetched once and can be referenced by many Kustomizations — a genuinely nice property when several apps live in one repo, because the clone happens once and each Kustomization points at a different path.
Mapping the fields you will actually touch:
| Intent | Argo CD (Application) |
Flux |
|---|---|---|
| Where the manifests live | spec.source.repoURL + path |
GitRepository.spec.url + Kustomization.spec.path |
| Which revision | spec.source.targetRevision |
GitRepository.spec.ref.{branch,tag,semver,commit} |
| Target cluster | spec.destination.server |
Kustomization spec.kubeConfig (default: local) |
| Target namespace | spec.destination.namespace |
Kustomization.spec.targetNamespace |
| Create the namespace | syncOptions: [CreateNamespace=true] |
Include a Namespace in the path (no auto-create) |
| Auto-apply | syncPolicy.automated |
On by default (reconcile loop) |
| Self-heal drift | syncPolicy.automated.selfHeal |
On by default |
| Prune | syncPolicy.automated.prune |
spec.prune: true |
| Wait for health | (health-gated by default) | spec.wait: true |
| How often | Global resync + webhook | spec.interval per object |
Two honest gotchas fall straight out of this table. First, Argo auto-creates the destination namespace with CreateNamespace=true; Flux does not create targetNamespace — you must ship a Namespace resource in the path or create it separately, and forgetting this is a classic first-Flux stumble. Second, Flux reconciliation is on by default — there is no automated: block to opt into; a Kustomization reconciles and self-heals unless you suspend it. Argo defaults to manual sync until you add syncPolicy.automated. That is a real difference in default posture: Flux is “GitOps on unless you stop it”; Argo is “you choose automated or manual per app”.
The API groups are worth committing to memory, since half of debugging is knowing which CRD (and which controller) owns a problem:
| Flux CRD | API group | Owned by |
|---|---|---|
GitRepository, HelmRepository, OCIRepository, Bucket |
source.toolkit.fluxcd.io/v1 |
source-controller |
Kustomization |
kustomize.toolkit.fluxcd.io/v1 |
kustomize-controller |
HelmRelease |
helm.toolkit.fluxcd.io/v2 |
helm-controller |
Provider, Alert, Receiver |
notification.toolkit.fluxcd.io/v1beta* |
notification-controller |
ImageRepository, ImagePolicy, ImageUpdateAutomation |
image.toolkit.fluxcd.io/v1beta2 |
image controllers |
Application, ApplicationSet, AppProject |
argoproj.io/v1alpha1 |
Argo CD |
(API versions are current as of Flux 2.x / Argo CD 3.x; some Flux groups such as helm and image advance their versions over time — check flux version and the CRD apiVersion on your cluster rather than trusting a tutorial.)
The interface: UI, SSO, and who logs in
This is the difference most teams feel first, and it is the clearest example of the platform-vs-toolkit split.
Argo CD has a first-class web UI, and it is genuinely excellent: a live resource tree per application, visual diffs of desired vs live, sync and health status at a glance, one-click sync and rollback, and a full audit of events. For app developers and less-experienced teams, this is Argo CD’s single biggest draw — you can see GitOps happening, which flattens the learning curve enormously.
Flux ships no UI, by design. You drive it with the flux CLI, kubectl, and Git; you observe it through metrics, flux get ..., and kubectl conditions. This is not an oversight — it is the toolkit philosophy, keeping the surface small. If you want a dashboard, you add a third-party one:
| Interface aspect | Argo CD | Flux |
|---|---|---|
| Built-in UI | Yes — rich, first-class | None |
| Third-party UIs | (n/a — has its own) | Weave GitOps (OSS), Capacitor, Headlamp Flux plugin |
| Primary driver | UI + argocd CLI + Git |
flux CLI + kubectl + Git |
| See live diff | In the UI | flux diff kustomization <n> --path ./... |
| See resource tree | In the UI | flux tree kustomization <n> |
| Trigger sync | Button, or argocd app sync |
flux reconcile kustomization <n> |
| Who it suits | Devs, mixed-skill teams | Platform engineers, CLI-first shops |
Be fair to both here. Argo’s UI is a real productivity and onboarding win and an extra component to run, secure, and keep patched. Flux’s lack of a UI is a smaller footprint and a genuine gap if your developers expect a dashboard — you either add Capacitor/Weave GitOps or you teach everyone the CLI. flux tree and flux diff are surprisingly good, but they are a terminal, not a shared screen the whole team watches during a release.
The UI decision drags authentication along with it. Because Argo CD serves a UI and API, it needs its own login, so it bundles SSO:
| AuthN / SSO | Argo CD | Flux |
|---|---|---|
| Login surface | Argo UI + API | None (no server to log into) |
| SSO mechanism | Dex (bundled OIDC broker) or direct OIDC | Delegates to Kubernetes authn |
| Identity providers | Entra ID, Google, Okta, Cognito, GitHub… via OIDC/SAML→Dex | Whatever your cluster/kubeconfig uses |
| “Who can deploy?” | Argo RBAC (argocd-rbac-cm) |
Kubernetes RBAC on the CRDs |
Flux has no concept of “Flux SSO” because there is nothing to log into — access to Flux is access to its CRDs, governed by ordinary Kubernetes RBAC and whatever OIDC your cluster’s API server already uses. That is elegantly minimal (one fewer identity system) but it means there is no app-layer, per-app UI login to hand a developer. Argo’s SSO is a real feature and a real thing to configure and secure.
Cloud edge, briefly: Argo CD’s SSO plugs into Entra ID (AKS), Cognito / IAM Identity Center (EKS), and Google (GKE) through OIDC — covered in the SSO lesson. Flux inherits whatever OIDC your managed cluster’s API server is configured with, so its “SSO” story is just your cluster’s story. This is one of only a few genuinely cloud-specific edges in the whole comparison; the tools themselves are cloud-agnostic.
Multi-tenancy and RBAC
If you are running one team’s apps, tenancy barely matters. If you are a platform team hosting many teams, it is one of the two or three decisions that dominate everything — and here the two tools take genuinely different, genuinely defensible approaches.
Argo CD’s boundary is the AppProject, enforced by Argo’s own RBAC. A project restricts which repos, destination clusters/namespaces, and resource kinds its Applications may touch, and you map SSO groups to project-scoped roles:
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: team-payments
namespace: argocd
spec:
sourceRepos:
- https://github.com/acme/payments-config.git
destinations:
- server: https://kubernetes.default.svc
namespace: 'payments-*'
clusterResourceWhitelist: [] # deny all cluster-scoped resources
Flux’s boundary is Kubernetes itself. A Kustomization (or HelmRelease) can name a serviceAccountName, and Flux impersonates that ServiceAccount when it applies — so the tenant’s permissions are exactly what Kubernetes RBAC grants that SA, enforced by the API server, not by Flux:
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: team-payments
namespace: flux-system
spec:
interval: 10m
serviceAccountName: team-payments # Flux applies AS this SA → native K8s RBAC
sourceRef:
kind: GitRepository
name: team-payments
path: ./apps
prune: true
The distinction is real and worth understanding:
| Tenancy aspect | Argo CD | Flux |
|---|---|---|
| Primary boundary | AppProject |
Namespace + serviceAccountName impersonation |
| Enforced by | Argo CD’s own RBAC engine | Kubernetes RBAC (the API server) |
| Restrict repos | spec.sourceRepos |
RBAC on GitRepository + repo structure |
| Restrict namespaces | spec.destinations glob |
The SA’s RBAC + namespace scoping |
| Block cluster-scoped resources | clusterResourceWhitelist: [] |
The SA simply lacks cluster-role perms |
| Map SSO groups → roles | argocd-rbac-cm policies |
Kubernetes RoleBindings to groups |
| “Escape the tenant” risk | Argo bug/misconfig in its RBAC | Would require a Kubernetes RBAC hole |
Here is the fair read. Flux’s model is arguably more robust in principle: tenancy is enforced by Kubernetes’ own battle-tested RBAC via impersonation, so a Flux bug is less likely to let a tenant escape — the API server is the guard, and the tenant genuinely cannot do what its SA cannot do. Argo CD’s model is arguably more convenient and more legible: AppProject gives you one object that says, in one place, “this team may deploy these repos to these namespaces”, plus a UI to see it, plus SSO-group mapping — but it is Argo enforcing that, so Argo’s RBAC correctness matters. If you deeply trust Kubernetes RBAC and want the smallest trusted computing base, Flux’s impersonation appeals. If you want a self-documenting, UI-visible tenancy boundary with SSO baked in, AppProject appeals. For the full Argo treatment see the AppProjects and multi-tenancy lesson’s companion material.
Templating and fan-out
The other platform-team pain is boilerplate: you do not want to hand-write one manifest per (app × environment × cluster). Both tools attack this, differently.
Argo CD’s answer is ApplicationSet — a controller that takes generators (Git directories, cluster list, matrix, pull-request, list, SCM) and stamps out one Application per generated element. One ApplicationSet with a matrix generator can fan every app across every matching cluster. This is a first-class, purpose-built fan-out engine, and it is a genuine Argo strength — the deep dive lives in the ApplicationSets and generators lesson.
Flux’s answer is composition, not a dedicated fan-out CRD. You fan out by structuring Git and composing Kustomizations: a base plus per-environment overlays, Kustomization objects that dependsOn each other, postBuild.substituteFrom for variable substitution from ConfigMaps/Secrets, and per-cluster directories that each cluster’s Flux reconciles. For multi-cluster, the common Flux pattern is each cluster runs its own Flux and reconciles its own path in a fleet repo.
| Fan-out need | Argo CD | Flux |
|---|---|---|
| App per Git directory | ApplicationSet Git generator | One Kustomization per path; repo structure |
| App per cluster | ApplicationSet cluster generator | Per-cluster Flux + per-cluster path |
| App × cluster matrix | ApplicationSet matrix generator | Compose overlays + per-cluster dirs |
| Ephemeral PR previews | ApplicationSet PR generator | External tooling / notification Receiver + automation |
| Variable substitution | Helm/Kustomize params in the template | postBuild.substituteFrom (ConfigMap/Secret) |
| Dedicated fan-out CRD | Yes (ApplicationSet) |
No — composition + Git layout |
The honest comparison: for large fan-out from a single control plane, Argo’s ApplicationSet is more turnkey — a matrix generator plus cluster labels genuinely is “add a cluster Secret, apps appear”, visible in one UI. Flux’s model of one-Flux-per-cluster is more decentralised: there is no central object generating a thousand apps, so there is no central blast radius, but you manage N Flux installs and a fleet repo layout instead. Which is better depends on whether you want a hub. A single Argo hub is a single pane and a single point of reconciliation; a fleet of self-reconciling Flux clusters has no hub to lose but no hub to look at either.
Multi-cluster: hub-spoke vs per-cluster
Following directly from fan-out, the multi-cluster topologies are the sharpest strategic difference:
| Multi-cluster aspect | Argo CD | Flux |
|---|---|---|
| Default topology | Hub-spoke — one Argo registers many clusters | Per-cluster — Flux installed in each cluster |
| Where credentials live | Hub holds a Secret per spoke |
Each cluster reconciles itself (no cross-cluster creds needed) |
| Single pane of glass | Yes — one UI for the whole fleet | No — each cluster is its own (unless you add tooling) |
| Cross-cluster reconcile | Native (application-controller → spoke API) | Possible via Kustomization.spec.kubeConfig (remote apply) |
| Blast radius of control plane | Hub down → no drift correction fleet-wide | One cluster’s Flux down → only that cluster affected |
| Scaling the control plane | Shard the application-controller | Independent per cluster (scales trivially) |
| Fleet bootstrapping | ApplicationSet cluster generator | flux bootstrap per cluster (or Cluster API + Flux) |
Both topologies are legitimate and widely run. Argo’s hub-spoke gives you one place to see and drive everything — fantastic for an ops team that wants a fleet dashboard — at the cost of a hub that concentrates power and risk (you HA it, and you keep break-glass kubectl to each spoke). Flux’s per-cluster model is inherently decentralised — no hub to compromise, each cluster self-heals independently, the control plane scales for free because there isn’t a shared one — at the cost of no single pane and more installs to manage. Flux can do remote reconciliation (a Kustomization with spec.kubeConfig applies to another cluster), so hub-style Flux is possible; it is just not the default the way it is for Argo.
For the Argo hub-spoke deep dive, the multi-cloud fleet lesson in this course walks the whole AKS+EKS+GKE hub topology end to end.
Helm: render vs a real release (the difference that bites)
If you remember one technical difference from this lesson, make it this one, because it silently changes behaviour and generates more confused tickets than anything else.
Argo CD does not run helm install. Its repo-server runs helm template to render the chart into plain manifests, and the application-controller applies and reconciles those manifests as if you had written them by hand. There is no Helm release object, helm list shows nothing, and Helm’s client-side lifecycle (hooks as Helm, helm rollback, helm test, revision history) does not exist — rollback is a Git revert, and Helm hooks are translated into Argo sync phases.
Flux’s helm-controller performs a real Helm release. It uses the Helm SDK to actually install/upgrade the chart, so a release Secret (sh.helm.release.v1.<name>.<rev>) exists, helm list shows it, Helm hooks and helm test run as Helm intends, and the controller can do Helm-native remediation and rollback.
Here is the same chart in each tool. Argo CD, Helm as a source:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: podinfo
namespace: argocd
spec:
project: default
source:
repoURL: https://stefanprodan.github.io/podinfo
chart: podinfo
targetRevision: 6.7.1
helm:
valuesObject:
replicaCount: 2
ui:
message: "hello from Argo CD"
destination:
server: https://kubernetes.default.svc
namespace: podinfo
syncPolicy:
automated: { prune: true, selfHeal: true }
syncOptions: [ CreateNamespace=true ]
Flux, a HelmRepository source plus a HelmRelease:
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: podinfo
namespace: flux-system
spec:
interval: 1h
url: https://stefanprodan.github.io/podinfo
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: podinfo
namespace: podinfo
spec:
interval: 10m
chart:
spec:
chart: podinfo
version: "6.7.1"
sourceRef:
kind: HelmRepository
name: podinfo
namespace: flux-system
install:
createNamespace: true
values:
replicaCount: 2
ui:
message: "hello from Flux"
The consequences are not cosmetic. They change what exists in the cluster and which Helm features work:
| Helm behaviour | Argo CD (helm template) |
Flux (real release) |
|---|---|---|
Release object / helm list |
None — helm list is empty |
Yes — release Secret exists, helm list shows it |
| Rollback | Git revert + resync | Git revert or Helm-native spec.rollback remediation |
| Helm hooks | Translated to Argo sync phases | Run as real Helm hooks |
helm test |
Not applicable | Supported (spec.test.enable) |
.Release.IsUpgrade |
Always false (every render looks fresh) | True on upgrades, as Helm intends |
lookup at render |
Empty (repo-server has no cluster access) | Works (real Helm dry-run against cluster) |
| Per-resource drift/diff | Yes — every rendered object is diffed in the UI | Coarser — drift tracked at the release level |
| Failure remediation | Sync fails; you see per-object errors | spec.upgrade.remediation retries/rolls back |
This is a genuine trade with costs on both sides — do not read the table as “Flux wins Helm”. Flux’s real release is better if you rely on Helm hooks, helm test, .Release.IsUpgrade, lookup, or Helm-native rollback/remediation, and if your team’s mental model is “it’s a Helm release, treat it like one”. Argo’s render is better if you want every rendered resource diffed and drift-corrected individually in a UI — because Argo treats the chart output as plain manifests, you see and self-heal each Deployment/Service/ConfigMap, which the release-level model does not surface as granularly. Many teams never hit the difference; the ones who do are usually running charts with meaningful hooks or expecting helm list/helm rollback to work. Know which camp you are in before you choose. (The Argo side of this is covered exhaustively in this course’s Helm integration lesson.)
Image automation, progressive delivery, and notifications
Three more areas where the packaging differs — and one of them is a clear Flux strength worth naming plainly.
Image automation is Flux’s standout capability. Flux ships two dedicated controllers: image-reflector-controller scans your registry and evaluates an ImagePolicy, and image-automation-controller writes the new tag back to Git as a real commit — closing the GitOps loop natively, so the newest image is always reflected in the source of truth.
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImagePolicy
metadata: { name: podinfo, namespace: flux-system }
spec:
imageRepositoryRef: { name: podinfo }
policy:
semver: { range: ">=6.0.0" }
---
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageUpdateAutomation
metadata: { name: podinfo, namespace: flux-system }
spec:
interval: 30m
sourceRef: { kind: GitRepository, name: podinfo }
git:
commit:
author: { name: fluxcdbot, email: fluxcdbot@users.noreply.github.com }
push: { branch: main }
update:
path: ./kustomize
strategy: Setters
You mark the image field in your manifest with a # {"$imagepolicy": "flux-system:podinfo"} comment, and Flux edits it in place and commits. Argo CD’s equivalent, Argo CD Image Updater, is a separate project configured via annotations on the Application; it works and is widely used, but it is less integrated and generally considered less polished than Flux’s built-in pair:
metadata:
annotations:
argocd-image-updater.argoproj.io/image-list: podinfo=ghcr.io/stefanprodan/podinfo
argocd-image-updater.argoproj.io/podinfo.update-strategy: semver
argocd-image-updater.argoproj.io/write-back-method: git
| Image automation | Argo CD | Flux |
|---|---|---|
| Mechanism | Argo CD Image Updater (separate project) | image-reflector + image-automation controllers (built in) |
| Config location | Annotations on the Application |
Dedicated ImagePolicy / ImageUpdateAutomation CRDs |
| Write-back to Git | Yes (write-back-method: git) or to Argo |
Yes — real Git commit |
| Update strategies | semver, latest, digest, name | semver, alphabetical, numerical |
| Maturity / polish | Works; less integrated | Best-in-class; a Flux strength |
Give Flux this one squarely: if automated image updates written back to Git are central to your workflow, Flux does it better and more natively.
Registries are the one place image automation touches a cloud edge — and both tools authenticate to all three the same way (workload identity or a pull secret), so this is not a differentiator between the tools, only a per-cloud fact you configure once:
| Cloud registry | Auth approach (both tools) | Service |
|---|---|---|
| Azure (ACR) | Azure Workload Identity or ACR token | Azure Container Registry |
| AWS (ECR) | IRSA / EKS Pod Identity, or ECR credential helper | Elastic Container Registry |
| GCP (Artifact Registry) | Workload Identity or SA key | Artifact Registry |
Progressive delivery is a wash — both delegate to an excellent sibling project:
| Progressive delivery | Argo CD | Flux |
|---|---|---|
| Tool | Argo Rollouts (Rollout CRD) |
Flagger (Canary CRD) |
| Strategies | Canary, blue-green, experiments | Canary, blue-green, A/B |
| Traffic shaping | Istio, NGINX, ALB, Gateway API, SMI | Istio, Linkerd, App Mesh, NGINX, Gateway API |
| Metric-driven analysis | AnalysisTemplate (Prometheus, etc.) |
Metric checks (Prometheus, etc.) |
| Coupling | Independent of Argo CD (works standalone) | Independent of Flux (works standalone) |
Both are outstanding and, notably, decoupled — you can run Argo Rollouts with Flux, or Flagger with Argo CD. Progressive delivery should almost never decide your GitOps engine.
Notifications slightly favour Flux’s design. Argo CD’s notifications-controller emits triggers+templates outward to Slack/Teams/webhooks. Flux’s notification-controller does that and ingests inbound events via Receiver (a webhook that triggers reconciliation) — it is bidirectional:
| Notifications | Argo CD | Flux |
|---|---|---|
| Outbound alerts | notifications-controller | notification-controller (Provider/Alert) |
| Inbound webhooks | Webhook to API server (sync on push) | Receiver (first-class, many event sources) |
| Config style | ConfigMap triggers/templates | Dedicated CRDs |
Security posture and operations
The last comparison axis is the one security teams care about: attack surface, and how each behaves in production.
| Security / ops aspect | Argo CD | Flux |
|---|---|---|
| Network-exposed server | api-server + UI (must be secured) | None — controllers only |
| Attack surface | Larger (UI, API, its own authn/RBAC, admin user) | Smaller (no UI/API daemon) |
| Historical CVEs | Some in the API/UI auth path (patched) | Fewer surface-area vectors |
| Secrets handling | Sealed Secrets / ESO / SOPS / Vault plugin | Native SOPS decryption in kustomize-controller |
| RBAC model | App-layer (argocd-rbac-cm) + K8s |
Pure Kubernetes RBAC |
| Multi-tenancy enforcement | Argo’s RBAC | K8s impersonation |
| Observability | Prometheus metrics + UI + Grafana mixins | Prometheus metrics + Grafana dashboards |
| Bootstrap | Manifests/Helm + app-of-apps; Terraform helm_release |
flux bootstrap (Git-native) + Terraform flux provider |
Two things deserve fair emphasis. First, Flux’s smaller attack surface is a real security advantage — there is no UI or API server to expose, phish a login for, or find an auth-bypass in; access is entirely Kubernetes RBAC. Argo CD’s larger surface is the price of the UI, SSO, and multi-tenancy that many teams want — a fair trade, but a trade. Second, Flux decrypts SOPS secrets natively in the kustomize-controller, which is a clean, popular pattern; Argo CD reaches the same outcome with Sealed Secrets, External Secrets Operator, SOPS via a plugin, or a cloud secret store — more options, less built-in. Neither is insecure; they distribute the responsibility differently.
On bootstrap and Terraform, both fit an IaC hand-off. Flux’s flux bootstrap github/gitlab/git is delightfully Git-native — it commits Flux’s own manifests plus a sync Kustomization into your repo, so Flux manages itself from Git thereafter:
# Flux: Git-native bootstrap (commits Flux components into the repo)
flux bootstrap github \
--owner=acme --repository=fleet \
--branch=main --path=./clusters/dev --personal
# Argo CD: apply the install manifests, then an app-of-apps root
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
There are first-class Terraform providers for both (flux provider for Flux; helm_release + kubernetes_manifest or a community provider for Argo CD), so either drops cleanly into a Terraform-provisioned cluster.
Going deeper
The comparison above is enough to choose. This section is for the engineer who will operate the winner for years — the internals, the tenancy locks, the diff edges, and the scaling knobs that the marketing pages skip. Nothing here changes the verdict; it sharpens it.
Under the hood: how each fetches, renders, and hands off manifests
Both “pull Git and apply it”, but the plumbing differs in a way you feel when you debug or scale it.
| Internal detail | Argo CD | Flux |
|---|---|---|
| Fetch + render store | repo-server clones to local disk, runs kustomize build / helm template, caches the rendered output in Redis |
source-controller fetches, verifies (checksum, optional Cosign/GPG), packages a gzipped tarball artifact, and serves it over an in-cluster HTTP endpoint |
| Shared datastore | Redis (ephemeral cache; lose it and it rebuilds from Git + cluster) | None — the tarball artifact is the handoff between controllers |
| What diffs live vs desired | application-controller (cluster informers vs cached render) | kustomize-controller / helm-controller (pull artifact, apply, compare) |
| Ownership marker on each object | tracking label app.kubernetes.io/instance or the annotation argocd.argoproj.io/tracking-id |
an inventory (status.inventory on the Kustomization) plus a server-side-apply field manager (kustomize-controller) |
Two consequences worth internalising. First, Argo’s Redis is a cache, never a source of truth — a common newcomer worry (“what if Redis dies?”) is a non-event; it is rebuilt from Git and the live cluster. Second, Flux has no shared datastore at all: source-controller produces an artifact and the other controllers consume it, so each controller is independently stateless around that artifact. That is why you can run, scale, and restart Flux controllers in isolation.
The ownership marker is what makes “one owner per object” real
The lesson’s golden rule — never let both tools reconcile the same object — is not a style preference; it is enforced (or violated) by that ownership marker in the table above. Argo stamps every managed object with its tracking id; Flux records the object in its inventory and claims fields via SSA. When two controllers stamp the same object, each sees the other’s writes as drift and self-heals them straight back, so the object flaps forever. Prefer Argo’s annotation tracking method (application.resourceTrackingMethod: annotation in argocd-cm) over the legacy label: the label is capped at 63 characters and collides across apps with long names, whereas the annotation does not — this is the recommended posture on Argo CD 2.x/3.x. Knowing how ownership is recorded is exactly what lets you run both tools safely (see the last subsection).
Tenancy you must actually lock down (the defaults are not multi-tenant)
Out of the box, neither tool is hardened against a hostile tenant — both assume you trust whoever can commit. For a genuine multi-team platform you set flags:
- Flux — turn on
--no-cross-namespace-refs=trueon the kustomize-, helm-, notification-, and image-controllers so a tenant’sKustomizationintenant-bcannotsourceRefaGitRepositoryliving intenant-a’s namespace. Add--default-service-account=<sa>so noKustomization/HelmReleasecan silently apply as the controller’s own (privileged) identity, and--no-remote-bases=trueto block akustomization.yamlfrom pulling arbitrary remote bases. Give each tenant its own namespace, its ownGitRepository, and its own least-privilege ServiceAccount. - Argo CD — by default
Applications may only live in theargocdnamespace. Enable apps-in-any-namespace (application.namespacesinargocd-cmd-params-cmplusAppProject.spec.sourceNamespaces) so tenants own theirApplicationCRs in their own namespaces, while theAppProjectstill constrains which repos, destinations, and resource kinds they may touch. The full treatment is in the AppProjects and multi-tenancy lesson.
The theme holds: Flux hardens tenancy with controller flags + native RBAC, Argo with project objects + its own RBAC — both real, both requiring deliberate configuration.
Server-side apply and the diff edges
Both tools have converged on server-side apply (SSA), and both meet the same class of bug at the edges. Flux’s kustomize-controller uses SSA by default with field managers, and spec.force: true recreates a resource when an immutable field changes. Argo CD offers SSA via syncOptions: [ServerSideApply=true] (and a server-side diff), and tames fields that other controllers mutate — an HPA rewriting replicas, a mutating webhook injecting a sidecar — with ignoreDifferences plus RespectIgnoreDifferences. The failure mode is identical on both sides: a field owned by another manager causes a perpetual OutOfSync/flap, and the fix is the same — ignore the contested field or let its true owner win. Argo’s side of this is covered in depth in diffing, drift, and ignoreDifferences; the SOPS-vs-plugin secret angle is in secrets: Sealed Secrets, ESO, SOPS, Vault.
Scale, and the knobs that move it
Their scaling stories mirror the platform-vs-toolkit split exactly. Argo CD scales one shared control plane: the application-controller shards by cluster across StatefulSet replicas (ARGOCD_CONTROLLER_REPLICAS, with --sharding-method of legacy, round-robin, or consistent-hashing), while the stateless repo-server scales as replicas and is the usual monorepo bottleneck because rendering is CPU-bound — the deep dive is scaling and sharding the repo-server. Flux scales each part independently: every controller is its own Deployment you resource and replicate on its own, tuned with --concurrent, --kube-api-qps/--kube-api-burst, and — for a genuinely hot controller — horizontal sharding via --watch-label-selector with a sharding.fluxcd.io/key label. And the per-cluster Flux model scales for free in one sense: there is no shared control plane to grow, because each cluster reconciles only itself.
Running both without a fight
Because the two are good at different things, a common mature pattern is Flux for the platform layer (bootstrap, CRDs, ingress-nginx, cert-manager — even the Argo CD install itself) and Argo CD as the developer-facing app plane. That works, and the single invariant that makes it safe is the one from the ownership discussion: no Kubernetes object is reconciled by both. Enforce it two ways — (1) keep their scopes disjoint (Flux owns platform namespaces/paths, Argo owns app namespaces/paths, never overlapping), and (2) if you ever suspect overlap, check the object’s markers: it should carry either Flux’s inventory/field-manager or Argo’s tracking-id annotation, never both. See both markers on one object and you have found your flap — split ownership immediately.
When to choose which
Pulling it together into the decision you actually have to make. This is the honest matrix — no thumb on the scale:
| Choose Argo CD when… | Choose Flux when… |
|---|---|
| Developers need a UI to see apps, diffs, health | You want a CLI/Git-only, no-server workflow |
| You want built-in multi-tenancy + RBAC + SSO | You want tenancy enforced by native K8s RBAC |
| You run a hub-spoke fleet from one pane | You want decentralised, per-cluster reconciliation |
You need turnkey fan-out (ApplicationSet) with a dashboard |
You compose via Kustomize + Git layout |
| Your team is mixed-skill; the UI lowers the barrier | Your team is platform/infra engineers who live in the CLI |
| You want an app-delivery platform | You want a small, K8s-native GitOps toolkit |
| — | You need best-in-class image automation to Git |
| — | You need real Helm release semantics (helm list, hooks, rollback) |
| — | You want the smallest attack surface (no UI/API) |
| — | You want native SOPS decryption |
Weight the rows by your context. A 200-developer product org with rotating on-call and app teams that expect a dashboard will feel Argo’s UI, SSO, and AppProject tenancy as daily wins. A tight platform team running a fleet of clusters, automating image bumps to Git, and valuing a minimal attack surface will feel Flux’s toolkit, image automation, and impersonation model as daily wins. Both teams would be correct.
And the ending the whole lesson has been building toward: many mature organisations run both. Flux to bootstrap clusters and reconcile the platform layer (its Git-native bootstrap and small footprint shine there), Argo CD as the developer-facing app-delivery plane (its UI and AppProject model shine there). They are not mutually exclusive — they are two good tools that are good at different things, and using each where it is strongest is a perfectly respectable architecture, not a failure to decide.
Migration considerations
Because tooling is sticky, teams often ask about switching — in both directions. The good news: since both reconcile the same Git and the same manifests, your Kustomize bases, Helm charts, and values files move over unchanged. What changes is the wrapper CRD.
| Migration concern | Argo CD → Flux | Flux → Argo CD |
|---|---|---|
| App-of-apps / ApplicationSet → ? | Rebuild as Kustomization composition + per-cluster dirs |
Recreate Kustomization/HelmRelease as Application/ApplicationSet |
| Helm apps | helm template render → real release (behaviour changes!) |
Real release → helm template render (behaviour changes!) |
| Namespaces | Add explicit Namespace (Flux won’t auto-create) |
Add CreateNamespace=true |
| Tenancy | AppProject → SA impersonation + K8s RBAC |
SA impersonation → AppProject + argocd-rbac-cm |
| Image automation | Image Updater annotations → ImagePolicy/ImageUpdateAutomation |
Flux image CRDs → Image Updater annotations |
| Cutover risk | Run side-by-side in separate namespaces first | Same — never point both at the same objects at once |
| Manifests themselves | Unchanged | Unchanged |
The one migration trap that bites hardest is the Helm model flip: moving a Helm app from Argo (render) to Flux (real release) means a Helm release object suddenly exists and hooks now run as Helm hooks; going the other way, helm list goes empty and your helm rollback muscle memory stops working. Plan that transition per-app, not fleet-wide. The universal safety rule for either direction: never let both tools reconcile the same objects simultaneously — they will fight, each reverting the other. Migrate app-by-app in separate namespaces, verify, then retire the old wrapper. If you are coming to GitOps from a script-driven pipeline rather than the other tool, the migrating from Jenkins/Helm-CLI to GitOps lesson covers that on-ramp.
Hands-on lab
Let us make the biggest difference — Helm render vs real release — concrete, by deploying the same app with each tool on a free local cluster. Nothing here bills: it is all kind. We deploy with Argo CD, observe, tear down, then deploy with Flux and observe the difference. We deliberately do not run both against the same objects at once (they would war). Outputs are representative — labelled shapes, not a live run on this machine.
Step 0 — A throwaway cluster.
kind create cluster --name gitops-compare
kubectl cluster-info --context kind-gitops-compare
# Kubernetes control plane is running at https://127.0.0.1:xxxxx
What just happened: one local cluster we will point each tool at in turn, then delete.
Step 1 — Install Argo CD and deploy podinfo as a Helm app.
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
kubectl -n argocd rollout status deploy/argocd-server # wait for it
# apply the Helm-source Application from the lesson above (saved as argo-podinfo.yaml)
kubectl apply -f argo-podinfo.yaml
argocd app get podinfo # (after argocd login)
Representative argocd app get output:
Name: argocd/podinfo
Project: default
Sync Status: Synced to 6.7.1
Health Status: Healthy
What just happened: Argo CD’s repo-server ran helm template on the podinfo chart and the application-controller applied the rendered manifests. podinfo is running.
Step 2 — The tell: ask Helm what it sees.
helm list -n podinfo
# NAME NAMESPACE REVISION STATUS CHART APP VERSION
# (no rows)
kubectl get secret -n podinfo | grep sh.helm.release || echo "no helm release secret"
# no helm release secret
What just happened: nothing. Argo CD never created a Helm release — it applied plain manifests. helm list is empty and that is correct, not broken. Your “release history” is the Git log. This is the render model, made visible.
Step 3 — Tear down the Argo side cleanly.
kubectl delete -f argo-podinfo.yaml # removes the Application (and prunes podinfo)
kubectl delete namespace argocd
What just happened: we retire Argo entirely so the two tools never touch the same objects.
Step 4 — Install Flux and deploy the same chart as a HelmRelease.
flux install # installs the toolkit controllers into flux-system
flux check # verify controllers are ready
# apply the HelmRepository + HelmRelease from above (saved as flux-podinfo.yaml)
kubectl apply -f flux-podinfo.yaml
flux get helmreleases -n podinfo
Representative flux check and flux get output:
✔ source-controller: deployment ready
✔ kustomize-controller: deployment ready
✔ helm-controller: deployment ready
✔ all checks passed
NAME REVISION SUSPENDED READY MESSAGE
podinfo 6.7.1 False True Helm install succeeded for release podinfo/podinfo.v1
What just happened: Flux’s helm-controller performed a real Helm install of the same chart, and reports it as a release.
Step 5 — Ask Helm again — this time it answers.
helm list -n podinfo
# NAME NAMESPACE REVISION STATUS CHART APP VERSION
# podinfo podinfo 1 deployed podinfo-6.7.1 6.7.1
kubectl get secret -n podinfo | grep sh.helm.release
# sh.helm.release.v1.podinfo.v1 helm.sh/release.v1 1
What just happened: the same chart, deployed by Flux, produced a genuine Helm release object — helm list shows it, the sh.helm.release.v1.podinfo.v1 Secret exists, and helm rollback would work. This is the render-vs-release difference from theory to your terminal, side by side. Nothing about the chart changed; only the tool’s model did.
Step 6 — Feel the developer experience. Try to answer “is my app synced and healthy, and what changed?” in each tool. With Argo you would open the UI and see the resource tree and a visual diff; with Flux you stay in the terminal:
flux get kustomizations # or helmreleases: status at a glance
flux tree helmrelease podinfo -n podinfo # the managed resource tree, in the CLI
flux diff kustomization <name> --path ./... # (for a Kustomization) live vs desired
What just happened: you experienced the interface split first-hand — Argo’s browser dashboard vs Flux’s CLI. Neither is wrong; they suit different people.
Developer-experience contrast, from this lab:
| Task | Argo CD | Flux |
|---|---|---|
| See app health | UI dashboard (or argocd app get) |
flux get helmreleases |
| See a diff | Visual diff in UI | flux diff kustomization … --path |
| See the resource tree | UI tree view | flux tree helmrelease … |
| Trigger a sync | Button / argocd app sync |
flux reconcile helmrelease … |
Does helm list show it? |
No (render) | Yes (real release) |
Step 7 — Teardown.
kind delete cluster --name gitops-compare
What just happened: the whole experiment — both tools, both apps, the cluster — is gone. No cloud resources were ever created, so nothing bills.
Practice challenges
Six exercises, easiest first. Try each before you open the solution — the point is to predict behaviour, which is exactly what choosing between these tools demands. All manifests use placeholders (example.com) and no live cluster is required.
1 (beginner) — The empty helm list. You deploy podinfo as a Helm-source Argo CD Application, and it reports Synced / Healthy. A teammate runs helm list -n podinfo, sees zero rows, and files a bug. Is anything broken? Where is the “release history”?
<details><summary>Solution</summary>
Nothing is broken. Argo CD ran helm template and applied the rendered plain manifests — it never called helm install, so no Helm release object exists and helm list is correctly empty. The release history is the Git log; the rollback is git revert.
Why: Argo CD uses the render model, not a real Helm release — the single most misread behaviour in the whole tool. </details>
2 (beginner) — Translate the wrapper CRD. Convert this Argo CD Application (Kustomize source at path: kustomize, repo https://github.com/example/podinfo-config.git, branch main, into namespace podinfo, auto-sync + prune + selfHeal + CreateNamespace) into the equivalent Flux objects.
<details><summary>Solution</summary>
A GitRepository (the source) plus a Kustomization (what to build) — and you must add a Namespace manifest to ./kustomize, because Flux will not auto-create targetNamespace.
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata: { name: podinfo, namespace: flux-system }
spec:
interval: 1m
url: https://github.com/example/podinfo-config.git
ref: { branch: main }
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata: { name: podinfo, namespace: flux-system }
spec:
interval: 10m
targetNamespace: podinfo # sets the field; does NOT create the namespace
prune: true # = Argo prune
sourceRef: { kind: GitRepository, name: podinfo }
path: ./kustomize # include a Namespace resource here
Why: Flux splits source from application of source, reconciles + self-heals on by default (no automated: block), and never creates the target namespace for you.
</details>
3 (intermediate) — “namespace not found”. A Flux Kustomization is Ready=False with namespace "podinfo" not found, yet the path builds fine with kustomize build locally. Give two fixes.
<details><summary>Solution</summary>
Either (a) ship a Namespace object in the reconciled path so Flux creates it:
apiVersion: v1
kind: Namespace
metadata: { name: podinfo }
or (b) pre-create it once out-of-band: kubectl create namespace podinfo.
Why: targetNamespace sets the .metadata.namespace field on the applied objects but does not create the namespace itself — the classic first-Flux stumble that Argo’s CreateNamespace=true hides.
</details>
4 (intermediate) — Same tenancy rule, both tools. Team payments may deploy only into payments-* namespaces and must never create cluster-scoped resources. Express it in each tool.
<details><summary>Solution</summary>
Argo CD — an AppProject that globs the destinations and whitelists no cluster-scoped kinds:
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata: { name: team-payments, namespace: argocd }
spec:
sourceRepos: [ 'https://github.com/example/payments-config.git' ]
destinations:
- { server: https://kubernetes.default.svc, namespace: 'payments-*' }
clusterResourceWhitelist: [] # deny all cluster-scoped resources
Flux — a least-privilege ServiceAccount payments, bound (via RoleBindings) only in payments-* namespaces and holding no ClusterRole, referenced by the tenant’s Kustomization:
spec:
serviceAccountName: payments # Flux impersonates this SA → native K8s RBAC decides
Why: Argo enforces the boundary with its own project RBAC; Flux enforces it with native Kubernetes RBAC through ServiceAccount impersonation — the SA simply cannot do what its Roles do not grant. </details>
5 (advanced) — Stop a cross-tenant escape. In a shared cluster, tenant B’s Kustomization in namespace tenant-b sets sourceRef to a GitRepository in tenant-a’s namespace and begins deploying tenant A’s code. Which single controller flag would have prevented this, and what else hardens the tenant?
<details><summary>Solution</summary>
Set --no-cross-namespace-refs=true on the kustomize-controller (and the helm-, notification-, and image-controllers). Then also set --default-service-account=<sa> (so nothing applies as the controller’s own identity), --no-remote-bases=true, and give every tenant its own namespace + GitRepository + least-privilege SA.
Why: by default Flux allows cross-namespace references; the flag scopes a sourceRef (and dependsOn) to the object’s own namespace, closing the escalation path.
</details>
6 (advanced) — Run both without a war. You want Flux to reconcile the platform layer (ingress-nginx, cert-manager, even the Argo CD install) while Argo CD serves app teams. State the one invariant that keeps them from fighting, and two concrete ways you enforce it.
<details><summary>Solution</summary>
Invariant: no Kubernetes object is reconciled by both — exactly one owner per object. Enforce by (1) disjoint scopes — Flux owns platform namespaces/paths, Argo owns app namespaces/paths, never overlapping; and (2) marker checks — an object must carry either Flux’s inventory / SSA field-manager or Argo’s argocd.argoproj.io/tracking-id annotation, never both. Seeing both on one object is the bug.
Why: each tool self-heals whatever it owns, so a shared object flaps forever as each reverts the other. Ownership markers are the concrete, checkable expression of “one owner per object”. </details>
Common mistakes and troubleshooting
The table doubles as a quick decision aid and a real error map. The top rows are the decisions; the lower rows are genuine failure modes with real states/messages from each tool.
| Symptom / question | Cause / meaning | Fix / answer |
|---|---|---|
| “I need a UI for developers” | Flux ships none | Argo CD (or add Weave GitOps/Capacitor to Flux) |
| “I want best-in-class image automation to Git” | Argo’s Image Updater is a separate, less-polished project | Flux (image-reflector + image-automation controllers) |
| “I need real Helm release semantics” | Argo renders helm template; no release object |
Flux (helm-controller does a real release) |
| “I want strong built-in multi-tenancy + RBAC + SSO” | Flux delegates to K8s RBAC, has no UI login | Argo CD (AppProject + argocd-rbac-cm + Dex) |
| “I want a composable, K8s-native toolkit” | Argo is an integrated platform | Flux (GitOps Toolkit controllers) |
| “Fan out to many clusters with one pane” | Flux default is per-cluster | Argo CD hub-spoke + ApplicationSet |
| “CLI/Git-only, no server to run” | Argo needs api-server + UI | Flux (flux CLI + Git) |
| “I need progressive delivery” | Both delegate to a sibling project | Either — Argo Rollouts or Flagger |
Argo app Synced/Healthy but helm list empty |
Argo used helm template; there is no release |
Working as designed — history is the Git log |
Flux Kustomization fails: namespace not found |
Flux does not auto-create targetNamespace |
Ship a Namespace in the path, or create it |
Flux HelmRelease Ready=False: install retries exhausted |
Chart failed to install; remediation gave up | flux logs --kind HelmRelease; fix values; flux reconcile |
Argo ComparisonError / Unknown on a Helm app |
Chart/values render failure in repo-server | Fix the chart/values; the blame is render, not cluster |
| Both tools flip an object back and forth | You pointed both at the same resource | Never co-manage; one owner per object |
Flux app stuck Reconciling, no change |
Source not ready or dependsOn unmet |
flux get sources git; check the referenced source |
Argo OutOfSync forever on a Helm chart |
A controller mutates a rendered field | ignoreDifferences, or move to Flux’s release model |
Three gotchas cost the most hours, so give them extra words:
1. The empty helm list panic (Argo). New GitOps engineers file a ticket: “Argo says Healthy but helm list shows nothing!” There is no bug. Argo rendered the chart and applied manifests; it never made a Helm release, so there is nothing for Helm to list. If your team needs helm list/helm rollback to mean something, that is a real reason to lean Flux — not a reason to think Argo is broken.
2. Flux won’t create your namespace. Argo’s CreateNamespace=true spoils you. Flux’s targetNamespace sets the namespace on the rendered objects but does not create the namespace itself — so the first apply fails with a not-found error until you include a Namespace resource in the path (or pre-create it). This is the single most common first-Flux stumble.
3. Two cooks, one kitchen. During any migration or evaluation, the tempting thing is to point both tools at the same app “just to compare”. Do not. Each will see the other’s changes as drift and revert them, and you get an infinite flap. Use separate namespaces, or do one tool then the other (as the lab does). One object, one owner — always.
Common beginner mistakes
These are conceptual traps — wrong mental models rather than error messages (the symptom-driven failures live in the troubleshooting table above). Each is a belief that quietly steers a bad decision.
| Misconception | The right model |
|---|---|
| “One of them is more real GitOps than the other.” | Both are CNCF-graduated and produce an identical cluster from identical Git. They differ in interface and packaging, not in GitOps correctness. |
| “The UI makes Argo the beginner tool and Flux the advanced one.” | They are different bets, not skill tiers. Flux is simpler in its own way — fewer server components to run and secure; Argo is friendlier on day one because you can see it. Skill level is not the axis. |
| “Flux can’t show me anything — no dashboard exists.” | Flux ships no built-in UI by design, but Weave GitOps, Capacitor, and the Headlamp Flux plugin are real dashboards, and flux tree / flux diff are strong CLIs. It is unbundled, not absent. |
| “Picking Flux loses Argo Rollouts / picking Argo loses Flagger.” | Progressive delivery is decoupled — run Argo Rollouts under Flux, or Flagger under Argo CD. It should almost never decide the engine. |
| “Running both is indecision, or a smell.” | It is a legitimate layered architecture — Flux for the platform/bootstrap layer, Argo for the developer-facing app plane — provided they never co-own the same object. |
| “Switching tools means rewriting my manifests.” | Your Kustomize bases, Helm charts, and values files move unchanged; only the wrapper CRD (Application ↔ Kustomization/HelmRelease) changes. The one behaviour that truly flips is Helm render ↔ real release. |
“Flux’s targetNamespace creates the namespace like Argo’s CreateNamespace.” |
It does not — it only sets the field on the applied objects. You must ship a Namespace in the path or pre-create it. |
| “More components = Argo is less secure, full stop.” | Argo’s larger surface is the price of the UI, SSO, and multi-tenancy you may actually want. Smaller surface is a security win only if you are not giving up features you need. |
Cheat-sheet
Component and CRD equivalents:
| Concept | Argo CD | Flux |
|---|---|---|
| Render Git | repo-server | source-controller |
| Reconcile | application-controller | kustomize-controller / helm-controller |
| Declare an app | Application |
GitRepository + Kustomization |
| Declare a Helm app | Application (helm source) |
HelmRepository + HelmRelease |
| Fan-out | ApplicationSet |
Kustomize composition + Git layout |
| Tenancy | AppProject |
serviceAccountName impersonation |
| Access control | argocd-rbac-cm |
Kubernetes RBAC |
| Image automation | Argo CD Image Updater | ImagePolicy + ImageUpdateAutomation |
| Progressive delivery | Argo Rollouts | Flagger |
| Notifications | notifications-controller | notification-controller |
| UI | Built-in | Weave GitOps / Capacitor (external) |
CLI equivalents (argocd ⟷ flux):
| Task | Argo CD | Flux |
|---|---|---|
| List apps | argocd app list |
flux get kustomizations / helmreleases |
| App detail | argocd app get <n> |
flux get kustomization <n> |
| Force sync | argocd app sync <n> |
flux reconcile kustomization <n> --with-source |
| Diff | argocd app diff <n> |
flux diff kustomization <n> --path ./... |
| Resource tree | (UI) | flux tree kustomization <n> |
| Pause | argocd app set <n> --sync-policy none |
flux suspend kustomization <n> |
| Resume | argocd app set <n> --sync-policy automated |
flux resume kustomization <n> |
| Logs / health | argocd app logs / UI |
flux logs / flux check |
| Bootstrap | apply manifests + app-of-apps | flux bootstrap github … |
Choose-which matrix (the one-line version):
| You want… | Pick |
|---|---|
| A UI, SSO, built-in multi-tenancy | Argo CD |
| A CLI/Git-only, minimal-surface toolkit | Flux |
Real Helm releases (helm list, hooks, rollback) |
Flux |
| Best image automation to Git | Flux |
| Hub-spoke fleet with one pane of glass | Argo CD |
| Decentralised per-cluster reconciliation | Flux |
| Turnkey fan-out with a dashboard | Argo CD (ApplicationSet) |
| Progressive delivery | Either (Rollouts / Flagger) |
Interview and exam questions
Q: Are Argo CD and Flux both “real” GitOps, or is one more legitimate? A: Both are equally legitimate — both are CNCF-graduated and both implement pull-based reconciliation with Git as the source of truth. Deploy the same app with each and the cluster is identical. “Which is more GitOps?” is the wrong question; they differ in interface, packaging, and Helm semantics, not in GitOps correctness.
Q: Describe the core architectural difference in one sentence.
A: Argo CD is an integrated, application-centric platform you log into (repo-server + application-controller + api-server/UI + its own RBAC, driving one Application CRD); Flux is a toolkit of small, independent, composable controllers (source, kustomize, helm, notification, image) driven by the flux CLI and Git with no built-in UI.
Q: What is the single biggest technical difference in how they handle Helm, and why does it matter?
A: Argo CD runs helm template and applies plain manifests — no Helm release object, helm list is empty, rollback is a Git revert, and Helm hooks become Argo sync phases. Flux’s helm-controller performs a real Helm release — a release Secret exists, helm list shows it, and Helm hooks/tests and Helm-native rollback all work. It matters because charts that rely on hooks, helm test, .Release.IsUpgrade, or lookup behave differently, and teams expecting helm list/helm rollback to work get surprised under Argo.
Q: A developer says “Argo shows Healthy but helm list is empty — something’s broken.” Are they right?
A: No — that is correct behaviour. Argo rendered the chart with helm template and applied manifests; it never created a Helm release, so there is nothing to list. The Git log is the history and a Git revert is the rollback.
Q: How does multi-tenancy differ between the two?
A: Argo CD uses AppProject (restricting repos, destinations, resource kinds) enforced by its own RBAC, mapped to SSO groups via argocd-rbac-cm. Flux names a serviceAccountName on the Kustomization/HelmRelease and impersonates it, so tenancy is enforced by native Kubernetes RBAC at the API server. Flux’s is arguably more robust (K8s enforces it); Argo’s is arguably more legible and UI-visible with SSO built in.
Q: How do the two approach multi-cluster, and what is the trade-off?
A: Argo CD favours hub-spoke — one control plane registers many clusters and ApplicationSet fans out — giving a single pane of glass at the cost of a hub that concentrates blast radius. Flux favours per-cluster — each cluster runs its own Flux and reconciles itself — giving decentralisation and no hub to lose, at the cost of no single pane and more installs. Flux can do remote reconciliation via Kustomization.spec.kubeConfig, but per-cluster is the default.
Q: Which has the stronger image automation, and why? A: Flux. It ships dedicated image-reflector and image-automation controllers that scan the registry and write the new tag back to Git as a real commit, closing the loop natively. Argo CD’s equivalent, Argo CD Image Updater, is a separate, less-integrated project. If image automation is central, that is a point for Flux.
Q: Which has the smaller attack surface, and why is that not automatically “better”? A: Flux — it runs only controllers, with no network-exposed UI or API server, and access is pure Kubernetes RBAC. But Argo CD’s larger surface is the price of the UI, SSO, and built-in multi-tenancy many teams want. Smaller surface is a security win only if you do not need the platform features you would be giving up.
Q: You are advising a 150-developer product org with app teams and rotating on-call. Which do you lean toward and why?
A: Argo CD, most likely — the built-in UI (visibility for mixed-skill developers), SSO, and AppProject multi-tenancy fit an app-delivery-as-a-platform model, and the UI flattens the GitOps learning curve for a large, rotating group. I would still confirm they do not have a hard requirement (real Helm releases, image-automation-to-Git) that tips it back to Flux.
Q: When would you genuinely recommend Flux over Argo CD? A: When the team is platform/infra engineers who want a CLI/Git-only, minimal-footprint, K8s-native toolkit; when best-in-class image automation written to Git matters; when real Helm release semantics are required; when a small attack surface is a priority; or when native SOPS decryption and per-cluster decentralised reconciliation fit the architecture better than a hub.
Q: Is running both Argo CD and Flux a sign of indecision?
A: No — it is a legitimate, common architecture. Flux often bootstraps clusters and reconciles the platform layer (Git-native bootstrap, small footprint), while Argo CD serves as the developer-facing app-delivery plane (UI, AppProject). Using each where it is strongest is sound, provided they never co-manage the same objects.
Q: What is the one rule you must never break when migrating between them or evaluating both? A: Never let both tools reconcile the same objects at once — they will each see the other’s writes as drift and revert them, flapping forever. Migrate app-by-app in separate namespaces (one owner per object), verify, then retire the old wrapper.
Key takeaways
- Both are CNCF-graduated, pull-based GitOps tools solving the same core problem; deploy the same app with each and the cluster ends up identical. There is no universal winner — choose on fit, not on quality.
- The mental model: Argo CD is an integrated platform you log into; Flux is a composable controller toolkit you assemble. Nearly every difference flows from that one architectural bet.
- Helm is the sharpest technical difference. Argo renders (
helm template, no release object,helm listempty, rollback = Git revert); Flux performs a real Helm release (release Secret,helm listworks, Helm hooks/tests/rollback run). Know which model your charts need before choosing. - Lean Argo CD for a first-class UI, built-in SSO/RBAC/multi-tenancy (
AppProject), hub-spoke multi-cluster with one pane, turnkeyApplicationSetfan-out, and mixed-skill teams the UI helps. - Lean Flux for a CLI/Git-only minimal-surface toolkit, best-in-class image automation written back to Git, real Helm semantics, native SOPS decryption, K8s-native tenancy via impersonation, and decentralised per-cluster reconciliation.
- Progressive delivery should not decide it — both delegate to excellent, decoupled sibling projects (Argo Rollouts, Flagger) that work with either engine.
- Many mature orgs run both — Flux for the platform layer, Argo CD as the developer-facing plane — and that is a respectable design, not a failure to decide.
- The universal safety rule: never let both tools (or two Applications/Kustomizations) reconcile the same objects simultaneously; one owner per object, always.
Glossary
- GitOps — an operating model where Git is the single source of truth for desired state and an in-cluster controller continuously reconciles the live cluster toward it. Both tools implement it.
- Pull-based reconciliation — the controller pulls desired state from Git from inside the cluster; nothing outside needs cluster credentials. The shared security property that makes GitOps attractive.
- Drift / self-heal — drift is the live cluster diverging from Git (someone ran
kubectl edit); self-heal is the controller reverting it. Argo opts in viaselfHeal; Flux does it by default. - Prune — deleting a resource from Git deletes it from the cluster. Argo
prune: true; Fluxspec.prune: true. - CNCF Graduated — the Cloud Native Computing Foundation’s highest maturity tier (the shelf as Kubernetes and Prometheus). Both Argo CD and Flux are graduated.
- GitOps Toolkit — Flux’s set of small, independent controllers (source, kustomize, helm, notification, image). “Flux 2” is these controllers.
- source-controller — the Flux controller that fetches and verifies artifacts from Git/Helm/OCI/Bucket and serves them internally. Owns
GitRepository,HelmRepository,OCIRepository,Bucket,HelmChart. - kustomize-controller — the Flux controller that builds and applies Kustomize/plain-YAML, prunes, and health-checks. Owns
Kustomization. - helm-controller — the Flux controller that performs a real Helm release from a source. Owns
HelmRelease. - Application (Argo CD) — the single CRD (
argoproj.io/v1alpha1) that carries both where the source is and what to do with it. Flux splits those into two objects. - ApplicationSet / generator — Argo’s fan-out controller: a generator (Git, cluster, matrix, PR, list, SCM) stamps out one
Applicationper generated element. - AppProject — Argo’s multi-tenancy boundary: restricts which repos, destinations, and resource kinds its
Applications may touch, enforced by Argo’s own RBAC. - GitRepository (Flux) — the “what to fetch” object; fetched once and referable by many
Kustomizations. - Kustomization (Flux) — the “what to build and apply from a source” object. Not the same as a
kustomization.yamlfile. - HelmRelease (Flux) — declares a real Helm release performed by helm-controller (
helm.toolkit.fluxcd.io/v2). - Helm render (
helm template) — Argo’s model: the chart is rendered to plain manifests and applied. No Helm release object exists, sohelm listis empty. - Real Helm release / release Secret — Flux’s model: an actual
helm install/upgrade, recorded in ash.helm.release.v1.<name>.<rev>Secret thathelm listandhelm rollbackcan see. - Impersonation (
serviceAccountName) — Flux applies as a named ServiceAccount, so native Kubernetes RBAC (not Flux) decides what a tenant may do. - Resource tracking (Argo) / inventory (Flux) — the ownership marker each tool stamps on managed objects (Argo’s
tracking-idannotation or instance label; Flux’sstatus.inventory+ SSA field manager). What makes “one owner per object” enforceable. - Server-side apply (SSA) — the API server merges and tracks field ownership by manager. Flux uses it by default; Argo offers it via
ServerSideApply=true. - Hub-spoke vs per-cluster — Argo’s default multi-cluster topology (one control plane registers many clusters) vs Flux’s (a Flux install per cluster reconciling itself).
flux bootstrap— Git-native install that commits Flux’s own manifests into your repo, so Flux then manages itself from Git.- SOPS — Mozilla’s secret-encryption tool; Flux’s kustomize-controller decrypts it natively, while Argo reaches the same end via Sealed Secrets / ESO / a SOPS plugin / a cloud secret store.
- Argo Rollouts / Flagger — the decoupled progressive-delivery projects (canary, blue-green). Rollouts is Argo’s sibling, Flagger is Flux’s — but either works with either engine.
- Argo CD Image Updater — Argo’s separate image-automation project (annotations on the
Application); Flux’s built-in image-reflector + image-automation controllers are generally considered more polished.