The Best Way to Install Apps on Multiple Clusters in ArgoCD

The Best Way to Install Apps on Multiple Clusters in ArgoCD

February 11, 2025
2170 views
Get tips and best practices from Develeap’s experts in your inbox

The Challenge: Managing Apps Across Dozens of Clusters 

Imagine managing 40–50 Kubernetes clusters, each running a wide variety of applications and tools. Sounds overwhelming, right? Now, think about needing to update one of those tools—maybe it’s a new version or a configuration change. Without a centralized system, you’d have to go into each cluster, make the update manually, and hope nothing breaks along the way. It’s a recipe for errors, wasted time, and frustration.   But it doesn’t stop there. Each cluster serves a unique purpose—some run on different cloud platforms, others are for development, staging, or production. That means the tools and versions required for each cluster aren’t the same. On top of that, you’ll need a way to customize settings for each environment using separate `values.yaml` files while still keeping everything organized and manageable.   The complexity is undeniable: how do you maintain control over all clusters while allowing flexibility for unique needs? When we faced this challenge, it was clear we needed a solution that could handle this scale and complexity without becoming a bottleneck. 

The ultimate solution for managing apps across clusters

To address the complexity of managing applications across multiple clusters, we implemented a dynamic and scalable solution using ArgoCD ApplicationSets. First, we created an ApplicationSet to dynamically generate a list of applications for each cluster. This process uses a matrix generator, which iterates through the list of clusters, pulling their details based on specific labels defined in the cluster’s secret within ArgoCD. These labels determine which clusters are targeted for the application deployment. For each cluster, the ApplicationSet dynamically generates a list of applications and tools to be deployed. To simplify and centralize the management, we encapsulated this logic within Helm charts. This approach allows us to define the list of applications in a modular and reusable way, making it easy to adjust configurations, add new applications, or upgrade existing ones. Using this setup, every cluster receives the correct set of applications and configurations dynamically. The Helm chart ensures that all deployments are managed from a single centralized source, enabling seamless updates and efficient version control. This solution not only reduces manual effort but also ensures consistency across all environments while allowing for environment-specific customizations through separate values files.

Code Optimization

Optimizing cluster management with labels in Argo CD
This section explains how using labels in Argo CD improved cluster management, making it more dynamic and flexible.

Identifying and mapping clusters with labels
Labels, defined within the secret of the cluster in Argo CD, are a simple and effective way to identify and map clusters in the ApplicationSet, making cluster selection easy and automated. By using labels like pim/instance-name and argocd.argoproj.io/secret-type in the secret of the specific cluster, you can filter and target specific clusters for deployment without any manual effort. This approach removes the need for hardcoding, making it effortless to scale and saving time and effort by avoiding the creation of a separate ApplicationSet for each cluster.

Example Secret Template:


Example Secret Template:
apiVersion: v1
kind: Secret
metadata:
  name: my-cluster-secret
  labels:
    pim/instance-name: my-clusters
    argocd.argoproj.io/secret-type: cluster
type: Opaque
data:
  # Your secret data here

In this example, the labels are defined within the secret, allowing Argo CD to automatically target and map the cluster for deployment based on the metadata.


Here’s how it looks in the ApplicationSet:


selector:
  matchLabels:
    pim/instance-name: my-clusters
    argocd.argoproj.io/secret-type: cluster

In this example, matchLabels selects clusters based on their instance name and secret type, simplifying the process of choosing the right cluster for deployment.

Using labels for customizing applications
Labels provide a simple way to make your configurations more flexible by adding metadata. This allows you to control application names and behavior better. For example, labels like pim/region let you create unique and meaningful application names, making it easier to track them across different environments and regions. Additionally, you can use metadata.labels in any part of the ApplicationSet configuration to tailor it to your needs.

Example Code:


metadata:
  name: 'cluster-{{`{{ index .metadata.labels "pim/region" }}`}}'

Managing applications with ApplicationSet

ApplicationSet allows you to define and deploy a list of applications to a single cluster using reusable templates. It simplifies managing multiple applications by automating the deployment process and reducing manual work.

Example Configuration:

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: example-applicationset
  namespace: argo-cd
