Portfolio · Notes · Dotfiles

Search everything

Search case studies, engineering notes, and Dotfiles documentation.

    all notes

    Argo CD ApplicationSet generators: list, cluster and matrix

    One ApplicationSet produces many Applications; the three generators that supply its parameters, what each one yields, and the matrix trick that pins a revision per environment.

    An ApplicationSet is one resource that produces many Argo CD Applications: a template for the application, plus a generator that supplies one set of parameters per application to produce. Ten services across four clusters is forty Application manifests written by hand, or one ApplicationSet whose generator yields forty parameter sets. This post is about the three generators that cover nearly everything I deploy, list, cluster and matrix, and what each one produces.

    The list generator

    The parameters are written out by hand, one map per application, and every key becomes a variable in the template:

    platform-tools.yaml
    apiVersion: argoproj.io/v1alpha1
    kind: ApplicationSet
    metadata:
    name: platform-tools
    spec:
    generators:
    - list:
    elements:
    - appName: atlantis
    namespace: automation
    - appName: crossplane
    namespace: crossplane-system
    template:
    metadata:
    name: "{{ appName }}"
    spec:
    project: default
    source:
    repoURL: [email protected]:org/repo.git
    targetRevision: main
    path: "charts/{{ appName }}"
    helm:
    valueFiles: ["{{ appName }}-values.yaml"]
    destination:
    server: https://kubernetes.default.svc
    namespace: "{{ namespace }}"

    The controller creates two Applications from it, and keeps them in step with the list: an element removed is an application deleted.

    Applicationhelm.valueFilesdestination.namespace
    atlantisatlantis-values.yamlautomation
    crossplanecrossplane-values.yamlcrossplane-system

    The cluster generator

    One parameter set per cluster Argo CD knows. Clusters are registered as secrets labelled argocd.argoproj.io/secret-type: cluster, and the generator reads them: name and server from the secret’s fields, plus every label and annotation as metadata.labels.* and metadata.annotations.*. A selector narrows the set; without one, every cluster is in. The cluster Argo CD itself runs in is named in-cluster.

    Here each cluster carries an env label, and the values file follows it:

    metrics-server.yaml
    apiVersion: argoproj.io/v1alpha1
    kind: ApplicationSet
    metadata:
    name: metrics-server
    spec:
    generators:
    - clusters: {}
    template:
    metadata:
    name: "metrics-server-{{ name }}"
    spec:
    project: default
    source:
    repoURL: [email protected]:org/repo.git
    targetRevision: main
    path: charts/metrics-server
    helm:
    valueFiles: ["{{ metadata.labels.env }}-values.yaml"]
    destination:
    server: "{{ server }}"
    namespace: kube-system

    With two registered clusters, east labelled env: development and west labelled env: production:

    ApplicationClusterhelm.valueFiles
    metrics-server-easteastdevelopment-values.yaml
    metrics-server-westwestproduction-values.yaml

    The matrix generator

    The Cartesian product of two generators: every parameter set of the first combined with every set of the second. A list of applications times the clusters is the common case:

    team-tools.yaml
    spec:
    generators:
    - matrix:
    generators:
    - list:
    elements:
    - appName: external-dns
    - appName: cert-manager
    - clusters: {}
    template:
    metadata:
    name: "{{ appName }}-{{ name }}"
    spec:
    project: default
    source:
    repoURL: [email protected]:org/repo.git
    targetRevision: main
    path: "kustomize/overlays/{{ appName }}"
    destination:
    server: "{{ server }}"
    namespace: "{{ appName }}"

    Two applications times the same two clusters is four Applications: external-dns-east, external-dns-west, cert-manager-east and cert-manager-west.

    A revision per environment

    The second generator in a matrix may use the first generator’s parameters, which turns the product into a join. Here the list carries an environment and the revision to deploy there, and the cluster selector reads the environment from the list, so each element lands only on its own clusters and staging can run a tagged release while development follows main:

    elasticsearch-exporter.yaml
    apiVersion: argoproj.io/v1alpha1
    kind: ApplicationSet
    metadata:
    name: elasticsearch-exporter
    spec:
    generators:
    - matrix:
    generators:
    - list:
    elements:
    - environment: development
    revision: main
    - environment: staging
    revision: elasticsearch-exporter-v1.3.0
    - clusters:
    selector:
    matchLabels:
    env: "{{ environment }}"
    template:
    metadata:
    name: "elasticsearch-exporter-{{ name }}"
    spec:
    project: default
    source:
    repoURL: [email protected]:org/repo.git
    targetRevision: "{{ revision }}"
    path: "kustomize/elasticsearch-exporter/overlays/{{ name }}"
    destination:
    server: "{{ server }}"
    namespace: monitoring

    The revision values are the fields a tool such as Renovate can bump in a pull request, and in a monorepo with CODEOWNERS routing each ApplicationSet to its team, that pull request goes to the people who own the deployment.

    Limitations

    Every generated parameter is a string unless the ApplicationSet sets goTemplate: true; a list element with a number or a boolean in it has to quote it, and nested maps flatten to dotted keys.

    A generator that produces nothing produces a deletion. A cluster secret removed by mistake, or a list emptied by a bad merge, deletes every Application the set owned, and with the default sync policy their resources with them. preserveResourcesOnDeletion and a sync policy of create-only are the guard rails, and they are opt-in.

    Template names must be unique across everything the set generates. Two elements that render the same metadata.name yield one Application and an error in the controller’s log about the other.

    The cluster generator sees only clusters registered with Argo CD. A cluster added to the fleet but not to Argo CD is not a target, and no application tells you so.

    What’s next?

    Everything above puts variables into template.spec.source, and that field is where the real choices are: a chart from a Helm repository, a chart in Git, a Kustomize overlay, or Kustomize inflating a chart. Pointing an ApplicationSet at Helm and Kustomize: five options takes them one at a time.

    Comments