Argo ApplicationSet override template per environment

ApplicationSet is an elegant feature for handing out manual work to the Argo. It enables you to generate Argo Applications programmatically.

In this article, I will demonstrate how I tackled the problem of overriding ApplicationSet template.spec of the applications itself per environment.

ApplicationSet reference example is given below:

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: my-service
  namespace: argo
spec:
  generators:
    - list:
        elements:
          - cluster: my-cluster-1
            environment: "c1"
            url: https://kubernetes-cluster-ip:443
          - cluster: my-cluster-2
            environment: "c2"
            url: https://kubernetes-cluster-ip:443
          - cluster: my-cluster-3
            environment: "c3"
            url: https://kubernetes-cluster-ip:443
  template:
    metadata:
      name: '{{`{{cluster}}`}}-service'
      labels:
        type: kubernetes
    spec:
      project: 'default'
      source:
        chart: "ChartNamel"
        repoURL: https://ChartURL
        targetRevision: "1.0.0"
        helm:
          parameters:
            - name: env
              value: "{{`{{environment}}`}}"
          releaseName: service
          valueFiles:
            - "values-{{`{{ cluster }}`}}.yaml"
      destination:
        server: '{{`{{url}}`}}'
        namespace: default

Example above handles 3 clusters. It will create an application using generator capability.

There are many generators (See more). In this example generator list was used. What it does is next:

  • Define the list of the custom parameters for use in templating the template.spec itself.

You can define ApplicationSet as plain YAML or under the Helm chart. If the plain YAML is used then next notation for the templating is used:

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: my-service
  namespace: argo
spec:
  generators:
    - list:
        elements:
          - cluster: my-cluster-1
            environment: "c1"
            url: https://kubernetes-cluster-ip:443
          - cluster: my-cluster-2
            environment: "c2"
            url: https://kubernetes-cluster-ip:443
          - cluster: my-cluster-3
            environment: "c3"
            url: https://kubernetes-cluster-ip:443
  template:
    metadata:
      name: '{{cluster}}-service'
      labels:
        type: kubernetes
    spec:
      project: 'default'
      source:
        chart: "ChartNamel"
        repoURL: https://ChartURL
        targetRevision: "1.0.0"
        helm:
          parameters:
            - name: env
              value: "{{environment}}"
          releaseName: service
          valueFiles:
            - "values-{{cluster}}.yaml"
      destination:
        server: '{{url}}'
        namespace: default

On the other hand if defining under the Helm chart it's like the first example.

The difference is in the backticks eg. server: '{{ `{{url}}` }}' - this tells Helm to parse it as literal.

This is good for templating any values that are not maps or lists.

For example, what you cannot do is next (using helm):

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: my-service
  namespace: argo
spec:
  generators:
    - list:
        elements:
          - cluster: my-cluster-1
            environment: "c1"
            url: https://kubernetes-cluster-ip:443
            values: |-
              foo: bar
          - cluster: my-cluster-2
            environment: "c2"
            url: https://kubernetes-cluster-ip:443
            values: |-
              foo: baz
          - cluster: my-cluster-3
            environment: "c3"
            url: https://kubernetes-cluster-ip:443
            values: |-
              foo: bax
  template:
    metadata:
      name: '{{`{{cluster}}`}}-service'
      labels:
        type: kubernetes
    spec:
      project: 'default'
      source:
        chart: "ChartNamel"
        repoURL: https://ChartURL
        targetRevision: "1.0.0"
        helm:
          parameters:
            - name: env
              value: "{{`{{environment}}`}}"
          releaseName: service
          value: {{`{{url}}` | toYaml | nindent 8 }}
      destination:
        server: '{{`{{url}}`}}'
        namespace: default

So what you cannot do is to template the template.spec with any value that is map or list.

So how could I tackle this issue?

I found out that a generator can have multiple lists in itself and every list generator can override template.speclists values. ~ Thank you contributors to this great feature.

You will see in the example what I am talking about:

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: my-service
  namespace: argo
spec:
  generators:
    - list:
        elements:
          - cluster: my-cluster-1
            environment: "c1"
            url: https://kubernetes-cluster-ip:443
        template:
          metadata: {}
        spec:
          project: 'default'
          source:
            chart: "ChartName"
            repoURL: https://ChartURL
            targetRevision: "1.0.0"
            helm:
              parameters:
                - name: env
                  value: "{{`{{environment}}`}}"
              releaseName: service
              value: 
                overriding: true
          destination: {}
    - list:
        elements:
          - cluster: my-cluster-1
            environment: "c1"
            url: https://kubernetes-cluster-ip:443
        template:
          metadata: {}
        spec:
          project: 'default'
          source:
            chart: "ChartName"
            repoURL: https://ChartURL
            targetRevision: "1.0.0"
            helm:
              parameters:
                - name: env
                  value: "{{`{{environment}}`}}"
              releaseName: service
              value: 
                overriding: false
          destination: {}
  template:
    metadata:
      name: '{{`{{cluster}}`}}-service'
      labels:
        type: kubernetes
    spec:
      project: 'default'
      source:
        chart: "ChartNamel"
        repoURL: https://ChartURL
        targetRevision: "1.0.0"
        helm:
          parameters:
            - name: env
              value: "{{`{{environment}}`}}"
          releaseName: service
      destination:
        server: '{{`{{url}}`}}'
        namespace: default

This way baseline of the template.spec can be overridden via template.spec under the specific list generator. Which is an elegant way to override the options of the Application itself where a map or list is needed.

The template.spec under the list will take precedence over the baseline version of the template.spec. Fields with {} value will be ignored and the one from the baseline template.spec will be used.

This way you can only modify the fields you want.