Skip to content
ioob.dev
Go back

ArgoCD Part 6 — Multi-Cluster and ApplicationSet

· 5 min read
ArgoCD Series (6/8)
  1. ArgoCD Part 1 — What Is ArgoCD
  2. ArgoCD Part 2 — Installation and Initial Setup
  3. ArgoCD Part 3 — Registering an Application
  4. ArgoCD Part 4 — Kustomize and Helm
  5. ArgoCD Part 5 — Sync Strategies: Auto Sync, Self Heal, and Order Control
  6. ArgoCD Part 6 — Multi-Cluster and ApplicationSet
  7. ArgoCD Part 7 — RBAC and SSO: Team-Level Access Control
  8. ArgoCD Part 8 — Practical Patterns: App of Apps, CI Integration, Troubleshooting
Table of contents

Table of contents

What You Don’t Know with a Single Cluster

When first adopting ArgoCD, you typically install it on one cluster and deploy applications to the same cluster. The setup is simple and the behavior is easy to understand.

But as organizations grow, clusters multiply. Separate ones for development, staging, and production. They get split by region or by team. At this point, you need to decide whether to install ArgoCD separately on each cluster or manage multiple clusters from a single ArgoCD instance.

The hub-spoke model where one ArgoCD manages multiple clusters has a clear advantage: centralized management. You can see all deployment states in one place and apply policies consistently.

Registering Clusters

The cluster where ArgoCD is installed is automatically registered under the name in-cluster. To add external clusters, use the argocd cluster add command.

First, the cluster you want to add must be registered in your kubeconfig. Check the current context list.

kubectl config get-contexts

Once you’ve identified the context name of the desired cluster, register it with the ArgoCD CLI.

argocd cluster add staging-cluster-context

This command creates a argocd-manager ServiceAccount on the target cluster, sets up the necessary RBAC permissions, and stores the cluster’s connection information as a Secret in ArgoCD. You can check the registered cluster list with:

argocd cluster list

The output looks roughly like this:

SERVER                          NAME        VERSION  STATUS
https://kubernetes.default.svc  in-cluster  1.28     Successful
https://10.0.1.100:6443         staging     1.28     Successful
https://10.0.2.100:6443         production  1.27     Successful

Once registration is complete, when creating an Application, specify the cluster’s API server URL in destination.server or the cluster name in destination.name.

spec:
  destination:
    server: https://10.0.1.100:6443
    namespace: my-app

Or you can reference it by name.

spec:
  destination:
    name: staging
    namespace: my-app

Name-based references are more readable than URLs and eliminate the need to update Application manifests when the cluster API server address changes, making the name-based approach more preferred in practice.

Why ApplicationSet Is Needed

Let’s visualize how ApplicationSet creates multiple Applications from a single template and the role Generators play.

flowchart LR
    subgraph GEN["Generator (parameter source)"]
        G1["List\n(explicit enumeration)"]
        G2["Clusters\n(registered clusters)"]
        G3["Git\n(directories/files)"]
        G4["Matrix\n(cross product)"]
    end
    TMPL["template\n(Application template)"]
    APS["ApplicationSet Controller"]
    APP1["Application: app-dev"]
    APP2["Application: app-staging"]
    APP3["Application: app-prod"]
    GEN -->|Provide parameters| APS
    TMPL --> APS
    APS -->|"Inject {{cluster}}"| APP1
    APS -->|"Inject {{cluster}}"| APP2
    APS -->|"Inject {{cluster}}"| APP3

Imagine deploying the same application to three environments (dev, staging, prod). You need three Application resources, but most of the content is identical — only the environment name and cluster address differ. This duplication becomes increasingly burdensome to manage as environments grow.

ApplicationSet was created to solve this problem. It combines a single template with a parameter source (Generator) to automatically create multiple Applications. When a new environment is added, you just add one line to the Generator’s list — making it highly scalable.

The ApplicationSet controller has been included by default since ArgoCD 2.3, so no separate installation is needed.

List Generator — The Simplest Starting Point

The List Generator lets you define the parameter list directly. It works well when you have a small, well-defined set of environments.

Let’s write an ApplicationSet that deploys the same application to three environments.

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: my-app-set
  namespace: argocd
spec:
  generators:
    - list:
        elements:
          - cluster: dev
            url: https://kubernetes.default.svc
            namespace: my-app-dev
          - cluster: staging
            url: https://10.0.1.100:6443
            namespace: my-app-staging
          - cluster: production
            url: https://10.0.2.100:6443
            namespace: my-app-prod
  template:
    metadata:
      name: "my-app-{{cluster}}"
    spec:
      project: default
      source:
        repoURL: https://github.com/my-org/my-app.git
        targetRevision: main
        path: "k8s/overlays/{{cluster}}"
      destination:
        server: "{{url}}"
        namespace: "{{namespace}}"
      syncPolicy:
        automated:
          prune: true
          selfHeal: true

Each item listed in elements is substituted into {{variable}} in the template, creating Applications. The result is three Applications: my-app-dev, my-app-staging, and my-app-prod.

It’s intuitive and easy to understand, but if environments grow to dozens, managing the list becomes tedious. In that case, it’s better to use the Cluster Generator or Git Generator.

Cluster Generator — Automatic Creation from Registered Clusters

