ArgoCD ApplicationSets: Managing Many Apps at Once
If you're running ArgoCD across multiple environments or clusters, you've probably hit the point where maintaining individual Application CRDs becomes unsustainable. Thirty applications, each with a dev/staging/prod variant, means ninety Application manifests that are 90% identical. ApplicationSets are the solution.
The Problem
An ArgoCD Application looks like this:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: payments-service-production
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/k8s-configs
targetRevision: HEAD
path: apps/payments-service/production
destination:
server: https://prod-cluster.example.com
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
Now multiply this by 20 services and 3 environments. You're maintaining 60 near-identical files. Every time a new service gets added, someone has to create three Application manifests. Naming conventions drift. People forget to set selfHeal: true on the staging version. This is how configuration debt accumulates.
What ApplicationSets Do
An ApplicationSet is a single CRD that templates Application manifests and generates them based on a generator. The ApplicationSet controller (part of ArgoCD since 2.0, bundled since 2.3) watches ApplicationSet resources and creates/updates/deletes the corresponding Application resources automatically.
One ApplicationSet can replace sixty Application files.
The Three Generators You'll Use
List Generator
Explicitly enumerate the variants you want:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: payments-service
namespace: argocd
spec:
generators:
- list:
elements:
- environment: dev
cluster: https://dev-cluster.example.com
namespace: development
imageTag: latest
replicas: "1"
- environment: staging
cluster: https://staging-cluster.example.com
namespace: staging
imageTag: v1.4.2
replicas: "2"
- environment: production
cluster: https://prod-cluster.example.com
namespace: production
imageTag: v1.4.1
replicas: "5"
template:
metadata:
name: 'payments-service-{{environment}}'
spec:
project: default
source:
repoURL: https://github.com/myorg/k8s-configs
targetRevision: HEAD
path: 'apps/payments-service/{{environment}}'
helm:
parameters:
- name: image.tag
value: '{{imageTag}}'
- name: replicaCount
value: '{{replicas}}'
destination:
server: '{{cluster}}'
namespace: '{{namespace}}'
syncPolicy:
automated:
prune: true
selfHeal: true
The {{environment}}, {{cluster}}, etc. substitutions pull from the list elements. This generates three Application resources, all from one file. Adding a new environment is adding one entry to the elements list.
Cluster Generator
The Cluster generator creates one Application per cluster registered in ArgoCD's cluster store:
generators:
- clusters:
selector:
matchLabels:
environment: production
This is useful for cluster-scoped resources that every cluster needs: monitoring stacks, cert-manager, cluster autoscaler config. Register a new cluster in ArgoCD and it automatically gets the standard stack deployed. No manual Application creation.
You can filter by cluster labels — I tag clusters with environment: production, environment: staging, etc. and use label selectors to target the right set.
Git Generator (Directory)
This one is powerful. It creates one Application per directory in a git repo:
generators:
- git:
repoURL: https://github.com/myorg/k8s-configs
revision: HEAD
directories:
- path: apps/*
If apps/ contains payments-service/, auth-service/, and notification-service/, you get three Applications. Add a new directory and ArgoCD picks it up on the next reconciliation. This is the "app of apps on steroids" pattern — your repository structure defines your application inventory.
Progressive Delivery with RollingSync
Added in Argo CD 2.6, the RollingSync strategy lets you roll out changes across generated Applications in waves rather than all at once. This is critical for production:
spec:
strategy:
type: RollingSync
rollingSync:
steps:
- matchExpressions:
- key: environment
operator: In
values:
- dev
- matchExpressions:
- key: environment
operator: In
values:
- staging
maxUpdate: 1
- matchExpressions:
- key: environment
operator: In
values:
- production
maxUpdate: 25%
This rolls out dev first, waits for it to be healthy, then proceeds to staging (one app at a time), then production (25% of apps at a time). If anything in the wave fails, the rollout stops. This is the difference between "one ApplicationSet for all environments" being safe versus reckless.
Sync Policies
I use automated sync with pruning and self-healing on all environments:
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
prune: true means resources removed from git are deleted from the cluster. selfHeal: true means manual kubectl changes get overwritten on the next sync. If you're practicing GitOps, these should both be on. If you're not ready for that, start without them.
CreateNamespace=true is convenient — ArgoCD creates the destination namespace if it doesn't exist. Less bootstrapping required.
Things That Will Confuse You
Deleting an ApplicationSet deletes all the generated Applications and their resources. This is the "cascade delete" behavior. It's correct from a GitOps standpoint, but it means deleting the ApplicationSet by accident takes down all the apps it manages. There's a preserveResourcesOnDeletion option in the syncPolicy if you want to protect against this.
The template metadata.name must be unique across all generated Applications. If two elements in your list generator produce the same name, you'll get errors. Always include the environment or cluster name in the generated app name.
The Cluster generator pulls from ArgoCD's cluster secret store, not from your Kubernetes cluster list directly. A cluster has to be registered with argocd cluster add before the Cluster generator will see it.
Migration Path
If you have existing Application CRDs and want to migrate to ApplicationSets, you can't just create an ApplicationSet that would generate Applications with the same names — ArgoCD will refuse to manage Applications it didn't create. You have two options: delete the existing Applications first (brief outage during recreation) or use the --app-namespace flag to generate into a separate namespace and migrate gradually.
I've done this migration twice. Deleting and recreating was fine for staging. For production we did the gradual migration: created the ApplicationSet generating to a test name suffix, verified it worked, then deleted originals and recreated the ApplicationSet with correct names.
ApplicationSets removed a significant operational burden. The enforcement of consistency alone — every service getting the same sync policy, the same Helm parameters, the same structure — is worth the migration effort.
