Until now every Application has deployed to the same cluster ArgoCD runs in (https://kubernetes.default.svc). In the real world you often have several clusters — dev, staging, prod, or one per region — and you want to manage them all from a single ArgoCD. This is the hub-and-spoke model: one ArgoCD (the hub) deploys to many workload clusters (the spokes).

Registering an external cluster

ArgoCD needs credentials to talk to each external cluster. The quickest way is the CLI. With your kubeconfig context pointing at the target cluster, run:

# List the contexts ArgoCD can see
argocd cluster list

# Add a cluster by kubeconfig context name
argocd cluster add prod-cluster --name prod

Under the hood this creates a ServiceAccount in the target cluster, grabs its token, and stores the connection details as a Secret in the argocd namespace of the hub.

Registering a cluster declaratively

Doing it by hand is fine for a demo, but GitOps means we prefer declarative config. A cluster is just a Secret with the label argocd.argoproj.io/secret-type: cluster:

apiVersion: v1
kind: Secret
metadata:
  name: prod-cluster
  namespace: argocd
  labels:
    argocd.argoproj.io/secret-type: cluster
type: Opaque
stringData:
  name: prod
  server: https://prod.example.com:6443
  config: |
    {
      "bearerToken": "<token>",
      "tlsClientConfig": {
        "insecure": false,
        "caData": "<base64-ca>"
      }
    }

Commit this (with the token itself sealed — see the Secret Management post!) and ArgoCD picks up the new cluster. From now on, prod is a valid deployment target.

Targeting a cluster from an Application

Once a cluster is registered, point an Application’s destination.server (or destination.name) at it instead of the in-cluster address:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: book-info-prod
  namespace: argocd
spec:
  project: default
  source:
    repoURL: 'https://github.com/VersusControl/devops-vn-blog'
    targetRevision: HEAD
    path: '_resource/argocd-series/06-kustomize/book-info/overlays/prod'
  destination:
    name: prod            # the cluster we registered
    namespace: book-info
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

That single line — name: prod — is all it takes to deploy to a different cluster.

Deploying to every cluster with the Cluster generator

Managing one Application per cluster by hand brings us back to the copy-paste problem from the previous post. The Cluster generator in ApplicationSet solves it: it produces one Application per registered cluster automatically.

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: book-info-all-clusters
  namespace: argocd
spec:
  goTemplate: true
  generators:
    - clusters: {}          # every registered cluster
  template:
    metadata:
      name: 'book-info-{{ .name }}'
    spec:
      project: default
      source:
        repoURL: 'https://github.com/VersusControl/devops-vn-blog'
        targetRevision: HEAD
        path: '_resource/argocd-series/06-kustomize/book-info/overlays/prod'
      destination:
        server: '{{ .server }}'
        namespace: book-info
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
        syncOptions:
          - CreateNamespace=true

The empty clusters: {} selector matches all clusters, including the local one. To target a subset, add a label selector that matches labels on the cluster Secrets:

  generators:
    - clusters:
        selector:
          matchLabels:
            environment: production

Now every production cluster receives the Book Info app, and adding a new prod cluster (with the right label) automatically gets it deployed — no changes to the ApplicationSet.

Per-cluster values

Different clusters often need different settings (region, replica count, ingress host). You can attach arbitrary values to a cluster Secret and read them in the template:

# on the cluster Secret
metadata:
  labels:
    argocd.argoproj.io/secret-type: cluster
    environment: production
  annotations:
    values.region: us-west-2
# in the ApplicationSet template, via goTemplate
      source:
        kustomize:
          namePrefix: '{{ index .metadata.annotations "values.region" }}-'

Architecture tips

  • Keep the hub cluster small and dedicated to ArgoCD; don’t run workloads on it.
  • Use network policies / private endpoints so only the hub can reach spoke API servers.
  • Give the hub’s ServiceAccount on each spoke the least privilege it needs.
  • Combine with Projects (from the Core Concepts post) to restrict which clusters and namespaces each team can deploy to.

Full manifests are in 10-multi-cluster.

With one ArgoCD managing many clusters, the last thing we need is proper access control for the humans logging in. In the final post we’ll set up SSO with Keycloak.