Flux
Every GitOps tool on this path answers the same four principles — declarative, versioned and immutable, pulled automatically, continuously reconciled — and Argo CD answers them with one product built around a single Application CRD. Flux answers the exact same principles with a completely different shape: a small family of narrow, single-purpose controllers — the GitOps Toolkit — that you compose yourself instead of adopting whole. There's no built-in web UI and no one CRD to learn; instead there's a GitRepository that says where the config lives, a Kustomization or HelmRelease that says what to do with it, and four more controllers handling notifications and fully automated image updates. CGOA's Tooling domain names Flux directly, alongside Argo CD, as a reconciliation engine. This page goes past the principle and into the controllers, the CRDs you'll actually write, the commands, and the failure modes that catch people who only skimmed the quickstart.
Picture a workshop with six small, very literal assistants instead of one all-purpose robot. One assistant's only job is walking to the supply shelf — Git, a container registry, a Helm repo, a storage bucket — grabbing the latest instructions, and setting them on the bench. A second assistant reads the bench and builds plain parts from it. A third reads it and assembles kits from a catalog (that's Helm charts). A fourth watches the finished build and texts you if something's wrong. And two more just watch the parts catalog for a shiny new version and quietly rewrite the instructions on the bench themselves. Nobody in the room ever picks up a wrench on your behalf — you only ever edit the instructions, and each assistant does its one narrow job, forever, on its own schedule.
Architecture: the GitOps Toolkit's six controllers
☺ Like you're 10: One assistant fetches the instructions, two more turn them into real parts, one shouts if a build breaks, and two watch the parts catalog for updates.
Flux installs into a namespace — conventionally flux-system — as a set of ordinary Deployments, each watching its own CRDs and writing status back to them. source-controller is the one every other controller depends on: it fetches from a GitRepository, OCIRepository, HelmRepository, HelmChart, or Bucket, verifies the result with a checksum, and republishes it as an artifact served over an in-cluster HTTP endpoint. kustomize-controller and helm-controller each consume that artifact independently — one runs a Kustomize build over plain YAML, the other drives the Helm SDK — and both apply, prune, and health-check what they produce. notification-controller turns a Ready or Failed condition into an outbound alert and can also accept an inbound webhook that triggers an early reconcile instead of waiting out the interval. Two more controllers close a separate loop entirely: image-reflector-controller scans a registry and catalogues tags by policy, and image-automation-controller writes the winning tag back into your manifests as a real Git commit.
If you find an old blog post about fluxctl, a standalone Helm Operator, or annotation-driven automation on a Deployment, it's describing Flux v1, long end-of-life. Everything on this page is Flux v2 — the CNCF-graduated toolkit, driven by the flux CLI and by CRDs in the *.toolkit.fluxcd.io API groups.
The six controllers never call each other directly — they coordinate only through Kubernetes objects and the shared, checksummed artifact source-controller serves over an in-cluster HTTP endpoint. kustomize-controller doesn't clone Git; it fetches the artifact source-controller already produced. That's the whole design in one sentence: separate "where does config come from" from "what do I do with it," and let Kubernetes' own watch mechanism glue the pieces together. It's why one GitRepository can safely feed twenty Kustomizations without twenty separate clones, and why any one controller can crash and restart without the other five noticing.
The resources you actually write
☺ Like you're 10: Almost everything reduces to two objects — "here's where the stuff lives" and "here's what to do with it."
Ninety percent of a real Flux repo is a GitRepository plus a handful of Kustomizations. The Kustomization here is not the same thing as a plain kustomization.yaml file — it's a CRD that tells kustomize-controller which source and path to build, then applies, prunes, and health-checks the result on a schedule.
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: fleet-config
namespace: flux-system
spec:
interval: 1m # how often source-controller re-checks the remote
url: https://github.com/kubestronaut/fleet-config.git
ref:
branch: main # or tag: / semver: / commit:
secretRef:
name: fleet-config-auth # basic-auth or SSH key Secret
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: mission-log
namespace: flux-system
spec:
interval: 10m # re-apply cadence even without a new commit — drift correction
retryInterval: 2m # back-off used only after a failed reconciliation
sourceRef:
kind: GitRepository
name: fleet-config
path: ./apps/mission-log/overlays/prod
prune: true # delete cluster objects removed from Git
wait: true # block until ALL applied objects report Ready
timeout: 5m
targetNamespace: mission-log
dependsOn:
- name: infrastructure # do not start until this Kustomization is Ready
healthChecks: # ignored entirely while wait: true is set
- apiVersion: apps/v1
kind: Deployment
name: mission-log
namespace: mission-logTwo fields get confused constantly. wait: true is the blunt instrument — the controller waits for every applied object to become Ready, and the healthChecks list is ignored while it's set. healthChecks is the scalpel — leave wait unset and name only the specific objects whose readiness actually gates you, useful when a Kustomization applies a hundred objects but only one Deployment matters for ordering. And interval is not retryInterval: interval is the steady-state cadence that corrects drift even when Git hasn't changed; retryInterval is the shorter back-off used only right after a failure.
HelmRelease is the declarative counterpart for Helm: you describe the chart and the values, and helm-controller decides whether that means an install, an upgrade, or nothing. Its remediation blocks are what make it safer than a pipeline running helm upgrade by hand — a failed upgrade can retry and then roll back automatically, with no one paged at 2am to type the rollback command themselves.
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: kubestronaut-charts
namespace: flux-system
spec:
interval: 1h
url: https://charts.kubestronaut.example/stable
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: telemetry-agent
namespace: flux-system
spec:
interval: 10m
targetNamespace: mission-log
chart:
spec:
chart: telemetry-agent
version: "3.x" # semver range, resolved by source-controller
sourceRef:
kind: HelmRepository
name: kubestronaut-charts
install:
remediation:
retries: 3
upgrade:
remediation:
retries: 3
remediateLastFailure: true # roll back to the last good release once retries run out
driftDetection:
mode: enabled # correct a manual kubectl edit on a Helm-managed object
values:
auth:
existingSecret: telemetry-agent-auth # reference a Secret — never inline the value"I don't have a Flux login, because there isn't one. My whole interface is a folder: apps/mission-log/overlays/prod/. I bump an image tag or a values field in a pull request, and about a minute after it merges it's live. If I want to know whether it landed, I run flux get kustomizations -A in the shared shell. Fewer buttons than Argo CD's UI, sure — but every answer is one command away and I never wonder which cluster I'm looking at."
Image automation: the built-in extra Argo CD doesn't have
☺ Like you're 10: Two assistants watch the parts catalog, and the moment a nicer part appears, they go rewrite the instructions on the bench themselves.
This is Flux's signature feature, and it has no first-party equivalent in core Argo CD. Three objects do the whole job: ImageRepository scans a registry on an interval, ImagePolicy picks a winner from the scanned tags by a filter and an ordering rule, and ImageUpdateAutomation rewrites the chosen tag into your manifests and pushes a real Git commit. A marker comment next to the image field is the only thing linking the policy to the YAML it's allowed to touch.
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageRepository
metadata:
name: mission-log
namespace: flux-system
spec:
image: ghcr.io/kubestronaut/mission-log
interval: 5m
---
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImagePolicy
metadata:
name: mission-log
namespace: flux-system
spec:
imageRepositoryRef:
name: mission-log
policy:
semver:
range: '>=1.0.0' # only ever propose a valid, newer semver tag
---
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageUpdateAutomation
metadata:
name: mission-log-auto
namespace: flux-system
spec:
interval: 5m
sourceRef:
kind: GitRepository
name: fleet-config
git:
checkout:
ref: { branch: main }
commit:
author: { name: fluxbot, email: fluxbot@kubestronaut.example }
push:
branch: main # or a side branch, to raise a PR instead of committing to main
update:
path: ./apps/mission-log
strategy: SettersAnd in the Deployment Flux is allowed to edit, the marker tells the automation exactly which field it may rewrite:
- name: mission-log
image: ghcr.io/kubestronaut/mission-log:1.4.2 # {"$imagepolicy": "flux-system:mission-log"}Image automation still goes through Git. Flux never patches the live Deployment directly — it commits the new tag, and the ordinary Kustomization reconcile loop picks it up exactly the way any other change would. Your audit log, your git revert rollback story, and all four OpenGitOps principles stay intact. Push to a side branch instead of main and you get an auto-raised pull request for any environment that needs a human to click approve first.
Day-to-day commands
☺ Like you're 10: The flux command mostly just asks and nudges — everything it can do, kubectl can also do, just with more typing.
# install, idempotent — safe to re-run, and how you upgrade Flux itself
$ export GITHUB_TOKEN=ghp_xxx
$ flux bootstrap github --owner kubestronaut --repository fleet-config \
--branch main --path ./clusters/prod --personal
$ flux check # every controller running, and which versions
$ flux get all -A # sources, kustomizations, helmreleases — Ready/Suspended
$ flux get kustomizations -A --status-selector ready=false
$ flux get sources git -A
$ flux reconcile source git fleet-config # stop waiting for the interval, fetch right now
$ flux reconcile kustomization mission-log --with-source
$ flux reconcile helmrelease telemetry-agent --with-source
$ flux suspend kustomization mission-log # the safe way to hand-edit during an incident
$ flux resume kustomization mission-log
$ flux diff kustomization mission-log --path ./apps/mission-log/overlays/prod
$ flux tree kustomization mission-log # every object this Kustomization owns
$ flux trace deployment/mission-log -n mission-log # which repo, path and commit put this here
$ flux logs --level=error --all-namespacesOn a throwaway cluster with a personal Git token: flux bootstrap into a fresh repo and read what Flux committed for itself. Add a public repo as a second GitRepository plus a Kustomization with prune: true — generate it with flux create kustomization --export and commit the YAML rather than applying it directly. kubectl scale the Deployment it creates by hand and watch the next reconcile revert you. Then flux suspend that Kustomization, edit it again, and confirm nothing happens until you flux resume. Finally point path at a folder that doesn't exist and read the failure with flux get kustomizations — that last step teaches you more than the first three combined.
Gotchas and failure modes
☺ Like you're 10: Most "Flux is broken" moments are really "Flux is asleep," "Flux is waiting on something that never finished," or "Flux read your folder correctly and is telling you it's wrong."
The interval trap
Flux doesn't react instantly by default — every object carries its own interval, and the worst-case latency for a change to land is the sum of every interval along the chain: source, then Kustomization, then anything that depends on it. Set the GitRepository interval to about a minute and add a Receiver webhook for near-instant reaction on top of it; don't set every interval to a few seconds trying to force speed, because that just hits your Git host's rate limits and hammers the API server for nothing. And the very first thing to check when "nothing is deploying" turns out to be true is whether something got flux suspended during a past incident and never resumed — flux get all -A has a SUSPENDED column for exactly this reason.
dependsOn without wait does almost nothing
dependsOn means "don't start until the named Kustomization is Ready." But if that dependency has wait: false and no healthChecks, it reports Ready the moment its objects are merely applied — not once they're actually working. Your app's Kustomization then proceeds while, say, a CRD it needs is still being established, and you get a stream of "no matches for kind" errors that look nothing like an ordering problem. If anything depends on a Kustomization, that Kustomization needs wait: true or explicit healthChecks — missing-CRD ordering is the single most common Flux bootstrap failure.
Kustomization.spec.postBuild.substitute runs an envsubst-style pass over every byte of the manifests it builds. Turn it on for a Kustomization that also ships an nginx config with $host in it, or a Prometheus rule referencing $labels, and Flux will happily replace those with an empty string — a silent, hard-to-spot corruption that only shows up once the workload is running the wrong config. Escape a literal dollar sign as $$, or scope postBuild to only the Kustomizations that actually need variable substitution rather than turning it on globally.
Where CGOA's Tooling domain gets concrete
☺ Like you're 10: The exam asks in the abstract where the instructions live and who's allowed to read them — Flux gives you a very literal, very inspectable answer to both.
CGOA's Tooling domain names state store systems and reconciliation engines as separate, examinable ideas, and Flux keeps them separate architecturally, not just conceptually. source.toolkit.fluxcd.io is the entire state-store layer — a GitRepository most often, but a Bucket or an OCIRepository works exactly the same way to kustomize-controller, because both are judged against "versioned and immutable," not against being Git specifically. kustomize.toolkit.fluxcd.io and helm.toolkit.fluxcd.io are the reconciliation-engine layer, and they never know or care which kind of source fed them. That clean separation is also why the blueprint's phrasing is "Argo CD, Flux, and alternatives" rather than naming Flux as the one true answer: the specification doesn't require a toolkit shape at all, Flux just happens to make the specification's own boundaries visible as separate CRDs you can point at individually.
One boundary worth being precise about: Flux is not named on the CAPA blueprint, which is scoped tightly to the Argo Project — Argo CD, Argo Rollouts, Argo Workflows, and Argo Events. If a question is about Flux specifically, it's a CGOA question about reconciliation engines in general, not a CAPA question about the Argo family.
Flux vs. the alternatives
☺ Like you're 10: Argo CD is a finished product with one dashboard; Flux is a box of parts you assemble yourself — neither one is the "more correct" choice.
| Option | Model | Best when | Costs you |
|---|---|---|---|
| Flux | Composable GitOps Toolkit — six narrow controllers, no built-in UI | You're composing your own platform APIs, lean heavily on Helm or OCI sources, or want built-in image automation | No official dashboard, so developer visibility has to be built or bought; more CRDs to learn |
| Argo CD | Pull-based GitOps reconciler; one Application CRD; built-in web UI | Many application teams need self-serve visibility into sync status and diffs without a kubeconfig | Kubernetes-only; image automation and native Helm releases both need a separate add-on |
Plain CI push (helm upgrade --install from the pipeline) | The pipeline runner applies changes directly, once, at deploy time | A small team, one cluster, and a reconciler's overhead genuinely isn't worth it yet | No drift detection, no self-heal, and cluster credentials live in CI — fails Pulled Automatically and Continuously Reconciled outright |
Both Flux and Argo CD are CNCF-graduated, both implement all four OpenGitOps principles fully, and the honest tie-breaker is shape and audience, not correctness. Reach for Argo CD when the platform's customers are many development teams who want to look at a screen; reach for Flux when the platform team itself is composing higher-level abstractions and values small, namespaced CRDs that compose cleanly underneath them. Running both is a defensible, common answer too — Flux reconciling cluster add-ons and infrastructure, Argo CD serving app teams their own dashboards — provided a hard line exists about which engine owns which namespace, because two reconcilers fighting over one object is a genuinely miserable incident. Flux does not appear as a domain on the core Kubernetes exams — CKA, CKAD and CKS examine the built-in controllers that share this exact reconcile-loop shape, not this specific implementation of it — but it's explicit, named content on CGOA's Tooling domain, and it carries real weight on the Platform Engineering course's own CNPE track, where GitOps & Continuous Delivery is tied for the exam's single largest domain at 25% — see the far deeper Flux reference there, including the Helm-release, image-automation, and multi-tenancy subsystems this page only summarizes.
Benny the Beaver: Just bootstrapped Flux against fleet-config. Six controllers, zero UI, and I already love it more than I expected to.
Foxy: No UI at all? How do you even see what's running without clicking around?
Benny: flux get all -A is the dashboard. Every object just tells you its own status — I don't have to guess.
Recon the Robot: BEEP. I run Argo CD's loop as one controller with one CRD. Benny's running six that only ever talk through the API server. Different shape, same four principles underneath.
Gizmo: Reconciling too slow for you, Benny? Just set every interval to five seconds. Instant everything! 🤑
Timmy the Turtle: That's how you get rate-limited by your Git host and hammer the API server for nothing. A one-minute source interval plus a Receiver webhook gets you the same instant reaction, without setting the house on fire.
1. Name the three controllers on Flux's core delivery path and the CRD each one owns. 2. What's the difference between a plain kustomization.yaml file and a Flux Kustomization resource? 3. You set dependsOn: infrastructure but your app's Kustomization still fails with "no matches for kind." What's the likely misconfiguration? 4. Name the three CRDs behind Flux's image automation and what each one does. 5. Which flux command tells you the exact repo, path, and commit SHA that produced a live object in the cluster? 6. Is Flux named on the CAPA blueprint? Which certification actually names it as a reconciliation engine?
Check your answers
source-controllerownsGitRepository(andOCIRepository/HelmRepository/Bucket);kustomize-controllerownsKustomization;helm-controllerownsHelmRelease.- A
kustomization.yamlis a plain file read by the Kustomize build engine. A FluxKustomizationis a CRD inkustomize.toolkit.fluxcd.iothat tellskustomize-controllerwhich source and path to build, then applies, prunes, and health-checks the result on a schedule — same word, different layer. - The
infrastructureKustomization almost certainly haswait: falseand nohealthChecks, so it reports Ready as soon as its objects are applied — before, say, a CRD it installs is actually established. Setwait: trueor add explicithealthCheckson the dependency. ImageRepositoryscans a registry and catalogues tags;ImagePolicypicks a winning tag by filter and ordering rule;ImageUpdateAutomationrewrites the chosen tag into Git and pushes a commit.flux trace deployment/<name> -n <namespace>.- No — CAPA is scoped tightly to the Argo Project (Argo CD, Rollouts, Workflows, Events). Flux is named directly on CGOA's Tooling domain, alongside Argo CD, as a reconciliation engine.