Flux vs Argo CD
Choosing your GitOps tool.
Two locksmiths keep an office building matching its master blueprint. One works out of a toolbox: no shopfront, no reception desk, they read the plans and fix the locks directly. The other runs a staffed control room with a wall of monitors, a sign-in desk, and a board showing every door's status. Both keep the building true to the plan. They give you very different things to watch and to protect. Flux and Argo CD are those two locksmiths for Kubernetes (the open-source system that runs and coordinates your containers across a fleet of machines). Both are CNCF-graduated (Cloud Native Computing Foundation, the open-source body that stewards Kubernetes) GitOps engines. GitOps means you keep the desired state of your cluster in Git (the version-control system that records every change to a set of files), and a controller (a background program that watches the cluster and acts to fix it) reconciles the live cluster to match, correcting drift as it goes. Same job. Different shape. And for a defender, a different thing to secure.
Two shapes of the same job
Flux is a toolbox. It ships as a handful of small controllers (background programs that each watch one kind of object and reconcile it), and you compose them with CRDs (Custom Resource Definitions, a way to add your own object types to the Kubernetes API). There is source-controller, which fetches Git and OCI (Open Container Initiative, the open standard for packaging container images and related artifacts) sources; kustomize-controller, which renders and applies manifests (the YAML files that declare what should run); helm-controller, which installs Helm charts (Helm is a package manager for Kubernetes); and a couple more. There is no first-party Flux server and no built-in dashboard. You drive it from the CLI (command-line interface) and from Git, and Flux manages Flux the same way it manages everything else, as objects in Git.
Argo CD moves the control plane somewhere you can log into. It runs a stateful application controller plus an API (application programming interface) server with a rich web UI (user interface). The UI draws your live resource tree, shows a diff of desired state against what is actually running, and lets an operator sync or roll back with a click. The desired state is still Argo Application objects, usually stored in Git, but the server is the operational hub: the thing you harden, back up, and hand people accounts on. Flux leans Git-native and headless. Argo leans server-and-UI-centric. Neither is more 'real' GitOps. They put the control plane in different places.
Here is the same deployment expressed for each engine. Read them next to each other.
# Flux: a Kustomization drives reconciliation, no server involvedapiVersion: kustomize.toolkit.fluxcd.io/v1kind: Kustomizationmetadata:name: podinfonamespace: flux-systemspec:interval: 10msourceRef:kind: GitRepositoryname: gitopspath: ./apps/podinfoprune: true # delete resources removed from Git
# Argo CD: an Application, reconciled by the argocd controllerapiVersion: argoproj.io/v1alpha1kind: Applicationmetadata:name: podinfonamespace: argocdspec:project: defaultsource:repoURL: https://github.com/org/gitopstargetRevision: mainpath: apps/podinfodestination:server: https://kubernetes.default.svcnamespace: podinfosyncPolicy:automated:prune: true # OFF by default, you must opt inselfHeal: true # correct manual drift, also OFF by defaultsyncOptions:- CreateNamespace=true
Look hard at the syncPolicy block in the Argo Application. Turning on automated does not turn on pruning or self-heal. Both prune and selfHeal default to false. So a resource you delete from Git quietly lingers in the cluster, and a manual kubectl edit (kubectl is the command-line tool for talking to Kubernetes) is left in place, because drift on its own never triggers a sync when selfHeal is off. That is the reverse of what most people mean by GitOps. Flux's Kustomization prunes the moment you set prune: true, and it re-applies the full desired state on every interval, so drift gets corrected without a second flag. The defaults train different instincts, and that difference bites teams during migration more than any feature gap does.
What you actually secure is different
For an operator, the sharpest difference shows up when you list the running pods. Ask each namespace (a named partition of the cluster) what it is running, and the security bill becomes obvious.
kubectl get pods -n flux-system
NAME READY STATUS RESTARTS AGEhelm-controller-5b9c8d7f6-x2p9q 1/1 Running 0 6dkustomize-controller-6d8f7c9b4-lm3tz 1/1 Running 0 6dnotification-controller-7c9d8f6b5-nv4kx 1/1 Running 0 6dsource-controller-8f7d9c6b4-pq2wn 1/1 Running 0 6d
kubectl get pods -n argocd
NAME READY STATUS RESTARTS AGEargocd-application-controller-0 1/1 Running 0 6dargocd-applicationset-controller-6b7c9f8d4-2xk9p 1/1 Running 0 6dargocd-dex-server-7f9c6d5b4-9wq2r 1/1 Running 0 6dargocd-notifications-controller-5c8d7f6b9-lm4tz 1/1 Running 0 6dargocd-redis-6fd9c88b5-nv8kx 1/1 Running 0 6dargocd-repo-server-7d9f8c6b5-pq7wn 1/1 Running 0 6dargocd-server-59d7c9f8b-tzk6x 1/1 Running 0 6d
Flux gives you four narrow controllers by default and nothing else. No login, no web endpoint, no session store. The trust boundary lives in two places you already run: the Git repository (protect it with reviews and signed commits) and Kubernetes RBAC (Role-Based Access Control, the cluster's own permission system), which scopes what each controller may apply.
Argo CD hands you a platform to defend. argocd-server exposes an API and a UI over the network, so anyone who can reach it gets a login prompt worth attacking. argocd-dex-server brokers SSO (Single Sign-On) to your identity provider. argocd-redis (Redis is an in-memory data store) caches the manifests the repo-server renders. Admin session tokens are signed with a key held in the argocd-secret object, so that secret and that Redis cache are inside your blast radius now. None of this is a mark against Argo. It buys you a place to see and govern GitOps across a fleet. But you own a server: put it behind SSO, fence it with a NetworkPolicy (a cluster firewall rule that limits which pods can reach it), back up argocd-secret, and watch the server logs for failed logins. Flux has less to secure because it has less surface to attack.
Scaling to many teams
Both tools answer fan-out and multi-tenancy, and each answers in character. Flux composes. One Kustomization waits on another through dependsOn, each tenant gets its own GitRepository plus a locked-down ServiceAccount (an identity for workloads inside the cluster), and reconciliation runs under that identity by impersonation. A tenant's GitOps can do exactly what their ServiceAccount's RBAC allows and nothing more, enforced by the same rules that govern the rest of the cluster.
Argo CD reaches for ApplicationSets. One controller-generated template fans a single spec across clusters, directories, or pull requests, backed by AppProjects that fence which repositories, destinations, and resource kinds a team may touch. If you want a self-service dashboard where a platform team hands developers a guarded slice of one shared Argo instance, that project-and-RBAC story is hard to beat. If you would rather every tenant be a plain Git directory reconciled by its own controller with no shared server to guard, Flux fits more naturally. The trade is centralized governance against a smaller blast radius.
apiVersion: argoproj.io/v1alpha1kind: ApplicationSetmetadata:name: tenantsnamespace: argocdspec:generators:- git:repoURL: https://github.com/org/gitopsrevision: maindirectories:- path: tenants/* # one Application per tenant dirtemplate:metadata:name: '{{path.basename}}'spec:project: '{{path.basename}}' # matched by an AppProject of the same namesource:repoURL: https://github.com/org/gitopstargetRevision: mainpath: '{{path}}'destination:server: https://kubernetes.default.svcnamespace: '{{path.basename}}'syncPolicy:automated:prune: trueselfHeal: true
Driving each on day two
Operation diverges as much as architecture. With Flux you inspect and nudge entirely from the CLI, and there is no extra endpoint to expose or patch. You ask what it reconciles, and you tell it to reconcile now.
flux get kustomizationsflux reconcile kustomization podinfo --with-source
NAME REVISION SUSPENDED READY MESSAGEflux-system main@sha1:a1b2c3d False True Applied revision: main@sha1:a1b2c3dpodinfo main@sha1:a1b2c3d False True Applied revision: main@sha1:a1b2c3d► annotating GitRepository gitops in flux-system namespace✔ GitRepository annotated◎ waiting for GitRepository reconciliation✔ fetched revision main@sha1:9f4c2e1► annotating Kustomization podinfo in flux-system namespace✔ Kustomization annotated◎ waiting for Kustomization reconciliation✔ applied revision main@sha1:9f4c2e1
With Argo you mostly work through the UI, but the argocd CLI mirrors it faithfully for scripting and CI (Continuous Integration) pipelines. You log in to the API server first, which is itself a standing reminder that there is a server to reach and therefore a server to protect.
argocd login argocd.example.com --ssoargocd app create podinfo \--repo https://github.com/org/gitops \--path apps/podinfo \--dest-server https://kubernetes.default.svc \--dest-namespace podinfoargocd app sync podinfoargocd app get podinfo
Name: argocd/podinfoProject: defaultServer: https://kubernetes.default.svcNamespace: podinfoURL: https://argocd.example.com/applications/podinfoSource:- Repo: https://github.com/org/gitopsTarget: mainPath: apps/podinfoSync Policy: <none>Sync Status: Synced to main (9f4c2e1)Health Status: HealthyGROUP KIND NAMESPACE NAME STATUS HEALTH MESSAGEService podinfo podinfo Synced Healthy service/podinfo createdapps Deployment podinfo podinfo Synced Healthy deployment.apps/podinfo created
A common and defensible setup runs both. Argo CD as the human-facing dashboard, Flux for unattended flows like image automation. Flux ships image-reflector-controller and image-automation-controller, which scan a registry for new tags and write the chosen tag back to Git as a commit, so the promotion lands in your history where you can audit it. Argo CD has no built-in equivalent (you add the separate Argo CD Image Updater project or wire it through CI). Whichever you run, the desired state stays in Git. The tool is the reconciler, and in Argo's case, the window you watch it through.
Before you commit to either tool, do one concrete thing: install it in a throwaway namespace, run kubectl get pods, and write down every endpoint, secret, and account the install creates. That inventory is your hardening checklist, and it is the honest measure of what each option costs you to run safely.
Try this
Run kubectl get pods -n flux-system on a scratch host or disposable cluster and read the output against what this lesson described. Then change one input so it fails, and re-run: the error you get is the one you will meet in production.
Takeaway
The trap worth remembering here: auto-sync is not auto-prune, and never double-own a namespace. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.