Kubernetes
10 min readJune 20, 2026Updated August 19, 2026

App-of-Apps vs ApplicationSet in Argo CD: Which Pattern to Use and When

AJ
Ajeet Yadav
Platform & Cloud Engineer
App-of-Apps vs ApplicationSet in Argo CD: Which Pattern to Use and When

Quick answer

Both App-of-Apps and ApplicationSet let one Argo CD Application manage many others — but they solve different problems. Here's the real decision: App-of-Apps for an explicit, hand-curated tree; ApplicationSet for generated fleets you don't want to maintain by hand.

10 min read · Kubernetes

If you run Argo CD past a handful of Applications, you eventually hit the same question: how do you manage many Applications without hand-writing and hand-syncing each one? Argo CD gives you two answers — the App-of-Apps pattern and the ApplicationSet controller — and they are constantly compared as if they were alternatives to the same problem. They're not.

App-of-Apps is a convention: a single Application whose Git source contains more Application manifests. ApplicationSet is a controller that generates Application resources from a template plus a generator. The short version of the decision: use App-of-Apps when you want an explicit, hand-curated list of Applications under one root, and ApplicationSet when you want Applications produced automatically from clusters, Git directories, or a list — so you never touch them by hand.

This post walks through how each actually works, where App-of-Apps quietly becomes a liability, and the decision rule I use on real platforms.


How App-of-Apps Works

App-of-Apps is just Argo CD pointed at itself. You create one "root" Application whose source is a Git path containing more Application YAML. When the root syncs, those child Applications are created in the cluster, and each child then syncs its own workload.

yaml
1apiVersion: argoproj.io/v1alpha1
2kind: Application
3metadata:
4  name: root
5  namespace: argocd
6spec:
7  project: platform
8  source:
9    repoURL: https://github.com/acme/gitops
10    path: apps            # this directory contains more Application manifests
11    targetRevision: main
12  destination:
13    server: https://kubernetes.default.svc
14    namespace: argocd
15  syncPolicy:
16    automated:
17      prune: true
18      selfHeal: true

Inside apps/ you commit one file per child:

yaml
1# apps/cert-manager.yaml
2apiVersion: argoproj.io/v1alpha1
3kind: Application
4metadata:
5  name: cert-manager
6  namespace: argocd
7spec:
8  project: platform
9  source:
10    repoURL: https://github.com/acme/gitops
11    path: addons/cert-manager
12    targetRevision: main
13  destination:
14    server: https://kubernetes.default.svc
15    namespace: cert-manager
16  syncPolicy:
17    automated: { prune: true, selfHeal: true }

The whole thing is recursive and explicit. Every Application that exists is a file in Git that a human wrote. That is App-of-Apps' biggest strength and, at scale, its biggest weakness.

What it's good at: a curated platform bundle — the set of addons every cluster gets (cert-manager, external-secrets, ingress, monitoring). The list is small, intentional, and changes deliberately. Sync waves (argocd.argoproj.io/sync-wave) let you order installs, so CRDs land before the controllers that need them. You can read the entire topology by reading one directory.

Where it breaks down: the moment "the list" stops being hand-maintainable. Onboard a tenth cluster and you copy-paste ten files and rewrite the destination.server in each. Add a new team namespace and you author another Application by hand. The pattern has no concept of "for each X, make an Application" — so every axis of growth (clusters, teams, regions, environments) multiplies the YAML you maintain manually. You also get a slightly awkward two-step delete: pruning a child requires the root to prune, and finalizers on the child to cascade the workload deletion.


How ApplicationSet Works

ApplicationSet inverts the model. Instead of you writing each Application, you write one template and a generator that produces the parameters. The controller renders one Application per generated parameter set and keeps them reconciled — create, update, and delete — as the generator's inputs change.

