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

  1. The Project Trap: Argo CD Application resources must belong to a project. Your ApplicationSet template must specify project: "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.

  2. Destination Server vs. Name: In the destination field, server is the Kubernetes API URL. name is 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 the list generator, 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 broken Application.

  3. Sync Waves and ApplicationSets: This is a subtle one. If your ApplicationSet manifest itself is managed by Argo CD (hello, meta-GitOps!), you might run into sync wave issues. The Application resources 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.

  4. Permissions, Permissions, Permissions: The ApplicationSet controller needs RBAC permissions to create Application resources in the argocd namespace. 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.