适用场景

某个 Deployment 使用 registry.example.com/payments/api:stable 这类会被覆盖的标签。流水线已经把新镜像推送到仓库,也执行了 kubectl rollout restart deployment/payments-api,但灰度流量中仍偶发旧接口行为;更棘手的是,同一 ReplicaSet 中不同 Pod 的日志还显示不同构建号。

本文以 Kubernetes Deployment 为例,说明如何区分“控制器确实创建了新 Pod”“节点重新解析了标签”和“容器实际运行的是哪个镜像 digest”,并给出可回滚的 digest 发布方案。示例要求具有目标命名空间的只读和更新 Deployment 权限;镜像仓库地址、digest 与构建号均为演示值。

先明确三个容易混淆的事实

kubectl rollout restart 的作用是修改 Pod 模板注解,从而创建新的 ReplicaSet 和 Pod;它并不把某个标签固定为新镜像。imagePullPolicy: IfNotPresent 会优先使用节点本地已存在的镜像;即使使用 Always,解决的也是“新建容器时重新向仓库解析标签”,不是可追溯性问题。

标签是可移动的名称,digest 才唯一标识镜像内容。若 stable 在发布期间从 digest A 被覆盖为 digest B,节点 A 可能启动 A、节点 B 可能启动 B;若某些旧 Pod 尚未终止,还会继续运行 A。排障时不要只比较 .spec.containers[].image,它往往仍只是 :stable。

现场取证:从模板、Pod 到 imageID

先取出控制器记录的镜像与拉取策略:

kubectl -n production get deploy payments-api \
  -o jsonpath='{range .spec.template.spec.containers[*]}{.name}{"\t"}{.image}{"\t"}{.imagePullPolicy}{"\n"}{end}'

kubectl -n production rollout status deploy/payments-api --timeout=5m

第一条输出的是期望,例如 api registry.example.com/payments/api:stable IfNotPresent。第二条只证明本次滚动的副本已就绪,不能证明所有容器来自同一构建。若拉取策略为空,不要据标签自行猜测:Kubernetes 会在对象首次创建时写入默认值,之后改变标签并不会自动修改该字段。

接着同时读取每个 Pod 的镜像声明和运行时 imageID:

kubectl -n production get pods -l app=payments-api \
  -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{range .status.containerStatuses[*]}{.name}{"\t"}{.image}{"\t"}{.imageID}{"\n"}{end}{end}'

典型输出中,.image 仍是标签,而 imageID 的末尾会包含运行时解析后的 @sha256:...。同一业务容器出现多个 digest,或存在没有 imageID 的未就绪容器时,都不应继续放量。imageID 的前缀会随 containerd、CRI-O 和私有仓库实现变化,所以脚本只应抽取 @sha256: 后的值,不能依赖固定前缀。

还应确认没有旧 ReplicaSet 遗留副本:

kubectl -n production get rs -l app=payments-api \
  -o custom-columns=NAME:.metadata.name,DESIRED:.spec.replicas,CURRENT:.status.replicas,READY:.status.readyReplicas

kubectl -n production get deploy payments-api \
  -o jsonpath='{.status.updatedReplicas}{" updated / "}{.status.readyReplicas}{" ready / "}{.status.replicas}{" total\n"}'

updatedReplicas 应等于期望副本数,readyReplicas 也应与总副本数一致。若旧 ReplicaSet 仍有期望副本,优先排查发布工具是否同时修改了 HPA、是否存在暂停的 Deployment,或是否手工扩容了旧 ReplicaSet;不要直接删除 Pod 来“碰运气”刷新镜像。

用小脚本把 digest 一致性变成发布门禁

下面的脚本接收 kubectl get pod -o json 的标准输入。它验证业务容器都已产生 imageID,并且全部等于流水线传入的预期 digest。真实流水线应把 digest 作为受保护的构建产物传入,不能从不可信日志拼接。

from __future__ import annotations

import json
import sys
from collections.abc import Iterable


def image_digest(image_id: str) -> str | None:
    marker = "@sha256:"
    if marker not in image_id:
        return None
    return "sha256:" + image_id.rsplit(marker, maxsplit=1)[1]


def verify_running_digests(pods: Iterable[dict[str, object]], container_name: str, expected: str) -> list[str]:
    problems: list[str] = []
    for pod in pods:
        metadata = pod.get("metadata", {})
        status = pod.get("status", {})
        pod_name = metadata.get("name", "<unknown>") if isinstance(metadata, dict) else "<unknown>"
        statuses = status.get("containerStatuses", []) if isinstance(status, dict) else []
        matched = [item for item in statuses if isinstance(item, dict) and item.get("name") == container_name]
        if len(matched) != 1:
            problems.append(f"{pod_name}: 找不到唯一的容器状态")
            continue
        actual = image_digest(str(matched[0].get("imageID", "")))
        if actual != expected:
            problems.append(f"{pod_name}: 运行 digest 为 {actual or '缺失'},期望 {expected}")
    return problems


