In GitOps, what is the app-of-apps (root application) composition pattern, and what does it change about how a new component reaches a cluster?
answer
- registrations become manifests too
- one root, many children
- the inventory is a reviewable file
- children keep their own sync and health
- deleting the root can cascade
basics
~10 sApp-of-apps is a root GitOps definition whose content is more GitOps definitions: the controller reconciles the root, the root creates the children, and the children deploy the workloads. Adding a component becomes one commit.
solid answer
~50 sInstead of registering twenty applications with the GitOps controller one by one, you register a single root whose managed manifests are themselves application definitions. The controller reconciles the root, the root's children appear, and each child reconciles its own component from its own path. The practical effect is that the list of what a cluster runs becomes a reviewable file in Git rather than a set of objects someone created by hand, so onboarding a component is a pull request adding one entry and offboarding is deleting it. You keep per-component granularity — each child has its own source path, its own sync state and its own health — while gaining one bootstrap handle for the whole platform. The costs are indirection (two hops from repo to workload), a deletion that can cascade to every child, and the fact that nesting still gives you no ordering guarantee between children.
code
yaml · 21 lines# platform/apps/ — one child definition per component, all rendered by the root.
# Tool-agnostic sketch of what a child binds together.
components:
- name: ingress
source:
path: platform/ingress
revision: main
destination:
namespace: ingress-system
- name: monitoring
source:
path: platform/monitoring
revision: v1.4.2
destination:
namespace: observability
- name: team-a-api
source:
path: teams/a/prod
revision: main
destination:
namespace: team-ago deeper
Know the shape: one root definition whose contents are more definitions, which in turn deploy the real workloads, so adding a component is a commit rather than a manual registration.
Explain why children beat one big unit — separate source path, revision, sync state and health per component — and describe the two flavours of children, hand-written files versus generated from an inventory.
Bring the failure mode unprompted: an empty rendered child list plus pruning converges the cluster to nothing. Describe reviewing rendered output, conservative deletion on the root, and rolling root changes to one cluster first.
Own the pattern as an ownership boundary: the platform team owns the root and the platform children, product teams own only their own paths, and the depth of nesting is a deliberate call, not a mirror of the organisation chart.
## The problem it solves A GitOps controller reconciles *registered units* — each one binds a source (repo, revision, path) to a destination (cluster, namespace). A cluster running a real platform has a lot of them: ingress controller, cert manager, monitoring stack, log shipper, policy engine, secret operator, plus every product team's services. Registering thirty of those objects by hand on every cluster is exactly the manual, drifting work GitOps exists to remove — and it is invisible to review, because those objects live only in the cluster. **App-of-apps** closes the loop: make the registrations themselves declarative. You register one *root* unit whose rendered manifests are more units. The controller reconciles the root, which creates children, and each child reconciles its own component. ```text root (points at platform/apps/) ├─ ingress -> platform/ingress/ ├─ monitoring-> platform/monitoring/ ├─ policy -> platform/policy/ └─ team-a -> teams/a/prod/ ``` Nothing about the nesting is special to the controller: a child definition is just another manifest, so it obeys the same reconcile, drift and prune semantics as a Deployment would. ## What actually changes **The inventory becomes a file.** "What does this cluster run?" is answered by reading one directory in Git rather than querying the cluster. Onboarding is a pull request that adds a child; offboarding is deleting that child, which — with pruning enabled — removes the component's objects too. **Bootstrap collapses to one handle.** The bootstrap step registers exactly one thing. Everything else arrives transitively, which is what makes rebuilding a cluster or standing up cluster number fifty tractable. **Granularity survives.** This is why the pattern beats "put all the manifests in one big unit". Each child keeps its own source path, its own revision, its own sync status and its own health — so one team's broken chart shows up as one unhealthy child instead of failing the whole cluster's sync, and a component can be pinned to a different revision from its neighbours. **Ownership boundaries get a place to live.** Because children are generated from a path you control, a platform team can own the root and the platform children while product teams own only the paths their own children point at. ## Costs and sharp edges **Indirection.** Debugging is now two hops: is the child missing because the root did not render it, or because the child's own source is broken? Newcomers regularly look at the workload namespace and cannot find who created it. **Cascading deletion.** The root's children are ordinary managed objects. Delete or mis-render the root with pruning on and every child — and transitively every workload — is a candidate for removal. This is the pattern's most famous outage: a templating mistake renders an empty child list, and the controller obediently converges the cluster to empty. Mitigations are ordinary ones: protect the root's path, keep deletion behaviour conservative on the root, and require the rendered output to be reviewed rather than the template alone. **No ordering.** Nesting gives composition, not sequencing. All children become live at roughly the same time; if one component needs another to exist first, that has to be expressed explicitly rather than assumed from the tree. **Deep nesting stops paying.** Root → children is clear. Root → children → grandchildren for organisational reasons mostly adds hops; most teams keep it two levels and use a per-cluster root instead of deeper trees. ## Hand-written children vs generated children App-of-apps says nothing about *where the child manifests come from*. Two variants exist: children written out as files (explicit, greppable, one file per component — fine up to a few dozen), and children produced by a generator from an inventory (one template plus a list — the only sane option once the same set repeats across many clusters or tenants). The two combine: hand-written children for the handful of one-off platform pieces, generated children for the repeating fleet. ## Interview framing The crisp answer is: *app-of-apps makes the registration list itself a reconciled artifact*. Everything else — reviewable inventory, one bootstrap handle, offboarding by deletion — follows from that sentence, and so does the main risk, because a list that the controller manages is a list the controller can also empty.
- Why not simply put every component's manifests into one large unit instead of a root with children?You would lose per-component granularity. One unit means one sync status, one health verdict, one revision and one failure blast radius, so a single bad chart blocks or fails the whole cluster's reconcile. Children keep separate sources, statuses and revisions, letting one component be pinned or fail independently while the rest converge, and letting different teams own different paths.
- What is the worst realistic failure of an app-of-apps setup, and how do you reduce it?The root renders an empty or truncated child list — a bad template value, a wrong path — and with pruning enabled the controller removes every child and, transitively, every workload. Reduce it by reviewing rendered output rather than the template alone, keeping delete behaviour conservative on the root, restricting who can merge to the root's path, and rolling root changes to one cluster first.
- Does the root/child nesting give you ordering between components?No. Composition is not sequencing: children become live together, and a child that depends on another being ready will simply fail its first attempts. Ordering has to be stated explicitly — an explicit dependency edge between units, or ordering hints inside one unit — otherwise you are relying on retries to eventually converge.
saying these in an interview costs you the question
- Says app-of-apps is just one big folder of manifests
- Assumes children deploy in the order they appear in the file
- Thinks children lose their individual sync and health status
- Ignores that deleting the root can prune every workload
- Nests four levels deep to mirror the org chart