Managing Amazon EKS at Fleet Scale · Part 4 of 18

GitOps at Fleet Scale: Argo CD Hub-and-Spoke vs Flux Distributed

The operational heart of the golden path. Once you run dozens or hundreds of EKS clusters, the layer that keeps them identical — and lets you change all of them safely from one place — is GitOps. Here is how to choose between Argo CD’s single pane and Flux’s distributed pull, how the bridge from Part 3 feeds it, and when to run both.

TL;DR

  • Default to Argo CD hub-and-spoke with ApplicationSets. One control plane, one pane of glass over the fleet; a cluster generator fans each change out to every matching cluster.
  • Choose Flux (one controller per cluster, pull-based) when you need edge / air-gapped operation, per-cluster resilience, or want no central blast radius.
  • The single pane is real but bounded. Practitioners report the Argo UI dragging past roughly 3,000–5,000 apps (a community figure, not an AWS or project SLA), and the central hub is a blast-radius concern — size it like production.
  • The GitOps bridge from Part 3 feeds this layer: IaC writes each cluster’s metadata into the Argo cluster secret, and ApplicationSets consume it. Registering a cluster is what enrolls it.
  • The common hybrid: Flux for infrastructure and add-ons, Argo CD for apps — run both when both pulls apply.
  • Tooling specifics are current as of 2026-06-30; re-verify Argo/Flux and EKS Blueprints behavior at publish.

Design it this way

  • Make Git the only way in — humans propose via PR, a controller reconciles.
  • Fan out with an ApplicationSet cluster generator keyed off cluster labels.
  • Turn on automated sync + self-heal + prune so drift is corrected, not just detected.
  • Stand up EKS Pod Identity + access entries first — the controller needs both.

Don’t do this

  • Don’t kubectl apply out-of-band — the controller will revert you, or you’ll drift.
  • Don’t park 5,000 apps behind one Argo UI and expect it to stay snappy.
  • Don’t treat the central hub as free — it’s a blast radius; make it HA and back it up.
  • Don’t hand-wire each cluster — let the bridge write the cluster secret.

What GitOps actually buys a fleet — one source of truth, drift caught, self-heal

The verdict first: at fleet scale, GitOps is not a deployment preference, it’s the only realistic way to keep hundreds of clusters identical and to change them without logging into any of them. Git holds the desired state; a controller running inside each cluster continuously reconciles live state to match it.

Three properties are what make the setup worth it: a single source of truth (the repo, not someone’s laptop), drift detection (the controller notices when live state diverges from Git), and self-heal (it corrects that drift on its own).

The payoff compounds with cluster count. One person can kubectl three clusters by hand. Nobody hand-drives three hundred. When the change is “bump this add-on everywhere” or “this cluster must never run a privileged pod,” you want to express it once in Git and let N controllers converge — which is also how policy-as-code ships, the subject of Part 6.7

What it assumes underneath. The controller needs real access to each cluster. On EKS that means access entries for its RBAC identity — not the deprecated aws-auth ConfigMap6 — and EKS Pod Identity is the recommended way to grant the controller and its add-ons AWS permissions across the fleet without a per-cluster OIDC provider.5 Establish those first (Part 10 goes deep); the GitOps layer sits on top of them.

The default I reach for — Argo CD hub-and-spoke with ApplicationSets

For most fleets, reach for Argo CD in a hub-and-spoke topology. One “hub” cluster runs the Argo CD control plane; every other cluster is a spoke it manages. The reward is a single pane of glass over the whole fleet: one UI, one place to see sync status and health for every app on every cluster (Figure 1).1

Figure 1 · Argo CD hub-and-spoke Argo CD hub-and-spoke: one control plane fans out to the fleetArgo control planemember clusterGit desired stateFleet Git repodesired stateHUB CLUSTERArgo CDsingle-pane UIApplicationSetcluster generatorone control plane for the whole fleetMEMBER CLUSTERS (N)prod-eu-1 clusterprod-us-1 clusterdev-1 cluster+ N more, same policyreadsgenerate + sync
One control plane, one UI: an ApplicationSet cluster generator reads the fleet repo and fans the golden-path stack out to every matching cluster — the single pane is the payoff, and the hub is the risk.

The construct that makes this scale is the ApplicationSet. Instead of hand-writing one Argo Application per app per cluster — the N×M problem that sinks naive setups — you write a single ApplicationSet with a generator that templates those Applications for you.2

