ᕕ( ᐛ )ᕗ Jimyag's Blog

KubeVirt 如何自动同步 VM Label 到 Launcher Pod

有些调度、监控或运维组件只能读取 Pod Label,但 KubeVirt 中业务侧维护的是 VirtualMachine。如果 VM 已经运行,只修改 spec.template.metadata.labels,怎样让 Label 继续更新到当前的 VirtualMachineInstancevirt-launcher Pod?

KubeVirt 当前 main 提供了 virt-controller 参数 --additional-launcher-labels-sync。它可以按完整 key 或前缀选择 Label,并沿着下面的方向单向同步:

1
VM.spec.template.metadata.labels -> VMI.metadata.labels -> virt-launcher Pod.metadata.labels

我在独立的 Kubernetes 集群中用两台 Label 不同的 VM 做了在线修改和删除测试。结果是 VMI 与现有 Pod 都能更新,并且 Pod UID 不变,不需要重建 VM Pod。

本文验证的是 KubeVirt main 的 commit 3e83f1130a。不要据此判断某个旧版本已经包含这项能力,使用前应检查目标版本源码或 virt-controller --help

配置要同步的 Label

通过 KubeVirt CR 给 virt-controller 增加参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
apiVersion: kubevirt.io/v1
kind: KubeVirt
metadata:
  name: kubevirt
  namespace: kubevirt
spec:
  customizeComponents:
    flags:
      controller:
        additional-launcher-labels-sync: "ops.example.com/*"

也可以直接 patch 现有 KubeVirt CR:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
kubectl -n kubevirt patch kubevirt kubevirt --type=merge -p '{
  "spec": {
    "customizeComponents": {
      "flags": {
        "controller": {
          "additional-launcher-labels-sync": "ops.example.com/*"
        }
      }
    }
  }
}'

如果只允许固定的几个 key,可以使用逗号分隔:

1
additional-launcher-labels-sync: "ops.example.com/owner,ops.example.com/vm-id"

如果每台 VM 的 key 也不同,使用末尾带 * 的前缀即可:

1
additional-launcher-labels-sync: "ops.example.com/*"

例如两台 VM 可以分别使用:

1
2
3
4
5
6
7
8
9
# VM A
labels:
  ops.example.com/vm-id: vm-a
  ops.example.com/owner-a: team-a

# VM B
labels:
  ops.example.com/vm-id: vm-b
  ops.example.com/owner-b: team-b

它们不需要拥有相同的 Label key。控制器匹配的是前缀,而不是预先枚举每台 VM 的完整 Label 集合。Kubernetes Label 的 prefix/name 格式可以参考 Labels and Selectors

源码中的参数说明同时给出了精确语义:支持完整 key、支持末尾 * 的前缀,并且同步方向是 VM template 到 VMI,再到 launcher Pod。1

单独配置 "*" 不会同步全部 Label。两段同步实现都会拒绝空前缀,避免一次配置错误把所有 Label 复制到 Pod。23

修改 VM 的 template,而不是 VM metadata

需要修改的是:

1
spec.template.metadata.labels

不是:

1
metadata.labels

下面的命令会在线修改 VM A 的 owner,并增加一个新 Label:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
kubectl -n vm-demo patch vm vm-a --type=merge -p '{
  "spec": {
    "template": {
      "metadata": {
        "labels": {
          "ops.example.com/owner-a": "team-a-v2",
          "ops.example.com/dynamic-a": "enabled"
        }
      }
    }
  }
}'

可以同时查看 VM、VMI 和 Pod:

1
2
3
4
5
6
7
8
kubectl -n vm-demo get vm vm-a \
  -o jsonpath='{.spec.template.metadata.labels}'

kubectl -n vm-demo get vmi vm-a \
  -o jsonpath='{.metadata.labels}'

kubectl -n vm-demo get pod -l vm.kubevirt.io/name=vm-a \
  -o jsonpath='{.items[0].metadata.labels}'

确认参数已经进入 virt-controller 时,需要一起检查 container 的 commandargs

1
2
kubectl -n kubevirt get deploy virt-controller \
  -o jsonpath='{.spec.template.spec.containers[0].command}{" "}{.spec.template.spec.containers[0].args}{"\n"}'

控制器如何完成同步

这不是 VM 重启流程,而是两段独立的 reconcile:

  flowchart LR
    VM["VirtualMachine<br/>spec.template.labels"]
    VC1["VM Controller<br/>筛选 exact key 或 prefix"]
    VMI["VirtualMachineInstance<br/>metadata.labels"]
    VC2["VMI Controller<br/>筛选同一组 key"]
    Pod["现有 virt-launcher Pod<br/>metadata.labels"]

    VM --> VC1 -->|"Patch VMI"| VMI
    VMI --> VC2 -->|"Patch Pod"| Pod