The Cluster Generator automatically reads information from clusters registered in ArgoCD to create Applications. When a new cluster is registered, the Application appears automatically, and when a cluster is removed, the Application disappears.

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: monitoring-set
  namespace: argocd
spec:
  generators:
    - clusters:
        selector:
          matchLabels:
            env: production
  template:
    metadata:
      name: "monitoring-{{name}}"
    spec:
      project: default
      source:
        repoURL: https://github.com/my-org/infra.git
        targetRevision: main
        path: monitoring/base
      destination:
        server: "{{server}}"
        namespace: monitoring
      syncPolicy:
        automated:
          prune: true
          selfHeal: true

The selector allows you to target only clusters with specific labels. The example above deploys the monitoring stack only to clusters with the env: production label.

Labeling clusters is simple.

argocd cluster set staging --label env=staging
argocd cluster set production-us --label env=production --label region=us
argocd cluster set production-eu --label env=production --label region=eu

With this setup, monitoring is automatically deployed whenever a new env: production cluster is added. This pattern is particularly useful for infrastructure components that need to be deployed across all clusters.

Git Generator — Repository Structure as Configuration

The Git Generator creates Applications based on the directory structure or file contents of a Git repository. Add a directory to the repo and an Application appears; delete it and it disappears. The repository structure itself becomes the deployment configuration.

There are two modes: directory-based and file-based.

The directory-based mode scans directories under a specific path. Suppose you have a repo structure like this:

k8s/
├── apps/
│   ├── api-server/
│   │   ├── deployment.yaml
│   │   └── service.yaml
│   ├── web-frontend/
│   │   ├── deployment.yaml
│   │   └── service.yaml
│   └── worker/
│       ├── deployment.yaml
│       └── service.yaml

To create an Application for each subdirectory, write the following:

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: apps-set
  namespace: argocd
spec:
  generators:
    - git:
        repoURL: https://github.com/my-org/infra.git
        revision: main
        directories:
          - path: k8s/apps/*
  template:
    metadata:
      name: "{{path.basename}}"
    spec:
      project: default
      source:
        repoURL: https://github.com/my-org/infra.git
        targetRevision: main
        path: "{{path}}"
      destination:
        server: https://kubernetes.default.svc
        namespace: "{{path.basename}}"
      syncPolicy:
        automated:
          prune: true
          selfHeal: true

{{path.basename}} becomes the directory name (api-server, web-frontend, worker), and {{path}} becomes the full path (k8s/apps/api-server, etc.). When adding a new microservice, just creating a directory automatically sets up the deployment.

The file-based mode reads parameters from JSON or YAML files. Use this when more granular configuration is needed.

config/
├── api-server.json
├── web-frontend.json
└── worker.json

If api-server.json contains the following:

{
  "appName": "api-server",
  "namespace": "api",
  "cluster": "https://kubernetes.default.svc",
  "replicas": "3"
}

These values can be referenced in the template.

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: apps-from-files
  namespace: argocd
spec:
  generators:
    - git:
        repoURL: https://github.com/my-org/infra.git
        revision: main
        files:
          - path: config/*.json
  template:
    metadata:
      name: "{{appName}}"
    spec:
      project: default
      source:
        repoURL: https://github.com/my-org/infra.git
        targetRevision: main
        path: "k8s/apps/{{appName}}"
      destination:
        server: "{{cluster}}"
        namespace: "{{namespace}}"
      syncPolicy:
        automated:
          prune: true
          selfHeal: true

Since each JSON file corresponds to one Application, you can manage each app’s configuration independently while keeping a single template.

Combining Generators

Generators are powerful on their own, but combining multiple ones handles more complex scenarios. The Matrix Generator creates a Cartesian product of two Generators’ results.

For example, if you need to deploy 3 apps to 3 clusters, that’s 9 Applications total. The Matrix Generator lets you express this concisely.

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: matrix-set
  namespace: argocd
spec:
  generators:
    - matrix:
        generators:
          - git:
              repoURL: https://github.com/my-org/infra.git
              revision: main
              directories:
                - path: k8s/apps/*
          - clusters:
              selector:
                matchLabels:
                  env: production
  template:
    metadata:
      name: "{{path.basename}}-{{name}}"
    spec:
      project: default
      source:
        repoURL: https://github.com/my-org/infra.git
        targetRevision: main
        path: "{{path}}"
      destination:
        server: "{{server}}"
        namespace: "{{path.basename}}"
      syncPolicy:
        automated:
          prune: true
          selfHeal: true

The Git Generator produces the app list, and the Cluster Generator produces the cluster list — Applications are created for every combination of the two. When new apps or clusters are added, it scales automatically, significantly reducing operational burden.


Managing multiple clusters from a central point and automating repetitive tasks with ApplicationSet noticeably improves infrastructure operations efficiency. But as clusters and applications grow, “who can access what” becomes important. In the next part, we’ll learn how to configure team-level access control through ArgoCD’s RBAC settings and SSO integration.

Part 7: RBAC and SSO


Related Posts

Share this post on:

Comments

Loading comments...


Previous Post
ArgoCD Part 5 — Sync Strategies: Auto Sync, Self Heal, and Order Control
Next Post
ArgoCD Part 7 — RBAC and SSO: Team-Level Access Control