The cluster generator is the fleet workhorse. It enumerates the clusters Argo knows about, filters them by label selector, and stamps out the right Applications for each match. “Deploy the logging stack to every cluster tagged env=prod” becomes a few lines of YAML, and a brand-new prod cluster inherits it the moment it’s registered.

That’s why it’s the default: standardization and visibility. A central platform team defines the golden path once and the fleet inherits it; a reviewer sees drift and health fleet-wide without hopping between clusters. It is the golden path this series keeps pointing back to.

Where the single pane starts to hurt — the app ceiling and the blast radius

Be honest about the two costs, because they decide whether Argo scales to your fleet.

The costHow you size around it
The UI has a soft ceiling. Practitioners consistently report the Argo CD UI and controller getting sluggish somewhere past roughly 3,000–5,000 applications on a single instance — a community/practitioner figure, not an AWS or project guarantee, and the real number depends on app size, sync frequency, and how you’ve sized the hub. The fix is architectural: shard across multiple application controllers or Argo instances, or split the fleet. The point is only that “one hub for everything” is not infinite.
The hub is a blast radius. Centralizing control also centralizes failure. If the hub cluster or the Argo control plane is down, you can’t push changes to any spoke. The spokes keep running what they already have — workloads don’t stop — but your ability to roll out or roll back fleet-wide is gone until the hub is back. Treat the hub as production infrastructure: run it HA, back up the Argo state, and rehearse recovery. The central control plane that hurts is the one you didn’t plan to lose.

Neither is a reason to avoid Argo. They’re reasons to size the hub deliberately and to know the threshold at which you shard — and, for some fleets, reasons to pull instead of push.

When to pull instead of push — Flux, one controller per cluster

The alternative flips the topology. With Flux there is no hub. Each cluster runs its own set of Flux controllers, and each independently pulls the same fleet Git repo and reconciles itself (Figure 2).3

Figure 2 · Flux distributed pull Flux distributed: every cluster pulls the repo on its ownGit desired statereconcilingcontroller downmember clusterFleet Git repodesired stateprod-eu-1 clusterFlux controller · reconcilingprod-us-1 clusterFlux controller · reconcilingdev-1 clusterFlux controller · DOWNindependent pullstalledOne dead controller stalls its own cluster — A and B keep reconciling.
No hub, no single pane: each cluster pulls the same repo on its own, so a dead controller stalls only its cluster while the rest keep reconciling — resilience in exchange for the fleet-wide console.

What you gain is resilience and reach. Because every cluster is self-contained, there’s no central control plane to lose — a failed controller stalls its own cluster and nothing else. It scales with each cluster’s own API server rather than one shared hub, and it’s the natural fit for edge, air-gapped, or intermittently-connected clusters, which only need to reach Git, not a live central service.

What you give up is the single pane of glass. There’s no built-in fleet-wide UI showing every app on every cluster; that view is yours to assemble from aggregated dashboards and rolled-up per-cluster status. For teams that value per-cluster autonomy, that’s a fair trade. For teams that adopted GitOps precisely for the fleet-wide console, it’s the whole reason they don’t pick Flux.

The rule of thumb: Argo centralizes for visibility; Flux distributes for resilience. Same Git-as-source-of-truth foundation, opposite answers to the question “where does the controller live?”

How the fleet repo learns each cluster — the GitOps bridge and the cluster secret

Here is the join that makes fleet GitOps actually work, and it’s the direct handoff from Part 3. Your clusters are provisioned by IaC — AWS now recommends a stable core Terraform module for the cluster itself, with EKS Blueprints patterns for bootstrapping add-ons (and explicitly not using Blueprints to provision the core cluster in production).4 But the GitOps layer needs facts that only exist after the cluster and its IAM are built: the cluster’s endpoint, its labels, the IAM role ARNs its add-ons should assume.

The GitOps bridge is the pattern that carries those facts across. IaC writes the generated values — role ARNs and the like — into the Argo CD cluster secret for each registered cluster.4 ApplicationSets and Helm charts then read them as template parameters. So the same env=prod, region=eu-west-1, or roleArn values Terraform produced flow straight into the cluster generator’s selectors and into the manifests it renders.

The practical effect: registering a cluster is what enrolls it. Terraform stands up cluster number 141, writes its secret with the right labels, and on the next reconcile the ApplicationSet’s cluster generator picks it up and deploys the whole golden-path stack — no human wiring per cluster. That’s the mechanism behind “one change, N clusters,” and it’s why the bridge, not the UI, is the part that earns its keep at scale.

How you roll one change to hundreds of clusters — repo layout, generators, self-heal

