Repository Structure and Multi-Tenancy
The four documented Flux repository layouts and their tradeoffs, plus the tenant isolation model built on service account impersonation and cross-namespace reference lockdown.
Overview
When layout becomes the problem
Once Flux runs on more than one cluster, the interesting problems stop being about controllers and start being about directory layout. The Flux docs answer three questions specifically enough that an interviewer can tell in thirty seconds whether you have run a fleet or only a demo cluster.
Four repository patterns
The repository-structure guide documents four patterns. They are not ranked, but the monorepo is the one every other Flux guide builds on, and the one to draw from memory.
- Monorepo — clusters/ plus infrastructure/ plus apps/
- Repo per environment — Separate staging and production repositories
- Repo per team — Platform-admin repo plus one repo per dev team
- Repo per app — Manifests beside source code
> [!IMPORTANT] clusters// contains no workload manifests at all, only Flux Kustomization objects pointing at the other directories. Adding a cluster means adding a directory of pointers, not copying manifests.
Ordering with dependsOn
Kubernetes has no built-in notion of "install the CRDs first". spec.dependsOn holds a Kustomization back until its dependencies report Ready.
> [!WARNING] Getting this wrong produces the most common Flux failure report: an app Kustomization stuck on "no matches for kind Certificate" because cert-manager's CRDs had not landed yet.
One base across many clusters
- Structural — Kustomize overlay — Extra sidecar in production, different Ingress class
- Scalar — spec.postBuild.substitute and substituteFrom, from inline values, ConfigMaps or Secrets, after the Kustomize build — Region, environment name, base domain
> [!NOTE] The cost to name in an interview: the YAML in Git is no longer byte-identical to what lands in the cluster.
Multi-tenancy through Kubernetes
Flux does not have a project object that allowlists repositories and destinations, which is where it diverges most from Argo CD. A tenant gets namespaces, a ServiceAccount and RoleBindings; their Kustomization sets spec.serviceAccountName so kustomize-controller impersonates that account. The API server, not Flux, decides whether the apply is allowed.
- --no-cross-namespace-refs — A tenant consuming another tenant's sources
- --no-remote-bases — Kustomize pulling manifests from arbitrary URLs outside the tracked source
- --default-service-account — A blank serviceAccountName falling back to the controller's privileges
Where the line sits
Flux gives you soft multi-tenancy: namespace isolation with a shared control plane and shared nodes.
- Kernel isolation — A tenant with privileged pod rights can escape to the node
- etcd or API server partitioning — No protection from a noisy neighbor
- Hard multi-tenancy — Still means separate clusters
> [!TIP] Knowing where that line sits, and saying so, is the answer that lands.
Key concepts
- Cluster entry-point directory
- clusters// — the path passed to flux bootstrap --path. It contains only Flux Kustomization and source objects that reference infrastructure/ and apps/. Adding a cluster means adding one directory of pointers, not copying manifests. This indirection is what makes the monorepo scale to a fleet.
- spec.dependsOn
- A list of Kustomizations (or HelmReleases) that must report a Ready condition of True before this one is applied. Used to sequence infrastructure controllers, then infrastructure configuration, then apps. The optional readyExpr field takes a CEL expression when the default Ready check is not precise enough, for example gating on a matching observed revision.
- postBuild substitution
- spec.postBuild.substitute (inline key/value) and spec.postBuild.substituteFrom (ConfigMap or Secret, with optional: true supported). Runs after kustomize build and replaces shell-style placeholders with an optional default form. Inline values take precedence over substituteFrom sources. substituteStrategy: Always applies defaults even for undefined variables.
- Tenant impersonation
- spec.serviceAccountName on a Kustomization or HelmRelease. The controller assumes that ServiceAccount identity for both reading sources and applying manifests, so the API server evaluates the tenant RBAC rather than the controller cluster-admin binding. The account must exist in the same namespace as the Flux object.
- --no-cross-namespace-refs
- A controller flag (helm-controller, kustomize-controller, notification-controller, image-reflector-controller, image-automation-controller) that rejects sourceRef.namespace and any other cross-namespace reference to a Flux object. Stops a tenant from consuming another tenant's GitRepository or subscribing to their events.
- --default-service-account
- Sets the ServiceAccount used when a tenant Kustomization or HelmRelease omits spec.serviceAccountName. Pointing it at "default" — which carries no permissions — turns impersonation from an opt-in into a requirement: a tenant who forgets the field deploys nothing.
- --no-remote-bases
- A kustomize-controller flag that rejects Kustomize remote bases (URLs in the resources list). Without it, a manifest inside your repository can pull arbitrary YAML from the internet at reconcile time, which breaks source hermiticity and means the Git history no longer describes cluster state.
- Soft vs hard multi-tenancy
- Soft: shared control plane and nodes, isolation by namespace, RBAC, network policy, and resource quota. What Flux delivers. Hard: separate clusters or separate control planes. Kubernetes RBAC alone cannot isolate node-level escapes, kernel exploits, noisy-neighbour pressure on the API server or etcd, or CRDs and webhooks, which are cluster-scoped by definition.
When to use it
- Starting a new GitOps repository — begin with the monorepo layout (clusters/, infrastructure/, apps/) and only split when an access-control or review-scope problem forces it
- Onboarding a fleet where clusters differ only in region, domain, or replica count — one base plus per-cluster postBuild substitution rather than per-cluster manifest copies
- Splitting a platform team from product teams — repo per team, with the platform repo owning tenant namespaces, RBAC, and the Kustomization that scopes each team
- Handing deployment autonomy to product teams on a shared cluster — namespace-per-tenant with spec.serviceAccountName impersonation and the lockdown flags enabled
- Any environment where a compliance reviewer must be able to answer "what can team X change?" by reading Role and RoleBinding objects rather than a controller config
Interview questions
- Draw the monorepo layout Flux recommends and explain what each directory is for.
- When would you move off the monorepo, and what do you give up?
- How do you run twenty clusters from one repository without duplicating manifests?
- Explain Flux multi-tenancy end to end. What stops tenant A from touching tenant B?
- How does Flux multi-tenancy compare to Argo CD AppProject, and when does each fit better?
Sign up free to read the full topic
More DevOps topics
- Python for Build Automation
- Qt Deployment and Packaging
- Qt for Build Engineers
- Release Engineering
- Release Manifests & Last-Known-Good
- Repositories and OCI Distribution
- Running AI/ML Workloads on Kubernetes
- Runtime Security
- Sanitizers and Memory Debugging
- SAST, DAST, SCA & IAST
- Secrets Management in GitOps
- Securing Repositories and CI/CD — GitHub, GitHub Enterprise Server, GitLab and Azure DevOps
Start free on Cariara · Coding problems · System design studio