背景#

我手上有几个走 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 中已废弃。如果项目用了新版 ImageUpdater CR(自定义资源),组织方式就不同了——通过 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 更干净。

选择的核心判据不是哪种模式更"先进",而是你的集群拓扑和团队结构在哪一层产生变量——变量在哪里,抽象就在哪里。