spec:
  generators:
    - list:
        elements:
          - releaseName: reloader
            chart: reloader
            repoURL: https://stakater.github.io/stakater-charts
            version: v1.0.*
            namespace: common
          - releaseName: keda
            chart: keda
            repoURL: https://kedacore.github.io/charts
            version: 2.15.*
            namespace: keda
  template:
    metadata:
      name: '{{ .releaseName }}-{{ .namespace }}'
    spec:
      destination:
        namespace: '{{ .namespace }}'
      source:
        repoURL: '{{ .repoURL }}'
        chart: '{{ .chart }}'
        targetRevision: '{{ .version }}'
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
Key benefits of using ApplicationSet:
  • Simplified management: Deploy a list of applications to a single cluster without creating separate ArgoCD applications.
  • Reusability: Use templates to easily deploy multiple applications with configurable parameters like versions and namespaces.
  • Scalability: Easily add more applications to the list as needed.
  • Consistency & automation: Ensures consistent deployments across applications with automated syncing and self-healing.

ApplicationSet streamlines the process of deploying and managing applications within a cluster, saving time and reducing errors.

Deploying multiple applications across multiple clusters

The Matrix generator in Argo CD’s ApplicationSet simplifies deploying multiple applications to multiple clusters. It dynamically generates configurations by combining cluster-specific details with an application list, ensuring consistent, scalable deployments while eliminating manual effort and reducing errors.

This approach addresses exactly the challenge we’re facing! Managing deployments across various environments with multiple clusters and applications can be tedious and error-prone. The Matrix Generator eliminates this complexity by automating the entire process, allowing us to scale deployments effortlessly and with confidence.

How the Matrix generator works

The configuration below demonstrates the Matrix Generator:


The configuration below demonstrates the Matrix Generator:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: infra-apps
  namespace: argo-cd
spec:
  generators:
    - matrix:
        generators:
          - clusters:
              selector:
                matchLabels:
                  pim/instance-name: my-clusters
                  argocd.argoproj.io/secret-type: cluster
          - list:
              elements:
                - releaseName: cert-manager
                  chart: cert-manager
                  repoURL: https://charts.jetstack.io
                  version: v1.16.*
                  namespace: cert-manager
                - releaseName: external-dns
                  chart: external-dns
                  repoURL: https://charts.bitnami.com/bitnami
                  version: 8.3.*
                  namespace: cert-manager
  template:
    metadata:
      name: 'cluster-{{ index .metadata.labels "pim/region" }}'
      namespace: argo-cd
    spec:
      project: infrastructure
      sources:
        - repoURL: '{{ .repoURL }}'
          path: '{{ or .path "" }}'
          chart: '{{ or .chart "" }}'
          targetRevision: '{{ .version }}'
          helm:
            valueFiles:
              - $values/infra-apps/{{ .releaseName }}.yaml
            releaseName: '{{ .releaseName }}'
      destination:
        name: '{{ index .metadata.labels "pim/cluster-name" }}'
        namespace: '{{ .namespace }}'
      syncPolicy:
        syncOptions:
          - CreateNamespace=true
        automated:
          selfHeal: true
          prune: true

Cluster selection
The clusters generator selects clusters automatically using matchLabels by targeting clusters with the labels pim/instance-name: my-cluster-dev and argocd.argoproj.io/secret-type: cluster ensuring that only the desired clusters are included in the deployment

Application list
The list generator specifies multiple applications along with their Helm chart details, such as name repository and version or Git repository paths. For example, the cert-manager application retrieves its chart from https://charts.jetstack.io and targets version v1.16.*

Dynamic application configuration
The Matrix Generator creates unique deployment configurations for each combination of cluster and application by leveraging the template field to dynamically set the application’s name namespace Helm values and destination cluster using parameters like releaseName repoURL and version

Automation and sync policies
Deployments are automated with sync policies that include selfHeal: true and prune: true to maintain consistency and correct any resource drift automatically.

What does the Matrix generator provide

This setup eliminates manual configuration for multi-cluster deployments. By defining applications and clusters in a single ApplicationSet, you can:

  • Scale effortlessly to support hundreds of clusters and applications.
  • Ensure consistency across deployments with minimal effort.
  • Adapt dynamically to new clusters or applications by simply updating the selectors or application list.

The Matrix Generator makes deploying and managing applications across multiple clusters simpler, faster, and more reliable.

Transforming the configuration to dynamic with Helm Chart

