ArgoCD 应用组织方式的选择和权衡:App-of-Apps 与 ApplicationSet 的实践对比
目录
背景#
我手上有几个走 GitOps 的 K8s 集群,ArgoCD 是它们唯一的部署入口。随着集群里部署的应用越来越多、环境越分越细,一个绕不开的问题出现了:ArgoCD 里那些 Application YAML,应该怎么组织?
这个问题的答案没有标准模板——ArgoCD 官方文档同时提供了 App-of-Apps 和 ApplicationSet 两种主流模式,社区还有一些混合变体。但我发现不少人在选型时会忽略一个前提:不同模式的优劣取决于你的集群拓扑、团队规模和应用结构。选错了,小则样板文件膨胀,大则每一次改动都牵一发动全身。
ArgoCD 的 Cluster Bootstrapping 文档里,ApplicationSet 配合 Cluster labels 是官方推荐的模式,App-of-Apps 被列为 alternative。这次要整理的正是这些官方文本没有深入展开的取舍过程。
这篇基于我接手过的几套 ArgoCD 架构做一次对比整理(我写这篇时 ArgoCD 最新稳定版为 v2.14.7,以下讨论的内容在该版本下验证通过)。三套架构对应三种典型场景:
| 场景 | 关键特征 |
|---|---|
| 个人项目 | 一个 ArgoCD 管两个异构集群,应用类型混杂,单人维护 |
| 团队项目 | 三个同构环境(dev/qa/stg),纯 Helm 应用,多人协作 |
| 混合 | 以上两者兼有——多集群且多环境,团队按领域分工 |
下面先看每种模式的实操面貌,最后汇总成一张决策表。
模式一:App-of-Apps(扁平显式)#
这是 ArgoCD 文档最早推荐的模式。核心思路是:一个 root Application 监视某个 Git 目录,目录下每个 YAML 文件对应一个子 Application。
root (Application)
└─ argocd/applications/*.yaml(排除 root.yaml)
├── loki.yaml # multi-source: remote chart + local values
├── kyverno.yaml # 同上
├── personal-services.yaml # directory.include 子集
├── cross-cluster-app.yaml # Kustomize 目录(推另一个集群)
└── monitoring-dashboards.yaml
子模式#
从我看到的实践来看,App-of-Apps 往下又可以分三种实现:
Helm multi-source(最常用)
Application 声明两个 source:一个指向 remote Helm chart repo,一个指向本地 Git 仓库的 values 文件路径。ArgoCD 的 $values 引用负责把两个 source 拼起来。
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: loki
namespace: argocd
spec:
project: default
sources:
- repoURL: https://grafana.github.io/helm-charts
chart: loki
targetRevision: "7.0.0"
helm:
valueFiles:
- $values/k8s/helm/values/loki.yaml
- repoURL: https://github.com/org/repo.git
targetRevision: main
ref: values
destination:
server: https://kubernetes.default.svc
namespace: monitoring
优点是 chart 版本声明式钉死、values 存在 Git 里可审计。缺点是这个 YAML 骨架每个应用都要抄一遍。
需要注意边界:multi-source 的设计目标是分离同一应用的 chart 和 values 来源,不是把无关应用拼进一个 Application。官方明确建议不要超过 2–3 个 source,超过这个数的项目基本在滥用这个特性,应该回到 ApplicationSet 或 App-of-Apps。
Kustomize 目录
直接把 application 指向本地仓库里的 Kustomize 目录。我看到的典型用法是用来推跨集群的 manifest——一个站点在 cloud/remote-cluster/manifests/ 里维护整棵 Kustomize 树,ArgoCD 的 destination.server 指向远程集群的 Tailscale 端点。
directory.include 子集
同一目录下有多个 YAML,用 glob 选出需要的那几个。适合把一组同 namespace 的小应用放在一起管理。
source:
path: k8s/helm/manifests
directory:
include: "{app-a.yaml,app-b.yaml,app-c.yaml}"
什么时候选 App-of-Apps#
从个人项目的实践中看,App-of-Apps 最适合这种场景:
- 集群拓扑异构——两个集群的 manifest 差别太大(一个本地小集群、一个云上免费层),模板化没有收益
- 应用类型混杂——Helm chart + Kustomize 树 + raw YAML 混在一起,没法统一抽象
- 单人维护——样板文件的重复成本可以接受,因为不用和同事同步
- 每个应用都有差异化配置——有的需要
ignoreDifferences,有的需要Prune=false保护 PVC,有的需要argocd-image-updater注解
在这个场景下,App-of-Apps 的核心优势在于完全显式。每个 Application 的配置都在文件里一字不差地写着,不需要推断模板渲染后的结果。调试时开一个 diff 就能看到所有变化。
代价是样板文件的重复。新加一个 Helm 应用需要复制粘贴一个 ~20 行的 YAML,改 repoURL、chart、values 路径、namespace 四个字段。应用从 10 个涨到 30 个时,这个重复感会越来越明显。
模式二:ApplicationSet#
ApplicationSet 是 ArgoCD 后来引入的模板化方案。核心思路是:定义一个 template + 一个 generator,generator 数据驱动 template 渲染出多个 Application。
我见过的实践里,Git generator 是最常用的:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: appsets
namespace: argocd
spec:
generators:
- git:
repoURL: https://gitlab.internal/team/infra.git
revision: HEAD
files:
- path: argocd/apps/*.yaml
template:
metadata:
name: '{{path.basename}}'
spec:
project: '{{project}}'
source:
repoURL: https://gitlab.internal/team/infra.git
targetRevision: HEAD
path: '{{path}}'
destination:
server: https://kubernetes.default.svc
namespace: '{{namespace}}'
syncPolicy:
automated:
prune: true
selfHeal: true
而每个 app 的 YAML 只需要写差异化字段:
# argocd/apps/nginx.yaml — 只需要定义 path 和 namespace
project: team-a
namespace: nginx
Generator 变体#
ArgoCD 官方文档列出了几种 generator,各自解决不同的场景:
| Generator | 数据来源 | 最佳场景 |
|---|---|---|
| Git | repo 内目录或文件 | 已有结构化的 Git 仓库,配置即数据 |
| Cluster | ArgoCD 已知的集群列表 | 多集群同质化部署:所有集群都跑同一套应用 |
| Matrix | 两个 generator 的笛卡尔积 | 集群 × 应用的组合管理 |
| Merge | 多个 generator 按优先级合并 | 基参数 + 环境覆盖,后定义参数覆盖前者 |
| SCM Provider | GitHub/GitLab org 下的 repo 列表 | 每个微服务一个 Git 仓库,按 repo 生成 |
| Pull Request | PR 列表 | Preview environment / PR-based review apps |
| Cluster Decision Resource | 自定义资源决定目标集群 | 需要自定义调度逻辑的灰度或金丝雀发布 |
| Plugin | RPC HTTP 请求 | 自定义外部数据源 |
| List | 硬编码列表 | 最简单的批量化 |
什么时候选 ApplicationSet#
从团队项目的实践中看,ApplicationSet 最擅长这个场景:
- 多环境同构——dev/qa/stg 三套环境,每套部署相同的组件,只是 values 不同
- 应用类型统一——全部是 Helm chart,每个环境下的 app 文件结构一致
- 团队协作——每个环境有独立的 AppProject(dev / qa / stg),RBAC 按 project 隔离
- 新增环境成本低——复制 ApplicationSet,换 project 和目录路径即可
在这个场景下,ApplicationSet 的核心优势是模板消除了重复。环境 A 到环境 B 的变化仅仅是一个 variables 文件,而不是一整组 YAML 的逐行对比。
除了 template 的条件分支,ApplicationSet 还有几个控制面细节隐含了运维成本:ignoreApplicationDifferences 允许某些实例偏离 template 定义——否则 ApplicationSet controller 会持续将 Application 修正回 template 的理想状态,这意味着手动改某个实例(比如临时调整 syncPolicy 做故障恢复)会被回滚;spec.syncPolicy.applicationsSync 则用来限制 controller 对子 Application 的改动权限(取值 create-only / create-update / create-delete / sync),比如设为 create-only 可以防止模板变化意外覆盖已手工调整的实例。
此外,ApplicationSet 还支持通过 spec.strategy.type: RollingSync 实现渐进式同步——分批推进更改、每批确认后再继续,适合控制批量改动时的爆炸半径。
这些控制点说明一个事实:ApplicationSet 不是"批量版 App-of-Apps",而是一种有独立运维模型的模式。改 template 的影响面覆盖所有实例——这既是它的优势,也是它的核心风险。
模式三:分层 App-of-Apps(模块化)#
这是前面两种的折中——把扁平的 App-of-Apps 按领域拆成几层,每层一个 root Application 管理自己的子应用。
root(顶层)
├── observability (loki / tempo / grafana / mimir)
├── security (kyverno / trivy / tetragon / falco)
├── infra (gateway / vault / backup / cloudflare)
└── cross-cluster (远程集群的应用,Kustomize)
这种模式的适用信号很明确:
- 不同领域的同步策略不同——安全组件需要更保守的 sync policy(fail-open 时不主动 prune),而普通应用可以用更激进的
selfHeal - 团队按领域分工——observability 组和管理 infra 组的变更面天然隔离
- AppProject 按领域拆分——精细化的 RBAC 控制
代价是增加了层级。每一层多一个 root Application,ArgoCD UI 里多了个层级结构。总体来说,应该在扁平模板已经不够用、但又不值得上全套 ApplicationSet + Cluster generator 的中间地带考虑它。
几个容易被忽略的考量点#
argocd-image-updater 兼容性#
如果你用了 argo-cd 的 argocd-image-updater,它在 Application 上打的 annotation(argocd-image-updater.argoproj.io/image-list)是 Application 级别的。ApplicationSet 里需要在 template 中按需注入这个 annotation——如果并非所有 app 都需要 image update,这个注入需要额外处理。
这个 annotation 模式对应的是
useAnnotations: true配置路径,在 image-updater v1.0.0 中已废弃。如果项目用了新版ImageUpdaterCR(自定义资源),组织方式就不同了——通过applicationRefs关联目标 Application 列表(用namePattern做名匹配筛选),每个 Application 下指定images列表,配置镜像名、版本约束、更新策略(semver / newest-build / alphabetical / digest)等。控制器通过 CR 的状态(Ready/Reconciling/Error)暴露运行情况。官方文档在 argocd-image-updater.readthedocs.io。新项目建议直接使用 CR 方式,避免后续迁移成本。
调试复杂度#
- App-of-Apps:调试复杂度最低。应用名就是文件名,去对应目录找到 YAML 就能看到全貌。
- ApplicationSet:需要 trace generator 的数据源 → template 渲染。Git generator 相对好理解,Cluster generator + Matrix generator 的组合需求先在纸上推演一遍。
- 分层 App-of-Apps:介于两者之间,看单层时和 App-of-Apps 一样清晰。
变更影响面#
- App-of-Apps:改一个 app 只影响它自己。但删除 root Application 时 child Application 会残留为 orphan,不会被自动清理——需要先用
argocd app delete删除 child,或通过 Cascade 策略控制。另外,App-of-Apps 不会持续修正 child Application 的配置漂移:创建后直接改 child 的 spec(比如加一个ignoreDifferences),改动会保持到下一次 root 重新同步。 - ApplicationSet:改 template 的
syncPolicy会影响该 ApplicationSet 管理的所有实例。必要时可以拆成多个 ApplicationSet(按环境拆分就是一种常见做法)。 - 分层 App-of-Apps:局部隔离,分到同一层内的 app 共享 sync policy。
新增应用的成本#
- App-of-Apps:复制粘贴 YAML,改几个字段。简单但机械。
- ApplicationSet(Git generator):在 generator 扫描的目录下放一个新的 YAML,只写差异化字段。减少样板文件。
- 分层 App-of-Apps:在对应的领域层下加一个 Application YAML,和 App-of-Apps 一样。
决策框架#
综合下来,我给自己总结了一个简化的判断流程:
集群拓扑是否同质?
├── 否(每个集群的 manifest 差别很大)
│ └── App-of-Apps —— 模板化没收益,显式声明更可靠
└── 是(所有集群跑同一套应用)
├── 是否多环境且环境结构一致?
│ ├── 是 → ApplicationSet(Cluster generator 或 Git generator)
│ └── 否 → App-of-Apps
└── 环境是否跨团队共享?
├── 是 → 考虑分层 App-of-Apps(按领域拆 project)
└── 否 → 扁平 App-of-Apps 或 ApplicationSet 均可
当然,这个判断框架不是金科玉律——实际项目里每个维度都有连续光谱。比如你同时在维护两个异构集群,但每个集群内部又有三套环境,那就需要"外层 App-of-Apps 按集群拆分 → 内层 ApplicationSet 按环境批量生成"的嵌套结构。
小结#
- App-of-Apps 适合异构集群、混杂应用类型、小团队或单人维护——牺牲样板文件换可读性和变更安全性。
- ApplicationSet 适合同构多环境、统一 Helm 或 Kustomize、团队按环境隔离——用模板消除重复,但注意条件分支是危险信号。
- 分层 App-of-Apps 是两者的折中——当不同领域的同步策略和 RBAC 需求分化时拆层,比在扁平结构里堆 switch-case 更干净。
选择的核心判据不是哪种模式更"先进",而是你的集群拓扑和团队结构在哪一层产生变量——变量在哪里,抽象就在哪里。