Home / Stories / Beyond Flux on GKE
Story · Platforms In development

New infrastructure gets measured before anything is proposed.

Acme runs one platform repository that drives six clusters. Two are OpenShift hubs, three are AKS spokes, and one more is an OpenShift spoke. The two hubs reconcile the rest: they apply the spokes' declared state from git. The assistant may not write a single line there until it takes a read-only census (a count of what is where). The census shows which files land on which cluster and who owns each object. It also shows how much of the setup its agents can actually analyse.

Starts fromA local clone of your platform repository at a pinned commit. Optionally, it also uses read-only access that you grant during the session.
Looks atEvery Flux Kustomization and HelmRelease, kustomize overlays and patches, kubeConfig references, and each hub's Flux inventory. Flux is the tool that applies git to a cluster.
You getA coverage report. It shows placements per file, ownership per object, topology findings, and an honest "not analysable yet" count.
01 · Which platform is this?

Flavour and platform come from authoritative signals.

The cluster-deployment scan asks the API server what it serves. The config.openshift.io Infrastructure object means OpenShift. Its status.platform says vSphere, bare metal or Azure. harvesterhci.io will mean Harvester (roadmap). The scan reads RKE2 and k3s from node markers. It reads AKS from its node-pool labels.

Absence is not evidence. A cluster with no cloud labels is unknown, not "bare metal". A vSphere cluster that runs another vendor's CSI driver is still vSphere.

02 · A runtime that cannot write

Read-only is a property of the code, not a flag.

The reach census runs in a separate, sealed runtime. Its import graph does not contain the merge-request writer, the proposal store, the chat notifier, the AI model (LLM) engine or any credential loader. A test walks that graph on every change. The test must also catch a planted violation.

It reads a local clone at an immutable commit through plain git plumbing. It never fetches, checks out or touches the working tree. No AI model sees your YAML during the census.

03 · Hub and spokes

A file's cluster is decided by its parent, not its path.

In a hub-and-spoke Flux layout, the hub reconciles everything. It reaches each spoke through a kubeConfig secret. Plain objects under a Kustomization with spec.kubeConfig land on the spoke. A HelmRelease patched to carry a kubeConfig lives on the hub. Its chart installs into the spoke.

The graph resolves every file to a controller, object and workload cluster, or to "unresolved". It never quietly assumes "local".

04 · Topology findings

The mistakes that only a topology can show.

Rule-based checks run over the graph and find these mistakes. A HelmRelease inside a spoke-applied layer is fatal, because spokes have no HelmRelease CRD (custom resource definition). The checks also find a path that no Kustomization covers and a ${VAR} that nobody declares. They find a directory that Flux applies recursively because it has no kustomization.yaml. They find a Namespace that a hub-render layer quietly creates on the hub.

If you already run your own render-based check, the assistant compares the graph with it, Kustomization by Kustomization. On the first real customer setup, the two agreed on every Kustomization.

05 · Who owns each object?

Ownership is per object, read from the hub's inventory.

Real setups are mixed. In one namespace, Flux may own the Namespace, RBAC and Routes. A CI script may apply the Deployments. A namespace allowlist is only a ceiling.

You grant read-only access during the session. With it, the census reads each Flux Kustomization's status.inventory on the controller cluster. It marks every object as in-inventory, not in-inventory, or abstain. The census reports the objects that Flux does not own. It never assumes that git manages them.

06 · How much can we actually analyse?

An unanalysed repository must never read as clean.

The coverage meter counts four things per Flux root. It counts files placed, files unresolved, and kustomize render inputs. It also counts kinds that the shipped agents do not handle yet: OpenShift Route, EgressIP, MachineConfig, OLM subscriptions, KubeVirt VirtualMachine.

The report lists those kinds as "seen, not analysed", with the count. Then nobody mistakes a quiet result for a healthy one. The number also tells us which platform pack to build next.

07 · Argo CD Roadmap

The same questions, asked of a different controller.