yaml
1apiVersion: argoproj.io/v1alpha1
2kind: ApplicationSet
3metadata:
4  name: addons
5  namespace: argocd
6spec:
7  goTemplate: true
8  generators:
9    - clusters: {}          # one element per cluster registered in Argo CD
10  template:
11    metadata:
12      name: 'cert-manager-{{.name}}'
13    spec:
14      project: platform
15      source:
16        repoURL: https://github.com/acme/gitops
17        path: addons/cert-manager
18        targetRevision: main
19      destination:
20        server: '{{.server}}'
21        namespace: cert-manager
22      syncPolicy:
23        automated: { prune: true, selfHeal: true }

Register a new cluster in Argo CD and cert-manager appears on it automatically — no new file, no PR to an apps/ directory. That is the entire point: the fleet maintains itself from a source of truth that isn't a hand-written list.

The power is in the generators, which is where ApplicationSet earns its complexity:

  • List — a literal array of parameters. The simplest generator; basically App-of-Apps without the per-file boilerplate.
  • Cluster — one Application per cluster registered in Argo CD, with label selectors to target subsets (prod vs staging).
  • Git (directories / files) — one Application per subdirectory or per config file in a repo. This is how you do "every folder under tenants/ becomes an Application" — add a folder, get an Application.
  • Matrix / Merge — compose generators. Matrix takes the cartesian product (e.g. every addon × every cluster) to render a grid; Merge combines parameter sets by a merge key, letting a later generator override values from an earlier one.
  • Pull Request / SCM Provider — generate Applications per open PR or per repo in an org, for preview environments and org-wide rollouts.

For careful, ordered rollouts across the fleet, ApplicationSet has progressive syncs (spec.strategy.type: RollingSync), so you can update canary clusters before production ones — something App-of-Apps cannot express at all. One caveat: progressive syncs have long been an opt-in feature, enabled with the --enable-progressive-syncs controller flag (or the equivalent ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_PROGRESSIVE_SYNCS env var), so confirm it's turned on for your Argo CD version before relying on it. I covered that in depth in Argo CD ApplicationSet Progressive Syncs, and the multi-cluster mechanics in Argo CD ApplicationSet for Multi-Cluster.

Where it bites: a templating bug or a bad generator can render — or delete — Applications across your whole fleet at once. Blast radius is the price of automation. You also debug through a layer of indirection: when an Application looks wrong, you're often reading generator output and Go templates, not a static file you can diff in a PR.


Kubernetes Production Readiness Checklist

The pre-launch checks we run before calling a cluster production-ready — probes, resources, RBAC, upgrades, and backups. Plain Markdown you can commit to your repo.

Free. Instant download. You'll also get the occasional deep-dive from the newsletter — unsubscribe anytime.

App-of-Apps vs ApplicationSet: The Real Differences

DimensionApp-of-AppsApplicationSet
MechanismConvention — an Application sourcing more ApplicationsController — generates Applications from a template
Source of truthA hand-written directory of Application filesA generator (clusters, Git dirs, list, PRs, matrix)
Adding an ApplicationAuthor and commit a new fileHappens automatically when generator input changes
Scales alongManually, one file per itemAutomatically, per cluster / dir / PR
Ordered rolloutSync waves within a clusterProgressive syncs across clusters
Blast radiusLow — changes are explicit and per-fileHigh — one template change touches every render
DebuggabilityRead the directory; it's all thereInspect generator output + template rendering
Best forA curated, slow-changing platform bundleFleets that grow along an axis you don't want to hand-maintain

The cleanest way to hold the distinction: App-of-Apps is a list you maintain; ApplicationSet is a list you generate. If a human should decide each entry, App-of-Apps keeps that decision explicit and reviewable. If the entries are mechanically derived from something else — your cluster inventory, a tenants folder, open PRs — then maintaining that list by hand is just toil ApplicationSet exists to delete.


Which Should You Use?

