Blog
July 19, 2022 Marie H.

ArgoCD ApplicationSets: Managing Many Apps at Once

ArgoCD ApplicationSets: Managing Many Apps at Once

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.