if __name__ == "__main__":
    expected_digest = sys.argv[1]
    payload = json.load(sys.stdin)
    errors = verify_running_digests(payload["items"], "api", expected_digest)
    if errors:
        raise SystemExit("\n".join(errors))

保存为流水线工作目录中的 verify_image_digest.py 后,在滚动完成并经过就绪检查时运行:

kubectl -n production get pods -l app=payments-api -o json |
  python3 verify_image_digest.py \
  sha256:8d92c7b1d1b88f7e4f5e0f70de0ad2a0d4e6e1c8a1234567890abcdef0123456

退出码为 0 才允许后续流量切换。脚本刻意把“没有容器状态”“缺少 digest”“digest 不一致”都视为失败;这避免在新 Pod 尚未启动完成时错误通过。它不替代可用性探针、接口冒烟测试或签名验证。

根治:发布不可变引用,而不是覆盖标签

推荐让构建流水线在推送后获取镜像 digest,并把发布清单写成不可变引用。以下值需要由实际镜像仓库返回,不要复制示例 digest:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: payments-api
  namespace: production
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  template:
    metadata:
      labels:
        app: payments-api
      annotations:
        release.example.com/build: "2026.10.29.1"
    spec:
      containers:
        - name: api
          image: registry.example.com/payments/api@sha256:8d92c7b1d1b88f7e4f5e0f70de0ad2a0d4e6e1c8a1234567890abcdef0123456
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080
          readinessProbe:
            httpGet:
              path: /readyz
              port: 8080

对于 digest 引用,IfNotPresent 是合理的:本地缓存命中同一 digest 不会改变实际代码,且无需重复下载已有层。重要的是 spec.template 中的 image 发生变化,这会生成新的 ReplicaSet;仅在仓库覆盖 :stable 后执行 restart 不能提供同样的确定性。

发布命令应保留变更记录,并等待控制器收敛:

kubectl -n production set image deploy/payments-api \
  api=registry.example.com/payments/api@sha256:8d92c7b1d1b88f7e4f5e0f70de0ad2a0d4e6e1c8a1234567890abcdef0123456

kubectl -n production annotate deploy/payments-api \
  release.example.com/build='2026.10.29.1' --overwrite

kubectl -n production rollout status deploy/payments-api --timeout=5m

set image 会修改 Deployment 的 Pod 模板;注解用于将代码版本、变更单或构建记录关联到这次发布,但不是镜像身份的可信来源。若集群有准入控制器把标签自动改写为 digest,应以实际创建后的 Pod 模板和 imageID 为准,并把这一改写纳入发布审计。

回滚不能只写“回滚版本号”

Digest 发布的回滚应明确指向上一条已验证的 digest。先保留每次成功发布的 manifest 或受版本控制的 Helm/Kustomize 输出;发生问题时,把镜像改回上一条 digest 并等待就绪,再运行同一份 digest 校验。

kubectl -n production set image deploy/payments-api \
  api=registry.example.com/payments/api@sha256:4c5b3a291ee0d129ef60f4c3a9d0d2cce7771234567890abcdef0123456789

kubectl -n production rollout status deploy/payments-api --timeout=5m

kubectl rollout undo 可用于回退 Deployment 的历史 Pod 模板,但在清单被其他自动化工具持续回写、或标签被覆盖的环境中,不应把它当作唯一证据。回滚完成后仍须核对运行中的 imageID、就绪副本和业务冒烟结果。

防止复发的检查项

  1. 生产清单禁止 latest、stable、prod 等可变标签作为唯一镜像身份;使用 仓库/镜像@sha256:...。
  2. 在 CI 中记录“源代码提交 → 镜像 digest → 部署清单提交 → 运行 Pod digest”的链路,并在切流前做一致性校验。
  3. 不要把 imagePullPolicy: Always 当作版本治理方案。它能要求新建容器重新解析名称,却无法给旧实例、回滚和审计提供不可变身份。
  4. 用 readiness、maxUnavailable、副本冗余和业务冒烟测试控制滚动风险;digest 一致只能证明运行版本一致,不能证明功能正确。
  5. 若组织要求镜像来源可信,再叠加镜像签名、漏洞扫描和准入策略;这些能力与 digest 固定互补而非替代。

总结

“已重启”不是“已运行新代码”的证据。先用 Deployment 模板确认期望镜像和拉取策略,再用每个 Pod 的 imageID 确认实际 digest,最后把该检查纳入切流门禁。将 Deployment 改为 digest 引用后,发布和回滚都能精确指向同一份镜像内容,节点缓存与标签覆盖不再让版本状态漂移。

参考资料