Planned: Argo CD Application and ApplicationSet graphs as a second reconciliation source. The census already keeps "which cluster does this file land on" and "who owns this object" apart from the Flux specifics. An Argo CD reader plugs in at that seam.

08 · Below the cluster Roadmap

Terraform and OpenTofu: measured state versus declared state.

Planned: the same loop applied to cloud infrastructure code. The assistant reads the declared resources and the provider's actual state. Where they disagree, it proposes the minimal reviewed diff. It manages only what the repository declares.

Nothing on this step exists yet. It is where the platform coverage is heading.

cluster flavour · platform detection
  • hub-prod config.openshift.io served → openshift · Infrastructure: vsphere
  • spoke-a kubernetes.azure.com/* labels → azure
  • spoke-b kubernetes.azure.com/* labels → azure
  • edge-1 rke2 node marker → rke2
  • vm-1 harvesterhci.io served → harvester-hci (roadmap)
  • lab-3 no authoritative signal → platform unknown
platform never inferred from absence, installed operators or CSI drivers
Inside the reach runtime
  • git reader: rev-parse, ls-tree, cat-file
  • Flux graph + kustomize closure
  • deterministic rules
  • read-only Kubernetes reader (session only)
  • local report writer
Not importable from it
  • merge-request / PR writer
  • proposal store
  • chat notifier
  • LLM engine
  • app config + credentials
emission firewall test
✓ import closure excludes the writer spine
✓ positive control: planted import caught
✓ git verbs limited to read plumbing
✓ poisoned writers untouched by a full census run
  names in the report use aliases, never real ones
hub-prod · controller
local12 files
ns
rb
rt
HelmRelease objectshub
hr
hr
hr
spoke-a · via kubeConfig
remote_directplain CRs
cm
sa
remote_hub_rendercharts
⎈
⎈
spoke-b · via kubeConfig
remote_directplain CRs
cm
unresolvednever "local"
?
clusters/spoke-a/addons.yaml
kind: Kustomization
spec:
  path: ./infrastructure/spoke-a
  patches:
    - target: { kind: HelmRelease }
      patch: - op: add  path: /spec/kubeConfig  # object on hub, chart on spoke
reach.flux.* · deterministic findings
  • helmrelease_under_kubeconfig 0 · fatal class, none present
  • uncovered_path 19 files no Kustomization reaches
  • autogenerated_kustomization 4 dirs applied recursively
  • substitution_undeclared 3 · ${CLUSTER_NAME} with no value
  • hub_render_layer_non_helm_object 3 · Namespace lands on the hub, not spoke-a
oracle: your render check vs the graph · 24/24 Kustomizations agree
Flux Kustomizationon hub-prod
status.inventoryread-only, explicit context
Ownershipper object, per placement
namespace shop-prod · who owns what
  • Namespace shop-prod in Flux inventory
  • RoleBinding, Route ×2, ExternalSecret in Flux inventory
  • Deployment orders-api, payments applied by CI · not in inventory
  • ConfigMap ui-templates on one hub only · flagged for you
coverage.md · acme-platform @ pinned commit
analysable 71% seen, not analysed unresolved
  • LocalVolume · EgressIP · MachineConfig OpenShift · no agent branches on these yet
  • WebApplicationFirewallPolicy · Gateway API AKS add-ons
  • Subscription · OperatorGroup OLM
  • kustomize patches and config docs render inputs · excluded from the denominator
Numbers from the same example run as the report below.
FluxKustomization · HelmRelease
Placement + ownershipcontroller-neutral
Argo CDApplication · ApplicationSet
planned · apps/orders-api.yamlroadmap
apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
  source: { path: apps/orders-api/overlays/prod }
  destination: { name: spoke-a, namespace: shop }  # → placement
planned · infra/network.tfroadmap
resource "google_compute_firewall" "ingress_https" {
  source_ranges = ["0.0.0.0/0"]
-  ports         = ["443", "8443"]
+  ports         = ["443"]  # 8443 unused for 90 days
}

The report you get.

This is not a merge request. The census cannot open one. It writes a local report, and we walk through it with you. The numbers come from an example run, with the same shape as the real output.

Report

Reach census · acme-platform

6 clusters · 2 hubs · 1 pinned commit · read-only · aliased names
reach censusread-onlyno LLMoracle 24/24
CoveragePlacementsOwnershipFindings
Summary

analysable 327 / 459 files (71%) · 151 render inputs · 14 unresolved. The commit that the census read matches the revision that both hubs reconcile now.

Flux rootFilesAnalysableUnresolved
clusters/hub-prod118962
infrastructure/spoke-a74510
tenants/1421129
apps/125683

Seen, not analysed: 106 objects of OpenShift and AKS-native kinds. A platform pack would raise reach by at most that many.

PlacementMeaningFiles
localapplied on the cluster that reconciles it373
remote_directapplied to a spoke with spec.kubeConfig82
remote_hub_renderHelmRelease on the hub, chart on the spoke20
multi-placementshared base reconciled by several clusters. A file counts once per placement, so the rows add up to more than 459.181
uncoveredno Kustomization reaches the path19
Per object, from each hub's Flux inventory
  • 790 in-inventory verdicts across both hubs.
  • 46 declared in git but not in any inventory (16 files). Some are overlay namespace rewrites that we do not model yet. 6 are genuine candidates, listed for you.
  • 0 abstentions. Shared bases get one verdict per placement.
For you to decide
  • A Namespace created on the hub by a hub-render layer, three times.
  • A ConfigMap present in one hub's git path and absent from the other.
  • Privileged developer RBAC declared on both hubs.
# findings.jsonl (excerpt)
{"rule": "reach.flux.uncovered_path", "path": "apps/legacy-report/prod/", "count": 19}
{"rule": "reach.flux.autogenerated_kustomization", "path": "tenants/team-b/", "count": 6}
{"rule": "reach.flux.substitution_undeclared", "var": "${CLUSTER_NAME}", "count": 3}
{"rule": "reach.flux.hub_render_layer_non_helm_object", "kind": "Namespace", "lands_on": "hub-prod"}
{"rule": "reach.flux.helmrelease_under_kubeconfig", "count": 0}
R
oracle comparison your own render-based spoke check vs the graph

24 of 24 Kustomizations got the same class. Every hub-render HelmRelease resolved to the right spoke. The assistant would report any difference as a graph defect.

What keeps it honest.

The assistant reads new infrastructure long before it writes to it. "We could not tell" is always a visible answer.

Cannot write, by constructionThe census runtime has no import path to any writer, store, notifier or AI model. A test with a planted violation proves the check works.
Pinned, not fetchedIt reads a clone at an immutable commit that you pulled. The report shows the commit and its age, so you can see if it is stale.
Live access is session-onlyOwnership reads use an explicit read-only context that you grant while you are present. The unattended identity of the assistant never gains access to your clusters.
Unknown is not cleanThe assistant counts unresolved placements and un-analysed kinds as such. They never quietly become "local" or "no findings".
Authority over guessesPlatform comes from the Infrastructure object or cloud labels. No signal gives "unknown", never a default.
Aliases in every outputReports use aliases for your clusters and repositories. The mapping stays in a local file that you do not commit.

Where this stands.

Live
  • Sealed reach census of a local clone: coverage, placements, topology findings
  • Hub-and-spoke Flux resolution, checked against a customer's own render check
  • Import firewall with positive control
  • Flavour and platform detection (OpenShift, RKE2, k3s, AKS)
In development
  • Per-object ownership from live Flux inventory (operator-present runs)
  • OpenShift cluster-shape collector
  • Kustomize overlay namespace rewrites in ownership matching
Roadmap
  • Harvester: detection and cluster-shape collector
  • OpenShift and KubeVirt rule packs (Route, EgressIP, VirtualMachine…)
  • Census analysis on a local, self-hosted model
  • Letting regular agents propose MRs on a censused setup
  • Argo CD Applications and ApplicationSets
  • Terraform / OpenTofu cloud IaC
← Previous storyLearning from your feedback