The exam names both Argo and Flux, and the lab installs both against the same Gitea for a reason: you should be able to express the same delivery in either. Flux has no Application object. It decomposes GitOps into small CRDs that reference each other, and reading those references is the skill.
make coreOrientation
Argo CD is one controller with one big CRD. Flux is a toolkit of small controllers with small CRDs, composed by reference. Neither is better; they fail differently, and the exam may hand you either. What transfers is the mental translation, which is the last exercise below.
| Controller | Owns | Reads |
|---|---|---|
| source-controller | GitRepository, OCIRepository, HelmRepository, HelmChart, Bucket | fetches and publishes an artifact (a tarball + revision) |
| kustomize-controller | Kustomization | an artifact + a path → builds and applies |
| helm-controller | HelmRelease | a chart source → installs/upgrades a release |
| notification-controller | Alert, Provider, Receiver | events out (Slack, webhooks) and events in (git push → instant reconcile) |
| image-*-controller | ImageRepository, ImagePolicy, ImageUpdateAutomation | registry tags → commits back to git |
That last row is worth knowing exists: Flux can close the CI→CD loop by writing the new image tag into git itself, which is the pull-based answer to "how does a new build get deployed without CI touching the cluster".
Working model: sources and appliers
GitRepository/platform (interval 1m, ref: main)
│ produces artifact @ revision main/9f2c1ab
├────────────▶ Kustomization/apps path ./apps prune ✓ interval 5m
├────────────▶ Kustomization/infra path ./infra prune ✓ dependsOn: []
└────────────▶ Kustomization/tenants path ./tenants dependsOn: [infra]
HelmRepository/podinfo ──▶ HelmRelease/podinfo ──▶ release in target namespace
The two-step split is the design: one GitRepository can feed many Kustomizations, each watching a different path with its own interval, health checks and prune setting. dependsOn then orders them, which is Flux's equivalent of Argo's sync waves, except it works between top-level objects rather than inside one sync.
Flux's Kustomization (kustomize.toolkit.fluxcd.io) is not kustomize's Kustomization (kustomize.config.k8s.io). The Flux one points at a directory; that directory may contain the other one. Exam tasks love this ambiguity, and kubectl get kustomizations.kustomize.toolkit.fluxcd.io versus reading a file resolves it.
Kustomization fields that decide behaviour
| Field | Effect |
|---|---|
| interval | how often it re-applies; drift correction is a side effect of re-applying, there is no selfHeal toggle |
| prune | delete resources removed from git; same blast radius as Argo's |
| wait / healthChecks / timeout | block Ready until the listed objects are healthy; this is what makes dependsOn meaningful |
| targetNamespace | override the namespace for everything the path applies |
| postBuild.substitute / substituteFrom | variable substitution from ConfigMaps/Secrets after kustomize build; Flux's answer to "one manifest, per-cluster values" |
| serviceAccountName | apply as that SA, the multi-tenancy control: a tenant's Kustomization cannot exceed its own RBAC |
| decryption | SOPS: decrypt secrets in the repo at apply time |
HelmRelease, in one paragraph
It references a chart (from a HelmRepository, a GitRepository, or an OCIRepository), sets values inline and/or valuesFrom ConfigMaps and Secrets, and controls failure behaviour with install.remediation and upgrade.remediation (retries, and whether to roll back). Newer versions also detect and correct drift on the release's rendered manifests. When one fails, the useful discrimination is which layer: source resolution (the chart could not be fetched), install/upgrade (Helm itself errored), or health (the release installed but the workload never became ready). Each surfaces in a different condition, and reading the right one is the whole diagnosis.
Daily verbs
flux get sources git # and: helm, oci, bucket, all
flux get kustomizations # Ready, revision, last applied
flux reconcile kustomization demo --with-source # force the loop now, refetch first
flux suspend kustomization demo # the sanctioned pause for surgery
flux resume kustomization demo
flux tree kustomization demo # what did this thing create
flux events --for Kustomization/demo
flux logs --level=error --all-namespaces
flux trace deploy/demo -n flux-demo # which Flux object owns this resourceflux trace is the reverse lookup: hand it any live resource and it tells you which Kustomization or HelmRelease put it there. On an unfamiliar cluster that is the fastest orientation command Flux has.
"Stop the controller overwriting my hotfix while I debug" has a named, auditable answer in both engines: flux suspend, or disabling auto-sync in Argo. Doing it with kubectl scale deploy/kustomize-controller --replicas=0 works and will cost you the mark, because it stops everything and leaves no record on the object.
The translation table
| Intent | Argo CD | Flux |
|---|---|---|
| where the manifests are | Application.spec.source | GitRepository + Kustomization.spec.path |
| auto-apply on change | syncPolicy.automated | implicit: every interval |
| revert live drift | automated.selfHeal | implicit: re-apply at interval |
| delete removed resources | automated.prune | Kustomization.spec.prune |
| ordering | sync waves + hooks | dependsOn + healthChecks + wait |
| pause | disable auto-sync | flux suspend |
| per-env values | kustomize overlays / Helm values | same, plus postBuild.substitute |
| tenant guardrails | AppProject | serviceAccountName + namespace isolation |
| instant trigger | webhook to argocd-server | Receiver (notification-controller) |
If you can move fluently across that table, "which tool will the exam give me" stops being a worry.
Exercises
Deploy the demo base (not the overlays, which Argo owns in 2.2; two controllers fighting over one resource is a lesson, not a setup) into a Flux-owned namespace:
kubectl create ns flux-demo
flux create kustomization demo-flux \
--source=GitRepository/platform \
--path=./demo-app/base \
--target-namespace=flux-demo \
--prune=true --interval=1m \
--health-check-timeout=2m
flux get kustomizationskubectl -n flux-demo get deploy demo shows 2/2. Then flux tree kustomization demo-flux and confirm it lists exactly the deployment, service, and nothing else.Scale the deployment by hand, then wait out one interval:
kubectl -n flux-demo scale deploy demo --replicas=5
sleep 70 && kubectl -n flux-demo get deploy demo -o jsonpath='{.spec.replicas}{"\n"}'flux suspend kustomization demo-flux, scale to 5 again, confirm it stays at 5 for two intervals, then flux resume kustomization demo-flux and confirm it snaps back.
flux get kustomizations as Suspended True. This is the answer to "make the controller stop overwriting my hotfix while I debug".Sources need not be git:
flux create source helm podinfo --url=https://stefanprodan.github.io/podinfo --interval=10m
flux create helmrelease podinfo --source=HelmRepository/podinfo \
--chart=podinfo --target-namespace=flux-demo --interval=5m
kubectl -n flux-demo get deploy podinfoBreak it once for the diagnostic reps: set --chart-version='>99.0.0' on a new helmrelease, read the failure in flux get helmreleases and kubectl describe helmrelease, then delete it. Failed source resolution vs failed install vs failed health check appear in different conditions, and knowing which layer failed is the troubleshooting pattern.
flux get helmreleases Ready True, release revision 1, and helm list -n flux-demo shows Flux as the release owner.Write down, from memory, the Flux equivalent of the Argo CD Application demo-staging from 2.2 (GitRepository exists; you need one Kustomization spec: path, targetNamespace, prune, interval). Verify against flux create kustomization --export.
Self-check
Flux has no selfHeal setting. Why is drift still corrected?
Because the Kustomization re-applies its rendered manifests every interval; correcting drift is a side effect of applying, not a separate feature. The corollary: your drift window is bounded by the interval, and shortening it costs API server load.
A Kustomization is Ready False. Name the three layers where it could have failed.
Source (the GitRepository is not Ready: auth, ref, or network), build (kustomize build failed, bad path or invalid YAML), apply/health (the API server rejected something, or health checks timed out). The conditions and flux logs name the layer; do not skip straight to the manifests.
How do you order "install CRDs, then the operator, then the tenant CRs" in Flux?
Three Kustomizations with dependsOn, each with wait: true (or explicit healthChecks) so a dependency counts as satisfied only when its objects are actually healthy. Without wait/healthChecks, dependsOn only orders the apply, not the readiness.
What is postBuild.substitute for, and what is its Argo CD analogue?
Injecting per-cluster or per-environment values into manifests after kustomize build, sourced inline or from ConfigMaps/Secrets. Argo's analogues are kustomize overlays, Helm values per Application, or ApplicationSet template parameters; Flux just lets you do it without another overlay directory.
How does Flux stop one tenant's Kustomization from deploying cluster-admin-level resources?
spec.serviceAccountName: the controller impersonates that ServiceAccount when applying, so the tenant's manifests can never exceed the tenant's RBAC. Combined with disallowing cross-namespace source references, that is Flux's multi-tenancy story: the structural equivalent of Argo's AppProject.
Docs to know your way around
- fluxcd.io: GitRepository, Kustomization, HelmRelease API references; the "flux CLI" cheat sheet; the multi-tenancy guide for the serviceAccountName pattern.
- Offline:
flux --helpandflux create <kind> --help --exportgenerate correct YAML without docs, which is faster than searching during the exam.