一个跑 dev、QA、STG 的非生产集群把 Argo CD 从 2.12 升到了 3.6.1。Argo CD 3.0.0(2025-05-06 发布)把 resource tracking 的默认方式从 label 换成了 annotation,为了让升级这一步只换版本,升级前先在 argocd-cm 里把 application.resourceTrackingMethod 固定成 label。升级后核对资源时,monitoring 命名空间里有两个 StatefulSet 带着 argocd.argoproj.io/instance: kube-prometheus-stack,可 chart 渲染出来的清单里没有它们,它们是 Prometheus Operator 根据 Prometheus 和 Alertmanager 资源建出来的。

Argo CD 把一个资源算进哪个 Application,决定了这个资源会不会被比对、会不会被 prune。下面在一个 kind 集群里对比两种跟踪方式:Argo CD v3.6.1(2026-11-04 发布),用 argo-cd Helm chart 10.17.1 安装,示例应用是 argocd-example-apps 的 guestbook,固定在提交 8088f4c,同步策略是 automated 加 prune 和 selfHeal。

Argo CD 靠什么认定一个资源属于哪个 Application#

Resource Tracking 文档列了三种方式,由 argocd-cm 里的 application.resourceTrackingMethod 选择:

  • label:在每个由它同步的资源上写一个 label,值是 Application 的名字。upstream 默认的 key 是 app.kubernetes.io/instance,argo-cd Helm chart 在 values.yaml 第 202 行 把它改成了 argocd.argoproj.io/instance,开头那个集群和下面的实验用的都是这个 key。
  • annotation:写一个 argocd.argoproj.io/tracking-id annotation,3.0 起是默认值。
  • annotation+label:用 annotation 跟踪,同时写一个只给其它工具参考的 label,按文档的说法这个 label 超过 63 个字符会被截断。

2.14 到 3.0 的升级指南解释了改默认值的原因:annotation「is more reliable and less prone to errors caused by external code copying tracking labels from one resource to another」。开头那两个 StatefulSet 正是被外部代码复制了 tracking label 的资源。

label 方式下,带着同名 label 的副本会被 prune#

实验集群先用 label 方式安装,Helm values 里只改了这一项:

configs:
  cm:
    application.resourceTrackingMethod: label

guestbook 同步完以后,guestbook-ui 这个 Service 和 Deployment 上都有 argocd.argoproj.io/instance=guestbook。接着模拟一个复制资源的工具:把 Service 导出、改个名字再建回去,label 原样带过去。

kubectl -n guestbook get svc guestbook-ui -o json \
  | jq 'del(.metadata.uid,.metadata.resourceVersion,.metadata.creationTimestamp,.metadata.managedFields,.metadata.annotations["kubectl.kubernetes.io/last-applied-configuration"],.spec.clusterIP,.spec.clusterIPs,.status) | .metadata.name="guestbook-ui-copy"' \
  | kubectl create -f -

副本在建出来的同一秒被删掉。application controller 的日志(只保留时间、消息和相关字段):

07:31:18 Requesting app refresh caused by object update application=guestbook kind=Service name=guestbook-ui-copy
07:31:18 Tasks (dry-run) application=guestbook tasks="[Sync/0 resource /Service:guestbook/guestbook-ui-copy obj->nil (,,)]"
07:31:18 Adding resource result, status: 'Pruned', phase: 'Succeeded', message: 'pruned' application=guestbook kind=Service name=guestbook-ui-copy

同一秒里 Application 上的事件:

07:31:18 OperationStarted Initiated automated sync to '8088f4c0d970abb09e250248cc97e35623447cb5'
07:31:18 ResourceUpdated Updated sync status: Synced -> OutOfSync
07:31:18 OperationCompleted Partial sync operation to 8088f4c0d970abb09e250248cc97e35623447cb5 succeeded
07:31:18 ResourceUpdated Updated sync status: OutOfSync -> Synced

label 方式下 Argo CD 只看 label 的值。副本的 label 写着 guestbook,它就被算进 guestbook;Git 里没有这个对象,它就成了多余资源,应用变成 OutOfSync;prune: true 的自动同步随即把它删掉,应用又回到 Synced。事后翻 UI,只能看到一次「Partial sync operation」。

