36.5 Argo CD ApplicationSets for Multi-Cluster GitOps
Alright, let’s talk about making GitOps actually work across the mess of clusters you’re now responsible for. You’ve got your shiny Argo CD installed in each one, and you’re manually creating Application resources. It works, but it feels like you’re just moving the manual kubectl apply step one level up. For every new cluster, you’re copy-pasting YAML and changing the cluster name. This is not the automated utopia we were promised.
Enter the ApplicationSet controller. This is Argo’s answer to the existential dread of multi-cluster management. Its job is brutally simple: it’s a controller that generates Argo CD Application resources for you, based on a template and a list of… well, things. Those “things” can be a list of clusters, a list of git directories, a matrix of both, or whatever else you can dream up using a generator. It takes the tedious, error-prone repetition and kills it with fire.
The Core Concept: Generators and Templates
Think of an ApplicationSet as a factory assembly line. The generator is the part that brings the raw materials to the line—a list of parameters like cluster names or git paths. The template is the robotic arm that stamps each set of parameters into a finished product: a valid Argo CD Application resource.
Here’s the most common generator you’ll use: the clusterDecisionResource generator. It’s a bit of a mouthful, but it’s brilliant. It asks an external service, “Hey, which clusters should I deploy to?” That external service is almost always the Open Cluster Management cluster registry. This is the proper way to do it: dynamic and based on a source of truth.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: my-app-multi-cluster
spec:
generators:
- clusterDecisionResource:
# This is the name of the ConfigMap that tells AppSet how to talk to OCM
configMapRef: my-ocm-config
labelSelector:
matchLabels:
environment: production # Only generate apps for prod clusters!
requeueAfterSeconds: 180 # Check for new clusters every 3 minutes
template:
metadata:
name: '{{name}}-my-app'
spec:
project: "default"
source:
repoURL: 'https://github.com/my-org/my-app.git'
targetRevision: HEAD
path: 'kustomize/overlays/production' # You are using Kustomize, right?
destination:
server: '{{server}}' # This 'server' and 'name' come directly
name: '{{name}}' # from the generator's output
syncPolicy:
automated:
selfHeal: true
syncOptions:
- CreateNamespace=true # This one is a lifesaver.
The magic is in those {{ }} placeholders. The generator fetches a list of clusters from OCM, each cluster object has a name and a server URL. The ApplicationSet controller then takes each cluster, injects those values into the template, and voilà, a brand new Application resource is born, perfectly configured for its target cluster.
The “Quick and Dirty” List Generator
Maybe you’re not using OCM yet. Maybe you’re just dipping your toes in. That’s fine, we all start somewhere. For that, you have the list generator. It’s exactly what it sounds like: a static, hardcoded list of clusters right in the manifest. It’s not elegant, but it gets the job done and is perfect for a proof-of-concept.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: simple-app-list
spec:
generators:
- list:
elements:
- cluster: cluster-east
server: https://kubernetes.default.svc # In-cluster for the local cluster
- cluster: cluster-west
server: https://api.cluster-west.example.com:6443
template:
metadata:
name: '{{cluster}}-my-app'
spec:
project: default
source:
repoURL: 'git@github.com:my-org/app.git'
targetRevision: main
path: manifests/
destination:
server: '{{server}}'
name: '{{cluster}}'
See? No magic, just a simple loop. The danger here is obvious: your infrastructure definition is now hardcoded in a YAML file, which is a recipe for drift. Use this to get started, but plan to migrate to a dynamic generator ASAP.
Common Pitfalls and How to Avoid Them
The Project Trap: Argo CD
Applicationresources must belong to a project. Your ApplicationSet template must specifyproject: "default"(or your custom project) or it will fail spectacularly. The error messages are… unhelpful. I’ve lost hours to this. Don’t be me.Destination Server vs. Name: In the
destinationfield,serveris the Kubernetes API URL.nameis the name of the cluster as defined in Argo CD’s own cluster secrets (argocd cluster add). These must match the values your generator provides. If you’re using thelistgenerator, you control these values. If you’re using a dynamic generator, you need to know what the external registry is returning. Mismatch here means a brokenApplication.Sync Waves and ApplicationSets: This is a subtle one. If your
ApplicationSetmanifest itself is managed by Argo CD (hello, meta-GitOps!), you might run into sync wave issues. TheApplicationresources created by the ApplicationSet controller will sync after the ApplicationSet itself. Plan your sync waves accordingly if you have hard dependencies, or you’ll be waiting forever for a sync that seems stuck.Permissions, Permissions, Permissions: The ApplicationSet controller needs RBAC permissions to create
Applicationresources in theargocdnamespace. This is usually handled by the Helm chart, but if you’re doing custom installations, you’ll screw this up at least once. The first thing to check when nothing happens is the controller’s logs.kubectl logs -l app.kubernetes.io/name=argocd-applicationset-controller -n argocd.
The power of ApplicationSets isn’t just in automating what you do now; it’s in enabling what was previously impossible. You can now define a single YAML file that says “deploy this app to every cluster tagged with ‘canary’” and be confident it will happen. That’s the GitOps dream, fully realized. Now go make your cluster sprawl someone else’s problem.