Imagine you need to deploy 10 applications across 40 clusters in 3 environments. You would typically need to define multiple ApplicationSet configurations—one for each environment or cluster group. Furthermore, whenever an upgrade is required or a new application needs to be added, you’d have to go through the files and make the necessary changes manually.

We’ve developed a solution to address this challenge that makes the entire deployment process dynamic using a Helm chart. By defining variables for clusters, environments, and applications, the Helm chart dynamically generates the required configurations, saving hours of manual effort and ensuring consistency. This approach transforms what would otherwise be static and repetitive configuration files into a fully adaptable setup, making your deployments highly efficient, scalable, and adaptable to any changes in your infrastructure or application setup.

The Value of Dynamic Helm Chart Configuration in ArgoCD ApplicationSet

  1. Dynamic Adaptation: The configuration can be adjusted dynamically for different clusters, environments, or applications without modifying the core logic.
  2. Scalability: Instead of manually configuring deployments, you can scale easily by updating variables.
  3. Consistency: It ensures all deployments follow the same structure, reducing misconfigurations and errors.
  4. Reusability: The same Helm chart can be reused across different projects, saving development time.
  5. Flexibility: It accommodates various needs, from adding new applications to deploying in new environments, by simply updating input variables.

Apologies for the omission! Here’s the updated version with all the requested sections included, along with additional explanations:

Achieving Dynamic Deployment with Helm and ArgoCD

{{- range $defaultName, $_ := .Values.infra_apps }}
{{- if or .create (eq .create nil) }}
{{- $name := .name | default $defaultName }}
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: {{ $name }}-{{ $.Values.instance_id }}
  namespace: argo-cd
spec:
  goTemplate: true
  generators:
    - matrix:
        generators:
          - clusters:
              selector:
                matchLabels:
                  pim/instance-name: {{ $.Values.instance_name }}
                  argocd.argoproj.io/secret-type: cluster
                  {{- with .clusters_selector }}
                  {{- toYaml . | nindent 18 }}
                  {{- end }}
          - list:
              elements:
              {{- range $releaseName, $_ := .helm_charts }}
              {{- if or .enabled (eq .enabled nil) }}
              - releaseName: {{ .releaseName }}
                {{- if hasKey . "path" }}
                chart: ""
                path: {{ .path }}
                {{- else }}
                chart: {{ .chart | default $releaseName}}
                path: ""
                {{- end }}
                repoURL: {{ .repoURL}}
                version: {{ .version }}
                namespace: {{ .namespace | default $releaseName }}
                extraSyncOptions: {{ .extraSyncOptions }}
                ignoreDifferences: {{ (.ignoreDifferences) | toYaml | nindent 18 }}
                {{- with .parameters  }}
                parameters:
                {{- range $name, $value := . }}
                - name: {{ $name | quote }}
                  value: {{ $value | quote }}
                {{- end }}
                {{- end }}
              {{- end }}
              {{- end }}
  syncPolicy:
    preserveResourcesOnDeletion: true
  template:
    metadata:
      name: '{{`{{ .releaseName }}`}}-{{`{{ index .metadata.labels "pim/region" }}`}}-{{ $.Values.instance_id }}'
      namespace: argo-cd
    spec:
      destination:
        name: '{{`{{ .name }}`}}'
        namespace: '{{`{{ .namespace }}`}}'
      syncPolicy:
        syncOptions:
        - CreateNamespace=true
        - '{{`{{ .extraSyncOptions }}`}}'
      {{- with .automated_sync }}
        {{- if .enabled }}
        automated:
          {{ toYaml .settings | nindent 10 }}
        {{- end }}
      {{- end }}
      project: {{ $.Values.project }}-infra
  templatePatch: |
    spec:
      {{`{{- with .ignoreDifferences }}`}}
      ignoreDifferences:
      {{`{{ . | toYaml | nindent 4 }}`}}
      {{`{{- end }}`}}
      sources:
        - repoURL: '{{`{{ .repoURL }}`}}'
          chart: '{{`{{ .chart }}`}}'
          path: '{{`{{ .path }}`}}'
          targetRevision: '{{`{{ .version }}`}}'
          helm:
            releaseName: '{{`{{ .releaseName }}`}}'
            {{`{{- with .parameters }}`}}
            parameters: 
            {{`{{ . | toYaml | nindent 10 }}`}}
            {{`{{- end }}`}}
            valueFiles:
            - $values/config/default/pi-instance/infra-apps/{{`{{ .releaseName }}`}}.yaml
            - $values/config/environments/{{ $.Values.project }}/pi-instance/infra-apps/{{`{{ .releaseName }}`}}.yaml
        - repoURL: {{ .config_repo_url | default $.Values.defaults.config_repo_url}}
          targetRevision: {{ .config_branch }}
          ref: values
{{- end  }}
---
{{- end }}
Cluster selection (selectors and matchLabels)