同样复制了 label、但带 ownerReference 的对象不会被这样处理。我又建了一个 ConfigMap,label 写 guestbook,ownerReference 指向 guestbook-ui 这个 Deployment。20 秒后它还在,应用保持 Synced,顶层资源列表里只有 Service 和 Deployment,controller 日志里也没有提到它:有 owner 的对象被当作 owner 的子资源,不参与 prune 判断。

开头那两个 StatefulSet 属于这种情况。Prometheus Operator v0.95.1(2026-11-06 发布)在 statefulset.go 第 85–97 行用 operator.WithLabels(objMeta.GetLabels()) 把 Prometheus 资源上的 label 全部复制到 StatefulSet,同一组调用里的 operator.WithManagingOwner(p) 设好了 ownerReference,所以它们只是在 UI 里挂在 kube-prometheus-stack 名下,没有被删。风险在没有 ownerReference 的副本上。文档举的例子是在命名空间之间复制资源的 HNC,还提到「several Helm charts and operators also use this label for generated manifests」;手工导出、改名、再 apply 一份,也是同样的结果。

label 方式下,应用名超过 63 字节就同步不了#

label 的值最长 63 字节。我建了两个 66 字节的应用名,前 64 个字节相同,只差结尾的 -a 和 -b,本来想看截断以后两个应用会不会互相认领资源。v3.6.1 的 label 方式下没有截断,同步直接失败:

one or more objects failed to apply, reason: Deployment.apps "guestbook-ui" is invalid: metadata.labels: Invalid value: "guestbook-payments-reconciliation-worker-with-a-long-team-prefix-a": must be no more than 63 bytes

两个应用都停在 OutOfSync / Missing,目标命名空间里一个对象也没建出来。文档里的「truncated」对应的是 annotation+label 方式下那个只供参考的 label。用 ApplicationSet 按「服务名-环境」拼应用名时,服务名一长就会碰到这个上限。

annotation 方式下,tracking-id 记的是资源自己的身份#

tracking-id 的格式在升级指南里:

argocd.argoproj.io/tracking-id: <app name>:<resource group>/<resource kind>:<resource namespace>/<resource name>

切到 annotation 并同步之后,guestbook 的两个资源上是这样的,core group 为空,所以 Service 的值里 / 前面没有内容:

Service     guestbook:/Service:guestbook/guestbook-ui
Deployment  guestbook:apps/Deployment:guestbook/guestbook-ui

再用上一节的命令复制一个 Service,这次复制的是长名字应用 lab-a 命名空间里那个已经带 annotation 的 guestbook-ui。副本的 annotation 里写的还是原来那个 Service 的名字:

{"name":"guestbook-ui-copy","tracking":"guestbook-payments-reconciliation-worker-with-a-long-team-prefix-a:/Service:lab-a/guestbook-ui"}

40 秒后副本还在,应用保持 Synced,没有触发同步。资源列表里多了这个副本,它没有同步状态:

[('Service', 'guestbook-ui', 'Synced'), ('Service', 'guestbook-ui-copy', None), ('Deployment', 'guestbook-ui', 'Synced')]

文档把这种情况叫「Non self-referencing annotations」:annotation 里的 name、namespace、group、kind 和资源本身对不上时,这个资源「will neither affect the application’s sync status nor be marked for pruning」。上一节那两个长名字的应用也是在这一步好的:改完设置 11 秒后两个都同步成功,annotation 里是完整的 66 字节应用名。

同一个集群里跑多个 Argo CD 实例时,annotation 方式还可以在 argocd-cm 里配 installationID,每个资源会多一个 argocd.argoproj.io/installation-id,不同实例里同名的 Application 不会互相认领资源。

从 label 切到 annotation:每个应用都要显式同步一次#

切换只改 argocd-cm 里的一个 key,Argo CD 不用重启:

data:
  application.resourceTrackingMethod: annotation