With the bridge in place, rolling one change to the fleet becomes a repo-structure-and-generator question.

Structure the repo so the fan-out is a query, not a copy. A common shape is base manifests (Kustomize bases or Helm charts) per add-on and app, plus thin per-cluster or per-environment overlays that carry only the differences. The ApplicationSet generator selects clusters by label; the overlay supplies the cluster-specific values the bridge exported. Adding a cluster adds a secret, not a folder of duplicated YAML.

Let the controller enforce, not just deploy. Turn on automated sync with self-heal and prune. Self-heal means if someone hand-edits a live resource, the controller reverts it back to Git — drift is corrected, not merely surfaced. Prune means resources removed from Git are removed from the cluster, so retiring an add-on everywhere is a pull request, not a fleet-wide cleanup. This is the line between GitOps as documentation and GitOps as control.

A staged rollout falls out of the same machinery: point the generator at env=dev first, promote by label to env=staging, then env=prod — the same manifest, gated only by which clusters match at each step. Add-ons and workloads both ride this path; the generator doesn’t care whether it’s shipping the logging DaemonSet or a product service.

Which model for which workload — and why the answer is often both

So which do you pick? Default to Argo CD hub-and-spoke with ApplicationSets. It’s the right call when you want fleet standardization and a single pane of glass, and when a central platform team owns rollout — which describes most fleets, and the golden path this series recommends (Figure 3).

Figure 3 · Which model, and the hybrid Which GitOps model for which workloadREACH FOR ARGO CD HUB-AND-SPOKEStandardize add-ons + apps across the fleetYou want one pane of glass over N clustersA central team owns fleet-wide rolloutWatch: UI drags past ~3-5K apps (practitioner)Watch: central control plane = blast radiusREACH FOR FLUX DISTRIBUTEDEdge / air-gapped / intermittent linksPer-cluster resilience matters mostYou want no central blast radiusWatch: no built-in single pane of glassWatch: fleet view is your own to buildTHE HYBRID — RUN BOTHFlux for infra & add-ons · Argo CD for apps — when both pulls apply
The decision: Argo hub-and-spoke by default for standardization and one pane; Flux for edge, resilience, or no central blast radius; the hybrid runs both — Flux for infra, Argo for apps.

Choose Flux when the costs above bite: edge or air-gapped clusters that can’t depend on a live central hub, a hard requirement for per-cluster resilience, or an explicit decision to avoid any central blast radius. You trade the console for autonomy.

And often, run both. The common hybrid is Flux for infrastructure and add-ons, Argo CD for applications — infra reconciled resiliently per-cluster by the component that’s happy without a UI, apps managed through the pane of glass the app teams actually live in. It’s not a hedge; it’s matching each layer to the model that fits it.

Whichever you pick, the foundation is identical: Git is the only way in, the controller reconciles continuously with self-heal, and the bridge enrolls each new cluster automatically. Get that right and the fleet stays identical by construction. Part 5 builds the golden-path developer platform on top of this layer; Part 6 ships policy-as-code — Kyverno or OPA/Gatekeeper — through the very same GitOps pipeline.7

Sources

  1. Argo CD — Declarative GitOps CD for Kubernetes: hub-and-spoke management of many clusters, single-pane UI, per-app sync status and health. argo-cd.readthedocs.io
  2. Argo CD — ApplicationSet controller and the cluster generator: template Applications across clusters by label selector to solve the N×M problem at fleet scale. argo-cd.readthedocs.io/.../applicationset
  3. Flux — the GitOps toolkit: pull-based reconciliation with controllers running in each cluster, one instance per cluster pointing at a fleet repo. fluxcd.io
  4. AWS — EKS Blueprints and the GitOps bridge: a stable core module for the cluster plus Blueprints patterns for add-ons; IaC-generated metadata (IAM role ARNs, etc.) is stored in the Argo CD cluster secret for ApplicationSets/Helm to consume. aws-ia.github.io/terraform-aws-eks-blueprints
  5. AWS — EKS Pod Identity: the recommended default over IRSA for EKS-in-cloud — no per-cluster OIDC provider, reusable IAM roles across clusters. docs.aws.amazon.com/.../pod-identities.html
  6. AWS — EKS access entries: replace the deprecated aws-auth ConfigMap as the way to grant cluster access to IAM principals. docs.aws.amazon.com/.../access-entries.html
  7. Kyverno — policy-as-code for Kubernetes (baseline enforcement, managed via GitOps; OPA/Gatekeeper is the Rego alternative). kyverno.io