
Deploying the same app to five clusters with plain ArgoCD means five Application manifests, each one a near-copy of the others with a different cluster endpoint. The sixth cluster means a sixth copy, hand-edited from the fifth, and the inevitable typo that only shows up in production. ApplicationSets replace that with one template and a generator that produces the per-cluster Application objects for you — add a cluster, and the app follows automatically.
What an ApplicationSet actually is
An ApplicationSet is a controller, not a smarter Application. It watches one or more generators (a list of clusters, a Git repository's directories, a plain list, or combinations of these), and for every item a generator produces, it renders your template block into a real Application object and lets ArgoCD sync it normally. Delete an item from the generator's source — remove a cluster, delete a directory — and the ApplicationSet deletes the corresponding Application too.
Step 1: Register clusters with labels the generator can select on
Add each cluster to ArgoCD as usual:
argocd cluster add my-cluster-context --name prod-us-east
ArgoCD stores every registered cluster as a Secret in the argocd namespace. Label it — this label is what the Cluster generator selects on:
kubectl label secret -n argocd <cluster-secret-name> env=production
Repeat for every cluster you want this ApplicationSet to target. A staging cluster gets env=staging, and so on — the label is the only thing that decides which clusters are in scope.
Step 2: A Cluster generator, one app per matching cluster
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: guestbook
namespace: argocd
spec:
goTemplate: true
goTemplateOptions: ["missingkey=error"]
generators:
- clusters:
selector:
matchLabels:
env: production
template:
metadata:
name: '{{.name}}-guestbook'
spec:
project: default
source:
repoURL: https://github.com/myorg/guestbook.git
targetRevision: main
path: manifests
destination:
server: '{{.server}}'
namespace: guestbook
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
{{.name}} and {{.server}} come from the matched cluster secret's own name and API server URL — goTemplate: true enables the {{.field}} syntax (older ApplicationSets use bare {{field}}, which is a legacy template mode still supported but not what you want for new policies). Add a sixth cluster with the env=production label, and a sixth Application appears with no further changes here.
Step 3: Different overlays per cluster with a Matrix generator
Real multi-cluster rollouts rarely use identical manifests everywhere — resource sizing, replica counts, and region-specific config usually differ. A Matrix generator combines the Cluster generator with a Git directory generator, producing one Application per combination of cluster × overlay directory:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: guestbook-matrix
namespace: argocd
spec:
goTemplate: true
generators:
- matrix:
generators:
- clusters:
selector:
matchLabels:
env: production
- git:
repoURL: https://github.com/myorg/guestbook.git
revision: main
directories:
- path: "overlays/*"
template:
metadata:
name: '{{.name}}-{{.path.basename}}'
spec:
project: default
source:
repoURL: https://github.com/myorg/guestbook.git
targetRevision: main
path: '{{.path.path}}'
destination:
server: '{{.server}}'
namespace: guestbook
syncPolicy:
automated:
prune: true
selfHeal: true
With a repo layout like overlays/us-east/ and overlays/eu-west/, the Git directory generator exposes {{.path.basename}} (the directory's own name) and {{.path.path}} (the full repo-relative path) to the template — so each cluster gets the overlay matching its own directory, and the generated Application name stays unique across the whole matrix.
Step 4: Drift handling
selfHeal: true is what actually enforces drift correction — without it, automated sync only reacts to new commits, and a manual kubectl edit against a live cluster sits uncorrected until the next Git change triggers a reconcile. With selfHeal: true, ArgoCD's controller reverts an out-of-band change back to match Git on its regular reconcile loop, typically within a few minutes.
Drift handling at the ApplicationSet level is a different problem: what happens to deployed resources when a generator entry disappears — a cluster is unlabeled, or an overlay directory is deleted. By default, ArgoCD deletes the generated Application, and because of prune: true, deletes its live resources with it. If that's not what you want — say, decommissioning a cluster from the generator shouldn't immediately tear down a namespace someone might still need — set:
spec:
syncPolicy:
preserveResourcesOnDeletion: true
This is an ApplicationSet-level syncPolicy field (distinct from the per-Application syncPolicy inside template.spec) — it stops the cascade at the Application object, leaving the actual cluster resources in place for a human to clean up deliberately.
Troubleshooting
- Duplicate
Applicationnames. If two generator entries can produce the same name (e.g. two clusters both just named "default" in different contexts), the ApplicationSet controller will collide. Always template the name from a value that's guaranteed unique per combination — cluster name plus overlay path, as in the Matrix example. missingkey=errorfailing a render. WithgoTemplateOptions: ["missingkey=error"], referencing a template field a generator didn't actually produce (a typo in{{.path.basename}}, for instance) fails loudly instead of silently rendering an empty string — intentional, and worth keeping on.- Cluster secret missing the expected label. The Cluster generator's
selectoronly ever sees labels on the secret itself, not on the cluster's kubeconfig or context name — if an app isn't appearing for a cluster you expect, checkkubectl get secret -n argocd --show-labelsfirst.
Where to go next
Once ApplicationSets are generating correctly, add a Pull Request generator for ephemeral preview environments per PR, or a SCM Provider generator to pick up every repository in an org automatically instead of listing them by hand — the same template-plus-generator pattern extends to both.

Experienced DevOps Engineer proficient in Python, Docker, Kubernetes, CI/CD, and cloud technologies. Specialized in automating processes and building scalable infrastructure for seamless development and deployment.