Here is the decision rule I actually apply:

  1. One cluster, a fixed set of platform addons? Use App-of-Apps. It's explicit, trivially reviewable, and ApplicationSet would be ceremony with no payoff. Sync waves handle ordering.
  2. The same set of apps across many clusters? Use an ApplicationSet with the cluster generator. This is the canonical case ApplicationSet was built for, and where App-of-Apps' copy-paste tax hurts most.
  3. Per-tenant or per-team Applications driven by a folder structure? Use an ApplicationSet with the Git directory generator — add a folder, get an Application.
  4. Preview environments per pull request? Use an ApplicationSet with the Pull Request generator. App-of-Apps has no equivalent.
  5. A handful of curated apps that nonetheless need ApplicationSet's lifecycle (e.g. you want one place to template repeated fields)? Use the list generator — it's ApplicationSet with the explicitness of App-of-Apps.

And the combination that most mature platforms land on: both. A root App-of-Apps bootstraps the platform and includes the ApplicationSets themselves as children. The root tree stays small and human-curated (the platform's "operating system"), while the ApplicationSets fan out the parts that scale by cluster, tenant, or PR. You're not choosing a side — you're using App-of-Apps for the curated core and ApplicationSet for the generated fleet.

If you're still standing up the basics first, GitOps with Argo CD: Production Setup and the hands-on App-of-Apps pattern tutorial are the right starting points before you reach for generators.


Frequently Asked Questions

Is ApplicationSet a replacement for App-of-Apps?

No. ApplicationSet generates Applications from a template; App-of-Apps is a hand-curated tree of Applications. They overlap only for the trivial "static list" case (where ApplicationSet's list generator does the same job with less boilerplate). For curated platform bundles, App-of-Apps is still perfectly valid — and the two are routinely combined.

Is ApplicationSet still a separate install in current Argo CD?

No. The ApplicationSet controller was merged into Argo CD core and ships with it (it has since the 2.3+ era). You don't install a separate component for current versions — the ApplicationSet CRD and controller come with a standard Argo CD install.

Can I use App-of-Apps and ApplicationSet together?

Yes, and it's the common production pattern. A root App-of-Apps bootstraps the platform and includes your ApplicationSet manifests as children. The root stays small and explicit; the ApplicationSets handle everything that scales per cluster, tenant, or PR.

How do I order resource creation with each pattern?

App-of-Apps uses sync waves (argocd.argoproj.io/sync-wave) to order children and resources within a sync — useful for CRDs-before-controllers. ApplicationSet adds progressive syncs (RollingSync) to order rollouts across clusters — useful for canarying a change to staging clusters before production ones.

Why did my ApplicationSet delete a bunch of Applications?

Because the generator's input changed and the controller reconciled to match. A cluster getting unregistered, a Git directory being removed, or a label selector being edited will prune the Applications that no longer match. This is the blast-radius trade-off. The guardrail for this case is the applicationsSync policy — create-only or create-update stops the controller from deleting generated Applications when results change (the controller-level --policy flag enforces the same globally). Note that preserveResourcesOnDeletion is a different lever: it only protects resources when the ApplicationSet object itself is deleted, not when a generator's input changes. Either way, review ApplicationSet changes as carefully as you'd review an rm -rf.

Does App-of-Apps work across multiple clusters?

It can, but awkwardly — each child Application sets its own destination.server, so multi-cluster means hand-writing the cluster targeting in every file. That manual per-cluster duplication is exactly the toil the ApplicationSet cluster generator removes, which is why multi-cluster is the textbook reason to switch.


For the broader GitOps tooling decision, see Argo CD vs Flux CD: A Detailed Comparison. For scaling generated Applications across a fleet, see Argo CD ApplicationSet for Multi-Cluster and Argo CD ApplicationSet Progressive Syncs.

Designing GitOps for a multi-cluster, multi-team platform? Talk to us at Coding Protocols — we help platform teams build Argo CD topologies that stay reviewable as they scale.

Official References

Was this article helpful?

Be the first to rate this article

Related Topics

Argo CD
GitOps
ApplicationSet
App of Apps
Kubernetes
Platform Engineering
Multi-Cluster

Found this useful? Share it.

Practice this

Related tools

Read Next