升级指南的说法是「the Argo CD tracking annotation will only appear on an Application’s resources when either a new Git commit is made or the Application is explicitly synced」。实验里分三种情况看了一遍:

  • 已经 Synced 的 guestbook:改设置后过了一个完整的 reconcile 周期(timeout.reconciliation 120 秒,加 60 秒 jitter),应用还是 Synced,资源上仍然只有 label。自动同步只在应用 OutOfSync 时触发,这里没有触发。
  • 来了新提交、但这个应用的清单没变:rev-demo 先固定在 e5f4ed8,切换后把 targetRevision 改成 8088f4c,这两次提交之间 guestbook/ 目录没有改动。应用在新 revision 上直接是 Synced,最后一次同步操作停在 e5f4ed8,label 也还在。很多服务共用一个清单仓库时,大部分应用在大部分提交里都是这种情况。
  • 显式同步:argocd app sync guestbook 之后 label 被去掉,换成了上一节看到的 annotation。

只带 label 的资源,在下一次需要删除东西时会出问题,升级指南把它列成了「Potential for orphaned resources」。edge-demo 在 label 方式下部署了 guestbook,切到 annotation 后把它的 path 改成 kustomize-guestbook(资源名带 kustomize- 前缀),切换后的第一次同步本该删掉旧的 guestbook-ui,结果是:

KIND         NAME                     LABEL       TRACKING
Service      guestbook-ui             edge-demo   <none>
Service      kustomize-guestbook-ui   <none>      edge-demo:/Service:edge/kustomize-guestbook-ui
Deployment   guestbook-ui             edge-demo   <none>
Deployment   kustomize-guestbook-ui   <none>      edge-demo:apps/Deployment:edge/kustomize-guestbook-ui

新资源建出来了,旧的 Service 和 Deployment 还在跑,也不在应用的资源列表里。Argo CD 已经不看 label,又找不到 annotation,认不出它们是自己的。label 方式下同样的改动会把它们 prune 掉,第一节的实验已经看到了。

所以切换要按这个顺序来:

  1. 确认没有东西靠 tracking label 选资源。Service、Deployment、NetworkPolicy、PDB 的 selector 可以这样查,实验集群输出为空;监控规则和脚本里有没有按这个 label 过滤,要另外 grep。有依赖的话用 annotation+label。

    kubectl get svc,deploy,sts,ds,networkpolicy,pdb -A -o json \
      | jq -r '.items[] | select(((.spec.selector // {}) | tostring | test("argocd.argoproj.io/instance")) or ((.spec.podSelector // {}) | tostring | test("argocd.argoproj.io/instance"))) | "\(.kind) \(.metadata.namespace)/\(.metadata.name)"'
    
  2. 找出带 ApplyOutOfSyncOnly=true 的应用,升级指南里有现成的 jq 命令。按指南的说法,这类应用切换后要在 UI 里取消这个选项再同步一次,CLI 做不到。

  3. 改设置,然后马上对所有应用显式同步一次。在这之前,不要合并任何会删除资源或删除应用的改动。实验集群里用的是 --core 模式,两条命令都多带一个 --core,而且 kubeconfig 当前 context 的 namespace 要设成 argocd,否则会报 configmap "argocd-cm" not found:

    argocd app list -o name | xargs -n1 argocd app sync
    
  4. 确认没有资源只带 label、没有 annotation。资源类型按集群实际用到的补全:

    kubectl get deploy,svc -A -l argocd.argoproj.io/instance -o json \
      | jq -r '.items[] | select(.metadata.annotations["argocd.argoproj.io/tracking-id"] == null) | "\(.kind) \(.metadata.namespace)/\(.metadata.name)"'
    

实验集群在批量同步之后跑第 4 步,只剩下 edge-demo 留下的两个孤儿,这两个只能手工删除:

Deployment edge/guestbook-ui
Service edge/guestbook-ui

小结#

label 方式只看 label 的值,带着同名 label、又没有 ownerReference 的副本会被当成多余资源,开着 prune 的应用在一秒内就会删掉它;Prometheus Operator 复制了 label 却没出事,是因为它同时设了 ownerReference。应用名超过 63 字节时,label 方式在 v3.6.1 下直接同步失败。annotation 方式在 tracking-id 里记下资源自己的 group、kind、namespace 和 name,复制出来的对象对不上就被忽略,名字长度也不再受 label 的限制。从 label 切过去时,已经 Synced 的应用不会自己补上 annotation,和它无关的新提交也不会,要对每个应用显式同步一次,并在这之前避免删除资源的变更,否则旧资源会变成没人管的孤儿。

参考资料#