The clusters section in the generators part of the template uses label selectors to dynamically match clusters. This is done based on labels like pim/instance-name and argocd.argoproj.io/secret-type: cluster, which are dynamically populated from the Values object. The use of .clusters_selector further enhances this by allowing additional customizations to specify which clusters to match at runtime. This flexibility ensures that the template can adapt to different clusters and environments without needing static definitions.

Helm Chart List

This section dynamically iterates over the .helm_charts array defined in the Values object. Each Helm chart’s configuration—including parameters like releaseName, chart, repoURL, version, and an optional enabled flag—is extracted directly from the Values file.

The configuration in the Values object allows for a highly flexible and environment-specific setup. By defining the list of applications in the Values file, we can control which charts are deployed, their specific configurations, and any additional customizations. This approach ensures that the deployment process adapts dynamically to different environments, such as development, staging, or production, without requiring changes to the template itself.

This design simplifies Helm chart management across environments, centralizing control and enabling rapid adjustments directly through the Values file.

Dynamic Template Metadata (name and namespace)

The template.metadata.name and template.metadata.namespace are dynamically constructed using values like releaseName, region, and instance_id from the Values object. This ensures that each deployment has a unique identifier, which is essential for managing multiple deployments in the same namespace. The use of these Helm variables guarantees that resources are created with the appropriate names and namespaces, maintaining organization and avoiding conflicts.

Sync policy

The syncPolicy is configured to ensure that resources are preserved during deletion by setting preserveResourcesOnDeletion: true. This is a critical feature for managing environments where accidental deletion of resources could have significant consequences.

In dev and stg environments, it is recommended to enable automated synchronization by setting automated_sync.enabled to true. This ensures that changes are applied automatically when updates are detected, making deployments more efficient and reducing the need for manual intervention.

In contrast, for prd (production), we disable automatic synchronization to ensure greater control over what gets deployed. This allows for thorough monitoring and manual approval of changes before they are synced with the production environment.

The ability to enable or disable synchronization dynamically is controlled through the Values file, providing flexibility to adjust the behavior based on the requirements of each environment.

goTemplate and templatePatch

The goTemplate flag enables Go templating within the Argo CD ApplicationSet, allowing us to insert dynamic values and apply conditional logic, such as selectively including or excluding Helm charts based on runtime parameters. This makes the configuration highly flexible and adaptable to different environments.

The templatePatch allows us to customize the default template further. In this case, it adds the ignoreDifferences section, instructing Argo CD to disregard specific differences, such as annotations or labels, between local and live configurations. This prevents unnecessary syncs, improving deployment efficiency and providing more control over the generated resources.

Here is a minimal example of a values.yaml file:

instance_id: ""
instance_name: ""
# -- The project to create the ApplicationSet in
project: ""


defaults:
  # -- The URL of the Git repository to use for configuration
  config_repo_url: git@github.com:YourOrg/your-config-repo.git


# -- The list of applications to create
infra_apps:
  # Example application group
  example-app-group:
    config_branch: main
    automated_sync:
      enabled: true
      settings:
        selfHeal: true
        prune: true
    helm_charts:
      redis:
        repoURL: https://charts.bitnami.com/bitnami
        version: "20.2.*"
      nginx:
        chart: ingress-nginx
        repoURL: https://kubernetes.github.io/ingress-nginx
        version: 4.11.*
        namespace: ingress
      custom-app:
        path: custom-app
        repoURL: git@github.com:YourOrg/your-helm-charts.git
        version: HEAD
        parameters:
          app.customParam: "customValue"

 

We’re Hiring!
Develeap is looking for talented DevOps engineers who want to make a difference in the world.
Skip to content