第一段位于 VM Controller 的 syncDynamicAnnotationsAndLabelsToVMI。它会克隆 VMI 当前的 Label,再按配置逐个处理:2

  1. 完整 key 直接比较 VM template 与 VMI 中的值。
  2. 前缀模式会遍历 VM 和 VMI 中匹配前缀的 key。
  3. VM 中存在的 key 会新增或覆盖到 VMI。
  4. VMI 中存在、但 VM template 已删除的 key 会从 VMI 删除。
  5. 有变化时使用带 test 的 JSON Patch 更新 VMI。

第二段位于 VMI Controller 的 syncDynamicAnnotationsAndLabelsToPod,使用相同规则把 VMI Label patch 到现有 launcher Pod。3

这也解释了为什么删除能够继续传播:前缀匹配不只遍历源对象,也遍历目标对象。目标中多出来的匹配 key 会被识别并删除。

隔离集群中的验证

测试环境如下:

项目
Kubernetes v1.36.3
KubeVirt main@3e83f1130a
Provider kubevirtci k8s-1.36
同步前缀 codex.test/*

我使用 make cluster-up 创建独立集群,再通过 make cluster-sync 部署当前源码。没有使用或修改已有 Kubernetes 集群。

测试创建了两台 VM:

VM 初始 Label 在线更新
A owner-a=team-a-v1 owner-a=team-a-v2,新增 dynamic-a=enabled
B owner-b=team-b-v1 owner-b=team-b-v2,新增 dynamic-b=blue

VM A 更新后,四个观测值一致:

1
2
3
4
VMI owner-a     = team-a-v2
VMI dynamic-a   = enabled
Pod owner-a     = team-a-v2
Pod dynamic-a   = enabled

更新前后的 Pod UID 都是:

1
3148fea0-fa94-4311-863a-fdb959ef15d2

随后从 VM template 删除 dynamic-a,VMI 和 Pod 上的 dynamic-a 都被删除,Pod UID 仍未变化。

VM B 使用完全不同的 owner-bdynamic-b。更新后的 VM template、VMI 和 Pod 都是:

1
2
owner-b  = team-b-v2
dynamic-b = blue

Pod 上没有出现 VM A 的 owner-a,更新前后的 Pod UID 都是:

1
b721533b-c5f7-4484-a987-bb50f62e9876

隔离集群只有一个节点,同时运行两台 VMI 时内存不足,因此测试按 A、B 顺序执行。这个限制影响并发运行数量,不影响每条同步链路的验证。

测试结束后删除了测试 Namespace,并执行 KUBEVIRT_PROVIDER=k8s-1.36 make cluster-down。节点容器和测试网络均已删除。

使用时需要注意的边界

  1. 同步是单向的。 修改 VMI 或 Pod 不会回写 VM template,后续 reconcile 还可能被 template 覆盖。
  2. 只同步配置命中的 key。 其他 VM/VMI/Pod Label 不会因为这个参数被统一复制。
  3. 不要使用裸 * 当前实现会忽略它;应使用自己的域名前缀,例如 ops.example.com/*
  4. Label 更新可能改变其他控制器行为。 如果 Service、NetworkPolicy、监控或调度逻辑依赖这些 Pod Label,应先在测试环境验证 selector 的影响。
  5. 关闭参数不等于清理旧 Label。 从实现看,控制器只遍历当前配置的 key 或前缀。移除配置后,它不再知道哪些旧 Label 应删除;需要先从 VM template 删除并等待同步完成,再关闭参数。
  6. 先确认版本。 本文只证明 commit 3e83f1130a 上的实际行为,没有验证某个正式 release 的首次支持版本。

对于「每台 VM 的 Label 都不同」这个场景,关键不是把所有 key 写进 controller 参数,而是先规划一个专用前缀,再让每台 VM 在这个前缀下自由定义自己的 key。这样既保留了 VM 之间的差异,也把允许传播到 launcher Pod 的范围限制在可审查的边界内。


  1. KubeVirt application.go 中的 additional-launcher-labels-sync 参数定义。 ↩︎

  2. KubeVirt VM Controller 的 syncDynamicAnnotationsAndLabelsToVMI。 ↩︎ ↩︎

  3. KubeVirt VMI Controller 的 syncDynamicAnnotationsAndLabelsToPod。 ↩︎ ↩︎

#KubeVirt #Kubernetes #VirtualMachine #Label