Kubernetes
14 min readMay 5, 2026

Argo CD ApplicationSet: Multi-Cluster Deployment and Generator Patterns

A single Argo CD Application deploys one application to one cluster. ApplicationSet generates multiple Applications from a template — one per cluster, one per Git directory, one per team. It's the mechanism that turns Argo CD from a single-app deployment tool into a fleet deployment platform.

AJ
Ajeet Yadav
Platform & Cloud Engineer
Argo CD ApplicationSet: Multi-Cluster Deployment and Generator Patterns

Argo CD's Application resource deploys one Helm chart or Kustomize directory to one target cluster. This works for a handful of applications, but doesn't scale to platform engineering use cases: deploying the same application to 20 clusters, deploying every subdirectory in a monorepo as a separate application, or onboarding a new team by adding a directory to Git and having Argo CD automatically deploy everything in it.

ApplicationSet solves this by templating Application creation. You define one ApplicationSet with a generator (which produces a list of parameter sets) and a template (which defines one Application per parameter set). The ApplicationSet controller creates, updates, and deletes Application resources automatically as the generator's output changes.


How ApplicationSet Works

ApplicationSet → Generator → [param set 1, param set 2, ...] → Application per param set

The generator queries a data source (cluster registry, Git repository, list) and produces a list of parameter sets. Each parameter set is used to render the template, producing one Application object. When the data source changes (a new cluster is added, a new directory appears in Git), the ApplicationSet controller reconciles the Application list.

ApplicationSet is bundled with Argo CD since v2.3 and enabled by default — no separate installation needed. Before v2.3, ApplicationSet required a separate kubectl apply of the applicationset-controller manifest.


List Generator

The simplest generator: an explicit list of environments or clusters.

yaml
1apiVersion: argoproj.io/v1alpha1
2kind: ApplicationSet
3metadata:
4  name: my-app
5  namespace: argocd
6spec:
7  generators:
8    - list:
9        elements:
10          - cluster: staging
11            url: https://k8s-staging.example.com
12            values:
13              replicas: "1"
14              resourceSize: small
15          - cluster: production
16            url: https://k8s-prod.example.com
17            values:
18              replicas: "3"
19              resourceSize: large
20  template:
21    metadata:
22      name: "my-app-{{cluster}}"
23    spec:
24      project: default
25      source:
26        repoURL: https://github.com/my-org/fleet-apps
27        targetRevision: HEAD
28        path: apps/my-app
29        helm:
30          values: |
31            replicaCount: {{values.replicas}}
32            resources:
33              preset: {{values.resourceSize}}
34      destination:
35        server: "{{url}}"
36        namespace: my-app
37      syncPolicy:
38        automated:
39          prune: true
40          selfHeal: true
41        syncOptions:
42          - CreateNamespace=true

The List generator is appropriate when the set of targets is small and changes rarely. For dynamic fleets, the Cluster generator is better.


Cluster Generator

Generates one Application per cluster registered in Argo CD's cluster store. As you register new clusters, ApplicationSet automatically deploys to them:

yaml
1spec:
2  generators:
3    - clusters:
4        selector:
5          matchLabels:
6            environment: production   # Only production clusters
7        values:
8          region: "{{metadata.annotations.region}}"   # Use cluster annotations
9  template:
10    metadata:
11      name: "monitoring-{{name}}"   # {{name}} = cluster name in Argo CD
12    spec:
13      source:
14        repoURL: https://github.com/my-org/fleet
15        path: platform/monitoring
16        targetRevision: HEAD
17      destination:
18        server: "{{server}}"         # {{server}} = cluster API endpoint
19        namespace: monitoring
20      syncPolicy:
21        automated:
22          prune: true
23          selfHeal: true

Register a new cluster in Argo CD, apply the environment=production label, and within seconds the monitoring ApplicationSet deploys the monitoring stack to it. No manual Application creation.

Cluster annotations are accessible as {{metadata.annotations.KEY}} and labels as {{metadata.labels.KEY}}, enabling cluster-specific values without a separate values file per cluster.


Git Generator: Directory Mode

Generates one Application per subdirectory in a Git repository. This is the canonical pattern for GitOps application onboarding — add a directory, get an Application.

