ApplicationSet no Argo CD: um Application por cluster, gerado a partir de uma pasta no Git
Se você já geriu Argo CD em mais de um cluster, provavelmente já viveu isso: um Application por cluster, cada um apontando pra uma pasta diferente do mesmo repo, todos quase idênticos — mudando só destination.server e um ou outro parâmetro de ambiente. Funciona até o dia em que alguém adiciona um cluster novo e esquece de copiar o Application, ou muda uma regra de sync em um e esquece nos outros três.
O ApplicationSet resolve isso: é um controller que vem embutido no Argo CD desde a v2.3 e adiciona um CRD, ApplicationSet, que gera Applications automaticamente a partir de um ou mais generators. Em vez de manter N manifests parecidos, você mantém um ApplicationSet e deixa o controller criar, atualizar e remover os Applications conforme a fonte de dados muda — pasta nova no Git, cluster novo registrado, item novo numa lista.
Este post é um padrão de referência: a estrutura é a que a documentação oficial do projeto recomenda para o caso mais comum (deploy por pasta, multiplicado por cluster), não um relato de “rodei isso em produção, aqui vão os números”. Os detalhes abaixo foram conferidos na documentação oficial do Argo CD.
O problema que o ApplicationSet resolve
Imagina um repo organizado assim:
apps/
cluster-addons/
cert-manager/
ingress-nginx/
external-dns/
E três clusters registrados no Argo CD: staging, prod-us, prod-eu. Sem ApplicationSet, você escreve 3 pastas × 3 Applications = 9 manifests (ou usa um script pra gerar). Com ApplicationSet, você escreve um ApplicationSet que combina duas fontes: a lista de pastas em apps/cluster-addons/* e a lista de clusters registrados. O controller multiplica as duas e cria um Application pra cada combinação.
Essa combinação de dois generators tem nome: Matrix generator.
Generator 1: Git generator (directories)
O Git generator varre um repositório e extrai parâmetros a partir da estrutura de diretórios ou do conteúdo de arquivos. Pra descobrir pastas:
generators:
- git:
repoURL: https://github.com/sua-org/seu-repo.git
revision: HEAD
directories:
- path: apps/cluster-addons/*
- path: apps/cluster-addons/_template
exclude: true
Cada diretório encontrado vira um conjunto de parâmetros: (caminho completo), (nome da última pasta) e `` pra pegar um segmento específico do caminho, se sua estrutura tiver mais níveis.
Uma regra que vale a pena guardar: exclusões têm prioridade sobre inclusões, independente da ordem em que você escreve as entradas. Se uma pasta bate com um path normal e também com um exclude: true, ela fica de fora — não importa qual veio primeiro na lista.
Existe também a variante files, que lê o conteúdo de arquivos JSON/YAML (por exemplo um config.json por cluster) em vez de só olhar nomes de pasta — útil quando você precisa de metadados que não cabem no nome do diretório.
Generator 2: Cluster generator
O Cluster generator itera sobre os clusters já registrados no Argo CD. Registro de cluster no Argo CD é, por baixo dos panos, um Secret no namespace do Argo CD com o label argocd.argoproj.io/secret-type: cluster:
generators:
- clusters:
selector:
matchLabels:
argocd.argoproj.io/secret-type: cluster
Parâmetros disponíveis: (nome do cluster), (endpoint da API), (campo project do secret, vazio por padrão) e / `` pra qualquer label ou annotation que você tenha posto no secret do cluster — é assim que dá pra marcar clusters como env: prod ou region: us-east e usar isso no template.
Gotcha real: o cluster local (onde o Argo CD roda) é incluído por padrão quando você usa clusters: {} sem seletor — porque ele não depende de um Secret pra existir. Se seu ApplicationSet usa um selector com matchLabels, o cluster local fica de fora automaticamente, já que ele não tem o label argocd.argoproj.io/secret-type pra bater. Isso é bom na maioria dos casos (você não quer que todo addon multi-cluster também role no control plane), mas vale conferir antes de assumir que “clusters: {}” cobre só os remotos.
Juntando os dois: Matrix generator
Agora a parte que resolve o problema original — multiplicar pasta × cluster:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: cluster-addons
namespace: argocd
spec:
goTemplate: true
goTemplateOptions: ["missingkey=error"]
generators:
- matrix:
generators:
- git:
repoURL: https://github.com/sua-org/seu-repo.git
revision: HEAD
directories:
- path: apps/cluster-addons/*
- path: apps/cluster-addons/_template
exclude: true
- clusters:
selector:
matchLabels:
argocd.argoproj.io/secret-type: cluster
template:
metadata:
name: '{{.path.basename}}-{{.name}}'
spec:
project: default
source:
repoURL: https://github.com/sua-org/seu-repo.git
targetRevision: HEAD
path: '{{.path.path}}'
destination:
server: '{{.server}}'
namespace: '{{.path.basename}}'
syncPolicy:
automated:
prune: true
selfHeal: true
Com 3 pastas de addon e 3 clusters registrados, isso gera 9 Applications, nomeados cert-manager-staging, cert-manager-prod-us, e assim por diante. Adicionar um cluster novo (registrar o Secret) ou uma pasta nova no Git basta — o controller recalcula a matriz e cria o que faltar, sem você tocar no ApplicationSet.
Duas restrições do Matrix generator que valem anotar antes de desenhar algo mais ambicioso:
- Ele combina no máximo dois generators filhos por vez. Pra combinar três fontes, você aninha um Matrix dentro de outro — mas generators de combinação (
matrixoumerge) só podem ser aninhados uma vez, não indefinidamente. - Quando um generator filho consome parâmetros do outro (por exemplo, um Git generator cujo
pathdepende de `` do Cluster generator), o generator que consome tem que vir depois, na lista, do generator que produz. E os dois não podem depender um do outro ao mesmo tempo — isso é dependência circular e o controller recusa.
Se os dois generators filhos forem Git generators, os dois vão gerar uma chave `` com valores diferentes — use pathParamPrefix em cada um pra não colidir.
goTemplate: true não é opcional de verdade
Repare que os dois exemplos acima usam goTemplate: true e goTemplateOptions: ["missingkey=error"]. Isso ativa o motor de templates do Go (em vez do substituidor de string simples legado) e, com missingkey=error, qualquer `` que não existir no conjunto de parâmetros falha a geração em vez de virar string vazia silenciosamente. Num ApplicationSet que combina generators com nomes de parâmetro parecidos (como dois Git generators sem pathParamPrefix), isso é a diferença entre descobrir o erro de configuração na hora e descobrir três semanas depois que um namespace saiu vazio.
E quando os clusters não são iguais?
O template do ApplicationSet só gera o Application CR — source, destination, syncPolicy. Ele não toca no manifest da aplicação em si. Então “clusters com config diferente” são, na prática, dois problemas separados, e cada um se resolve numa camada diferente.
Divergência dentro da aplicação (réplicas, limites de recurso, env vars) não é problema do ApplicationSet — é problema do Helm/Kustomize que ele está invocando. Resolve apontando pra um arquivo de values por cluster:
spec:
source:
repoURL: https://github.com/sua-org/seu-repo.git
path: '{{.path.path}}'
helm:
valueFiles:
- 'values-{{.name}}.yaml'
Ou, se a app usa Kustomize, um overlay por cluster (overlays/) no lugar de um values file.
Divergência no próprio Application — namespace diferente, project diferente, syncPolicy mais conservador em prod — aí sim é o ApplicationSet. O caminho é marcar o Secret do cluster com labels (env: prod, tier: critical) e usar condicional do Go template, já que goTemplate: true está ligado:
spec:
syncPolicy:
automated:
prune: true
selfHeal: '{{if eq .metadata.labels.env "prod"}}false{{else}}true{{end}}'
Isso funciona bem pra 2-3 variações. Passado isso, o template fica um emaranhado de if/eq e a leitura piora rápido. Nesse ponto compensa trocar o Cluster generator por um List generator, onde cada cluster é uma entrada explícita com os valores já resolvidos — perde a auto-discovery (cluster novo não aparece sozinho, precisa editar a lista), mas ganha em clareza:
generators:
- matrix:
generators:
- git:
directories:
- path: apps/cluster-addons/*
- list:
elements:
- name: staging
server: https://staging.k8s.internal
selfHeal: "true"
- name: prod-us
server: https://prod-us.k8s.internal
selfHeal: "false"
Regra prática: se a divergência é só nome/label do cluster, Cluster generator + condicional resolve. Se a divergência é um conjunto de valores arbitrário por cluster (não só um ou dois campos), List generator com os valores escritos explicitamente é mais fácil de revisar num PR do que decifrar uma cadeia de {{if}}.
O gotcha de segurança que a doc oficial marca como crítico
Se você está pensando em deixar o campo project do template dinâmico — algo como project: '', pra multitenancy self-service — pare e leia o aviso da documentação primeiro: se o campo project é templado, um time com permissão de editar o Git generator pode criar Applications sob Projects com permissões maiores do que deveria ter. A recomendação oficial, se você precisar desse padrão, é usar apenas repositórios “non-scoped” (sem --project fixado) e exigir aprovação de admin em todo PR que toque a definição do ApplicationSet. Fora desse cenário específico de multitenancy self-service, o caminho mais simples — e mais seguro — é fixar project como valor literal, como no exemplo acima.
Um detalhe de cache que explica sync atrasado
O Git generator faz polling a cada 3 minutos por padrão. Se o cache de revisão do Repo Server do Argo CD tiver expiração maior que esse intervalo, o controller pode continuar enxergando a revisão antiga por mais tempo do que o esperado — ou seja, você fez push, esperou 3 minutos, e o ApplicationSet ainda não viu a pasta nova. Antes de abrir um ticket de “o ApplicationSet não atualizou”, confere o TTL do cache do Repo Server.
Quando usar isso (e quando não)
ApplicationSet com Matrix generator (Git + Cluster) compensa quando você tem uma estrutura repetível — mesmo conjunto de addons ou aplicações, aplicado a uma lista de clusters que cresce com o tempo. Se você tem só 1 ou 2 clusters e manifests que divergem bastante entre eles, o Application direto ainda é mais simples de ler e debugar — ApplicationSet adiciona uma camada de indireção que só paga o custo quando a repetição é real.
Pra começar: registre os clusters normalmente (argocd cluster add), organize o repo em uma pasta por addon/app, e comece com o Matrix generator acima trocando os paths pelos seus. Adicione goTemplateOptions: ["missingkey=error"] desde o primeiro dia — é mais barato descobrir um parâmetro quebrado no argocd appset generate do que num cluster de produção que ficou sem sync.