yaml
1spec:
2  generators:
3    - git:
4        repoURL: https://github.com/my-org/fleet
5        revision: HEAD
6        directories:
7          - path: apps/*          # Match all direct subdirs of apps/
8          - path: apps/excluded   # Exclude a specific directory
9            exclude: true
10  template:
11    metadata:
12      name: "{{path.basename}}"    # {{path.basename}} = directory name
13    spec:
14      source:
15        repoURL: https://github.com/my-org/fleet
16        targetRevision: HEAD
17        path: "{{path}}"           # {{path}} = relative path from root
18      destination:
19        server: https://kubernetes.default.svc
20        namespace: "{{path.basename}}"

Add a directory apps/new-service/ containing Kubernetes manifests or a Kustomize overlay, and Argo CD automatically creates and syncs the new-service Application. Delete the directory, and Argo CD deletes the Application (and optionally prunes the resources).


Git Generator: Files Mode

Generates Applications from JSON/YAML configuration files in a repo:

yaml
1spec:
2  generators:
3    - git:
4        repoURL: https://github.com/my-org/fleet
5        revision: HEAD
6        files:
7          - path: "clusters/**/config.json"   # Match all config.json files recursively
8  template:
9    metadata:
10      name: "{{cluster.name}}-{{app.name}}"
11    spec:
12      source:
13        path: "{{app.path}}"
14        helm:
15          values: |
16            image:
17              tag: "{{app.version}}"
18      destination:
19        server: "{{cluster.address}}"
20        namespace: "{{app.namespace}}"

Each config.json can contain structured data:

json
1{
2  "cluster": {
3    "name": "us-east-1-prod",
4    "address": "https://k8s-prod-us.example.com"
5  },
6  "app": {
7    "name": "payments-api",
8    "path": "apps/payments-api",
9    "namespace": "production",
10    "version": "v2.1.4"
11  }
12}

This pattern works well for teams that want explicit, reviewed configuration files for each deployment rather than implicit directory-scanning.


Matrix Generator

Generates the Cartesian product of two generators. Deploy every application to every cluster without listing all combinations:

yaml
1spec:
2  generators:
3    - matrix:
4        generators:
5          # Inner generator 1: all production clusters
6          - clusters:
7              selector:
8                matchLabels:
9                  environment: production
10          # Inner generator 2: all applications from Git
11          - git:
12              repoURL: https://github.com/my-org/fleet
13              revision: HEAD
14              directories:
15                - path: platform-apps/*
16  template:
17    metadata:
18      name: "{{path.basename}}-{{name}}"  # app-cluster
19    spec:
20      source:
21        path: "{{path}}"
22      destination:
23        server: "{{server}}"
24        namespace: "{{path.basename}}"

3 clusters × 5 applications = 15 Applications, all from one ApplicationSet. Adding a new cluster or a new app directory automatically adds all the missing combinations.


SCM Provider Generator

Discovers repositories from GitHub, GitLab, or Bitbucket organisations and creates Applications for each:

yaml
1spec:
2  generators:
3    - scmProvider:
4        github:
5          organization: my-org
6          appSecretName: github-token   # PAT with repo read access
7          allBranches: false            # Only default branch
8        filters:
9          - repositoryMatch: ".*-service$"   # Only repos ending in -service
10          - labelMatch: "deploy-to-argocd"   # Only repos with this topic
11  template:
12    metadata:
13      name: "{{repository}}"
14    spec:
15      source:
16        repoURL: "{{url}}"
17        targetRevision: "{{branch}}"
18        path: deploy/    # Assume each repo has a deploy/ directory
19      destination:
20        server: https://kubernetes.default.svc
21        namespace: "{{repository}}"

New repositories matching the filter are automatically added; deleted repositories trigger Application deletion. This pattern works well for microservices organisations where teams own their own repositories.


Sync Waves and Hooks

Sync waves control the order resources are applied during a sync. Lower wave numbers are applied first; Argo CD waits for all resources in a wave to become healthy before proceeding to the next:

yaml
1# Applied first (wave -1): CRDs and namespaces
2apiVersion: v1
3kind: Namespace
4metadata:
5  name: payments
6  annotations:
7    argocd.argoproj.io/sync-wave: "-1"
8---
9# Applied second (wave 0, default): main application resources
10apiVersion: apps/v1
11kind: Deployment
12metadata:
13  name: payments-api
14  annotations:
15    argocd.argoproj.io/sync-wave: "0"
16---
17# Applied last (wave 1): resources that depend on the main deployment
18apiVersion: batch/v1
19kind: Job
20metadata:
21  name: post-deploy-migration
22  annotations:
23    argocd.argoproj.io/sync-wave: "1"

Note: Sync-wave annotations on Application objects only control resource ordering within a single Application's sync — they do not cause the ApplicationSet controller to deploy Applications sequentially across clusters. For true progressive multi-cluster rollouts (deploy to staging, wait, then deploy to prod), use ApplicationSet spec.strategy.rollingSync with the ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_PROGRESSIVE_SYNCS=true feature gate (beta in ArgoCD v2.10+).

Sync hooks run at specific sync lifecycle phases:

yaml
1apiVersion: batch/v1
2kind: Job
3metadata:
4  name: db-migration
5  annotations:
6    argocd.argoproj.io/hook: PreSync        # Run before the sync
7    argocd.argoproj.io/hook-delete-policy: HookSucceeded   # Delete after success
8spec:
9  template:
10    spec:
11      containers:
12        - name: migrate
13          image: my-app:latest
14          command: ["./migrate", "--apply"]
15      restartPolicy: Never

Hook phases: PreSync (before sync), Sync (during sync), PostSync (after all resources are healthy), SyncFail (on sync failure — for cleanup).


Progressive Multi-Cluster Delivery

Deploy to staging first, wait for health checks, then deploy to production — across an entire cluster fleet:

yaml
1spec:
2  generators:
3    - list:
4        elements:
5          - cluster: staging
6            wave: "0"
7          - cluster: canary-prod
8            wave: "1"    # Canary production cluster — deploy after staging
9          - cluster: production-us-east-1
10            wave: "2"    # Full production — deploy after canary is healthy
11          - cluster: production-eu-west-1
12            wave: "2"
13  template:
14    metadata:
15      name: "my-app-{{cluster}}"
16      annotations:
17        argocd.argoproj.io/sync-wave: "{{wave}}"
18    spec:
19      ...

Combined with Argo CD's sync waves and health checks, this creates a multi-cluster progressive delivery pipeline entirely within GitOps.


Frequently Asked Questions

How does ApplicationSet handle cluster failures?

If a target cluster is unreachable, its Application shows as Unknown or Degraded — ApplicationSet continues managing other cluster Applications normally. The ApplicationSet itself doesn't fail; only the affected Application shows the error. With syncPolicy.automated, Argo CD retries syncs with exponential backoff.

Can I use ApplicationSet with Helm values per cluster?

Yes. Using the Cluster generator, Argo CD cluster labels and annotations are accessible as template variables. For cluster-specific Helm values, combine with the Git Files generator — each cluster has a values.yaml file that the Application references:

yaml
helm:
  valueFiles:
    - "clusters/{{name}}/values.yaml"    # Per-cluster values file in repo
    - values.yaml                         # Shared default values

What's the difference between ApplicationSet and app-of-apps?

App-of-apps uses a root Application that points to a directory of Application YAML files — you write and maintain each Application manifest. ApplicationSet generates Application manifests dynamically from a template and generator — you don't write or maintain individual Application manifests. For large fleets, ApplicationSet is significantly less operational overhead. For small numbers of applications with very different configurations, app-of-apps is simpler. They can also be combined: an ApplicationSet deploys a root app-of-apps Application per cluster, which in turn manages cluster-specific applications.


For Argo CD production setup including app-of-apps, RBAC, and cluster registration, see GitOps with Argo CD: Production Setup Guide. For the Flux CD alternative to ApplicationSet for multi-cluster GitOps, see Flux CD: GitOps for Kubernetes. For the progressive delivery layer on top of ApplicationSet, see Argo Rollouts: Canary and Blue-Green Deployments.

Building a multi-cluster GitOps platform with Argo CD? Talk to us at Coding Protocols — we help platform teams design ApplicationSet patterns that scale from 5 clusters to 500.

Related Topics

Argo CD
ApplicationSet
GitOps
Multi-Cluster
Kubernetes
CI/CD
Platform Engineering
CNCF

Read Next