Kubernetes 基础体系 · 第 64/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Kubernetes API 弃用治理:发现、迁移、兼容测试和升级门禁

Kubernetes API 弃用治理解决的不是“把 YAML 里的版本字符串改掉”这一件事,而是保证以下链路在集群升级后仍然成立:

源码与模板
   ↓
渲染后的清单
   ↓
客户端请求
   ↓
kube-apiserver 发现、认证、准入与转换
   ↓
etcd 中的持久化对象
   ↓
控制器、Webhook、运维脚本读取并更新对象

任何一个环节仍然依赖即将移除的 API,都可能在升级后产生失败。典型结果包括:

  • kubectl apply 返回 no matches for kind
  • 控制器启动后无法 list/watch 某个资源;
  • Validating 或 Mutating Webhook 因 rules 仍匹配旧版本而失效;
  • 旧对象暂时还能读取,但无法再通过旧版本更新;
  • 升级成功,业务发布却因 Helm 模板渲染出旧 apiVersion 而失败;
  • CRD 的存量对象仍以旧存储版本保存,后续转换 Webhook 或控制器升级后出现兼容问题。

因此,治理对象应定义为:

在目标 Kubernetes 版本集合中,所有客户端、清单、控制器、Webhook、存量对象和运维工具对 API 的使用都满足发现、语义、存储和运行时兼容条件。


一、先建立 API 版本模型:GVK、资源路径与对象存储

1. GVK 不是一个字符串,而是对象类型标识

Kubernetes 对象的 GVK 是 Group、Version、Kind 的组合:

  • Group:API 组,例如 appsnetworking.k8s.io
  • Version:组内版本,例如 v1v1beta1
  • Kind:对象类型,例如 DeploymentIngress

清单中的:

apiVersion: apps/v1
kind: Deployment

对应:

Group = apps
Version = v1
Kind = Deployment

核心资源的 apiVersion 可能没有显式 Group。例如:

apiVersion: v1
kind: ConfigMap

这里的 Group 是核心组,通常表示为空字符串,而不是名为 core 的普通 API 组。

GVK 用于识别“对象是什么类型”,但客户端访问 HTTP API 时通常还会涉及 GVR,即 Group、Version、Resource:

apps/v1        → GVR 的 group=apps, version=v1
deployments    → resource=deployments

例如:

/apis/apps/v1/namespaces/default/deployments

其中:

  • apps 是 API Group;
  • v1 是 API Version;
  • deployments 是复数资源名;
  • URL 中的资源名不一定等于 Kind 的小写形式,真实资源名应以 Discovery 为准。

kubectl api-resources 展示的就是 Kind、短名称、Namespaced 属性和资源名之间的映射。

2. Metadata、Spec 和 Status 的职责不同

一个典型 Kubernetes 对象可以抽象为:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: default
  labels:
    app: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: web
        image: nginx:1.27
status:
  availableReplicas: 3

三个区域的性质不同:

  • metadata 描述对象身份和管理信息,例如 namenamespaceuidresourceVersionlabelsannotationsownerReferences
  • spec 是用户或控制器声明的期望状态;
  • status 是控制器或 API Server 维护的观测状态。

迁移清单时,不能简单地把整个对象原样复制。原因包括:

  1. metadata.resourceVersion 是并发控制字段,不应被当作新的对象版本提交;
  2. metadata.uidcreationTimestamp 等字段描述已有对象身份;
  3. status 通常由控制器写入,用户提交它可能造成冲突或被忽略;
  4. API 版本变化往往不仅改变 apiVersion,还会改变 spec 的字段结构和必填约束。

因此,迁移通常以“保留用户意图的声明式对象”为目标,而不是复制 API Server 返回的完整快照。

3. 三个“版本”必须分开讨论

API 弃用治理至少要区分以下版本:

API 版本

例如:

extensions/v1beta1
networking.k8s.io/v1

它决定请求如何被解析、验证和转换。

Kubernetes 集群版本

例如:

v1.21
v1.22
v1.30

它决定 kube-apiserver、控制器和内置资源支持哪些 API 版本。

对象存储版本

API Server 可以对外提供多个版本,但通常会选择一个内部存储版本写入 etcd。客户端请求的版本不一定就是 etcd 中的版本。

因此,下面两个事实可以同时成立:

  1. 客户端请求 networking.k8s.io/v1
  2. etcd 中对象仍以某个内部存储版本保存。

API Server 负责在外部版本与内部版本之间进行转换。kubectl get -o yaml 展示的是请求版本经过转换后的对象,不等价于“etcd 原始字节”。

这一区分直接影响迁移判断:

“现在还能读取”不等于“所有客户端都已迁移”,也不等于“存量对象已经完成存储版本迁移”。


二、弃用、停止服务和删除是三个不同阶段

一个 API 版本可能经历以下状态:

正常提供
  ↓
标记弃用,但仍 Served
  ↓
停止 Served
  ↓
必要时完成存储版本迁移

1. 弃用通常先表现为 Warning

当客户端请求已弃用但仍提供服务的 API 时,API Server 可以返回 HTTP Warning 响应头,常见内容类似:

Warning: 299 - "apps/v1beta1 is deprecated ..."

kubectl 可能将其显示为警告。这个阶段请求通常仍能成功,但它已经是迁移信号,不是可以忽略的普通日志。

Kubernetes 官方弃用策略会规定不同稳定级别 API 的弃用和移除约束,但具体移除版本必须以目标 Kubernetes 版本的发布说明和弃用 API 文档为准。不能根据某个旧版本文章推断当前集群一定还提供某个版本。

2. Served 与 Storage 是两个独立开关

对 CRD 来说,版本配置通常具有:

spec:
  versions:
  - name: v1
    served: true
    storage: true
  - name: v1beta1
    served: false
    storage: false

含义是:

  • served: true:API Server 对外接受这个版本的请求;
  • storage: true:这个版本作为该 CRD 的存储版本;
  • 同一个 CRD 必须且只能有一个存储版本;
  • 设置新的版本为 storage: true,不会自动把 etcd 中所有旧对象立即重写一遍。

因此,下面的推理是错误的:

“我把 CRD 的 storage 改成 v1,所以旧对象已经全部迁移成 v1。”

真实情况是:

  1. 新写入对象可能使用新的存储版本;
  2. 旧对象仍可能保留在旧版本的存储形式;
  3. 只有经过读取再写入、专门的存储迁移工具或控制器处理,旧对象才可能被重写;
  4. 在确认存量对象迁移完成前,不能安全删除旧版本的转换路径。

对于内置资源,存储版本由 API Server 和集群实现决定,不能直接把“修改 CRD 版本”的方法套用于 Deployment、Ingress 等内置资源。

3. “资源还存在”不等于“旧 API 还存在”

以下命令:

kubectl get ingress -A

只说明 kubectl 找到了某个当前可用的 Ingress 资源映射。它不能证明:

  • extensions/v1beta1 仍然 Served;
  • 仓库中的 Helm 模板没有旧版本;
  • 控制器没有通过旧 GVK 注册 Informer;
  • Webhook 没有匹配旧版本;
  • etcd 中没有旧存储版本对象。

API 兼容性必须按调用方和 API 版本分别验证。


三、发现阶段:建立完整的 API 使用清单

发现的目标不是只扫描 YAML,而是建立如下关系:

调用方 → GVK/GVR → 使用方式 → 当前集群状态 → 目标集群状态 → 迁移动作

1. 先从 API Server Discovery 观察真实能力

kubectl api-versions 列出当前 API Server 对外提供的 Group/Version:

kubectl api-versions

示例输出可能包含:

apps/v1
batch/v1
networking.k8s.io/v1
policy/v1

kubectl api-resources 进一步显示资源名称和作用域:

kubectl api-resources

典型输出结构:

NAME         SHORTNAMES   APIVERSION             NAMESPACED   KIND
deployments  deploy       apps/v1                true         Deployment
ingresses     ing         networking.k8s.io/v1   true         Ingress
nodes                      v1                     false        Node

只查看可列出的 Namespaced 资源:

kubectl api-resources \
  --verbs=list \
  --namespaced \
  -o name

查看集群级资源:

kubectl api-resources \
  --verbs=list \
  --namespaced=false \
  -o name

这里的前置条件是当前身份具有相应资源的 list 权限。没有权限时,命令失败不代表资源不存在;它只代表当前身份不能观察该资源。

2. 用 Discovery 验证单个版本是否真的 Served

例如检查 Ingress:

kubectl api-resources --api-group=networking.k8s.io

也可以直接访问 Discovery 端点:

kubectl get --raw /apis/networking.k8s.io/v1 | jq .

对于核心组:

kubectl get --raw /api/v1 | jq .

如果某个版本不在 Discovery 响应中,面向该版本的请求通常会失败。但 Discovery 本身也受 API Server 版本、聚合 API、权限和升级过程影响,因此它是“当前观察结果”,不是对未来版本的承诺。

3. 扫描仓库时,必须扫描渲染结果

只执行:

grep -R "extensions/v1beta1" .

是不充分的,因为旧版本可能来自:

  • Helm 模板;
  • Kustomize patch;
  • Jsonnet;
  • Operator 生成器;
  • CI/CD 中动态拼接;
  • GitOps 控制器在集群内渲染;
  • 第三方 chart 或远程 base。

Helm 应先渲染,再扫描:

helm template web ./chart \
  --namespace production \
  -f values-production.yaml \
  > /tmp/web-rendered.yaml

grep -nE 'apiVersion:|extensions/v1beta1|networking.k8s.io/v1beta1' \
  /tmp/web-rendered.yaml

对于已经部署的对象,可以查看 API Server 返回的当前版本:

kubectl get ingress web -n production -o yaml

但这只能发现当前对象,不能发现“创建它的客户端曾经使用过什么版本”。要补充检查:

  • CI/CD 任务日志;
  • Operator 或控制器镜像版本;
  • Webhook 配置;
  • 自定义脚本中的 kubectl、REST URL 和客户端库;
  • 审计日志;
  • API Server 的弃用请求指标。

4. 使用弃用指标识别运行时调用方

常见 Kubernetes 实现会暴露弃用 API 请求指标,例如:

apiserver_requested_deprecated_apis

查询方式取决于监控系统,Prometheus 中可以按集群实际标签查询:

apiserver_requested_deprecated_apis > 0

该指标通常能帮助发现仍在请求弃用 API 的服务,并可能包含 group、version、resource、removed_release 等标签。标签名称和指标可用性应以当前 Kubernetes 版本和发行版暴露的指标为准。

指标的局限也很重要:

  • 没有流量就没有样本;
  • 只扫描清单但尚未部署的旧 API 不会出现在指标中;
  • 请求可能被缓存,不能据此证明所有调用方已消失;
  • 托管 Kubernetes 可能隐藏部分控制面指标。

因此,静态扫描和运行时观测必须同时存在。

5. 用弃用请求警告定位请求来源

对仍然 Served 的弃用 API 发起请求时,客户端可能收到 Warning。可以从以下位置收集:

  • CI 日志;
  • 控制器日志;
  • API Gateway 或审计系统;
  • kubectl 执行输出;
  • 集群监控告警。

如果请求由客户端库发起,日志可能只记录“list failed”,而没有完整 Warning。此时应结合:

  1. API Server 审计日志中的 userAgent
  2. ServiceAccount;
  3. source IP;
  4. 请求的 API path;
  5. 控制器 Deployment、Job 或 Pod 的归属关系。

这样才能从“有旧 API 请求”进一步定位到“哪个发布单元需要升级”。


四、迁移阶段:从旧 GVK 映射到新 GVK 和新字段语义

API 迁移至少包含三项工作:

  1. GVK 迁移;
  2. 字段结构迁移;
  3. 行为和权限迁移。

只改 apiVersion,往往只能完成第一项。

1. 完整算例:Ingress 从旧版本迁移到 networking.k8s.io/v1

旧版本 Ingress 常见写法如下:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: web
  namespace: production
spec:
  rules:
  - host: web.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: web
          servicePort: 8080

迁移到 networking.k8s.io/v1

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web
  namespace: production
spec:
  ingressClassName: nginx
  rules:
  - host: web.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: web
            port:
              number: 8080

变化不是单一版本替换:

旧写法 新写法 迁移原因
extensions/v1beta1 networking.k8s.io/v1 API 组和版本变化
serviceName service.name 后端结构变化
servicePort service.port.number 端口字段结构变化
pathType pathType: Prefix 新版本要求明确路径匹配语义
可能由注解决定 Class ingressClassName 推荐使用结构化字段表达 IngressClass

pathType 不是可随意填的占位字段:

  • Exact 表示精确匹配;
  • Prefix 表示按路径段前缀匹配;
  • ImplementationSpecific 将行为交给 Ingress Controller 实现。

例如,旧控制器对 /foo 的匹配行为可能与新版本 Prefix 的路径段规则不同。迁移后必须验证实际路由,而不仅是 API Server 接受了对象。

执行迁移前先渲染并检查:

kubectl diff \
  --server-side \
  --field-manager=api-migration \
  -f ingress-v1.yaml

然后进行服务端试运行:

kubectl apply \
  --server-side \
  --dry-run=server \
  --field-manager=api-migration \
  -f ingress-v1.yaml

参数含义:

  • --dry-run=server:请求发送到 API Server,但不持久化;
  • 服务端会执行 API 解析、schema 校验和适用的准入流程;
  • --server-side:使用 Server-Side Apply 的字段管理语义;
  • --field-manager:为字段所有权记录提供稳定名称。

通过后再实际应用:

kubectl apply \
  --server-side \
  --field-manager=api-migration \
  -f ingress-v1.yaml

风险包括:

  • Server-Side Apply 可能报告字段所有权冲突;
  • Webhook 可能修改对象;
  • Ingress Controller 可能需要支持新 API 和新字段;
  • 旧对象中的注解可能仍产生行为影响;
  • 业务流量可能因路径或默认 Class 改变而路由到不同后端。

验证应同时检查对象和行为:

kubectl get ingress web -n production -o yaml
kubectl describe ingress web -n production
kubectl get endpointslice -n production \
  -l kubernetes.io/service-name=web

还应通过真实或合成请求验证:

curl -i -H 'Host: web.example.com' http://<ingress-address>/

2. 不要把服务端转换当作完成迁移

如果旧 API 仍然 Served,下面的命令可能成功:

kubectl apply -f old-ingress.yaml

这只证明当前 API Server 能接收旧版本并进行转换,不证明目标版本仍然接受它。

真正的迁移应达到:

源码模板 → 新 GVK
客户端库 → 新 GVK
控制器 Informer → 新 GVK
Webhook rules → 新 GVK
RBAC apiGroups/resources → 兼容目标调用方式
运行时对象 → 已通过新版本读写验证

例如,控制器如果仍然注册:

extensions/v1beta1, Ingress

即使仓库中的 YAML 已经改成 networking.k8s.io/v1,控制器也可能在目标集群中启动失败,因为它仍对已删除的 GVR 执行 list/watch。

3. CRD 迁移需要考虑转换 Webhook

CRD 可以同时提供多个版本。若版本间结构不同,需要配置转换策略:

  • None:结构基本兼容,API Server 进行简单转换;
  • Webhook:由用户提供转换服务。

转换 Webhook 的关键问题是:

  1. API Server 是否能访问 Webhook Service;
  2. TLS 证书是否有效;
  3. Webhook 是否能够处理所有声明的版本;
  4. 转换是否保持对象语义;
  5. 失败时 failurePolicy 如何影响请求;
  6. Webhook 自己依赖的 API 是否会在升级中消失。

Webhook 转换失败时,可能导致 CRD 对象无法读取或写入。若转换 Webhook 的 Service、证书或 Deployment 在升级期间不可用,影响面可能比单个业务发布更大。

CRD 迁移不能只修改:

spec.versions[].name

还要检查:

  • 每个版本的 OpenAPI v3 schema;
  • servedstorage
  • status.storedVersions
  • 转换 Webhook 的 conversionReviewVersions
  • 现有对象是否已被重新写入;
  • 控制器客户端是否支持新版本。

查看 CRD 状态:

kubectl get crd widgets.example.com -o yaml

输出中的 status.storedVersions 表示 API Server 认为对象曾以哪些存储版本存在。它不是“所有对象当前都已经完成迁移”的充分证明,但可以作为迁移核查信号。


五、兼容测试:验证版本、结构和行为三个层次

兼容测试不能只依赖 kubectl apply --dry-run=client

1. 客户端试运行与服务端试运行的差异

客户端试运行:

kubectl apply --dry-run=client -f manifest.yaml

主要检查本地命令行能够解析清单。它可能无法反映:

  • 目标 API Server 是否 Served 该 GVK;
  • 服务端 schema 是否接受字段;
  • ValidatingWebhook 是否拒绝对象;
  • MutatingWebhook 会如何修改对象;
  • 当前身份是否有权限;
  • Server-Side Apply 是否产生字段冲突。

服务端试运行:

kubectl apply \
  --dry-run=server \
  --server-side \
  --field-manager=ci \
  -f manifest.yaml

更接近真实提交,但它仍不是完整业务测试,因为:

  • 不会持久化对象;
  • 不会完整模拟控制器异步调谐;
  • 不会证明节点、Ingress Controller、CSI 等数据面组件支持该对象;
  • 目标集群必须真实运行对应 API 版本;
  • 生产准入策略可能与测试集群不同。

2. 兼容性矩阵应覆盖支持范围

假设组织支持三个集群版本:

v1.28、v1.29、v1.30

至少应建立如下矩阵:

检查项 v1.28 v1.29 v1.30
所有 GVK 被 Discovery 发现
清单服务端试运行通过
Webhook 接收并返回成功
控制器 list/watch 成功
业务对象达到期望状态
旧版本请求为零 迁移后 迁移后 迁移后

这不是要求所有旧 API 在所有版本都可用,而是要求发布产物使用的 API 位于支持矩阵的交集内。

设:

  • GG 为发布系统可能生成的 GVK 集合;
  • S(v)S(v) 为 Kubernetes 版本 vv 当前 Served 的 GVK 集合;
  • TT 为组织支持的集群版本集合。

仅从 API 发现角度,发布可以通过的必要条件是:

GvTS(v)G \subseteq \bigcap_{v \in T} S(v)

直觉是:每个发布产物中的 GVK,都必须在每一个受支持的目标集群版本中存在。

但这只是必要条件,不是充分条件。还需要满足:

C(g,v)=1C(g,v)=1

其中 C(g,v)C(g,v) 表示 GVK gg 在版本 vv 上不仅能被解析,而且:

  • schema 校验通过;
  • 准入 Webhook 接受;
  • 控制器能观察并调谐;
  • 业务行为符合预期。

因此更完整的门禁条件可以写成:

gG, vT:gS(v)C(g,v)=1\forall g \in G,\ \forall v \in T: \quad g \in S(v) \land C(g,v)=1

如果 API 版本迁移改变了字段语义,还要增加语义等价条件:

Bold(x)Bnew(m(x))B_{\text{old}}(x) \approx B_{\text{new}}(m(x))

其中:

  • xx 是旧版本对象;
  • m(x)m(x) 是迁移后的新版本对象;
  • BB 是控制器和数据面观察到的业务行为;
  • \approx 表示满足组织定义的等价关系,例如路由、滚动更新、权限和可用性不发生非预期变化。

3. 用多版本临时集群做真实服务端测试

CI 可以使用与目标 Kubernetes 版本对应的临时集群,例如 kind、minikube 或其他测试环境。核心流程如下:

# 具体镜像标签应根据测试平台和目标 Kubernetes 版本选择
kind create cluster --name api-compat --image <目标版本对应的 kind 节点镜像>

kubectl cluster-info
kubectl version

kubectl apply \
  --server-side \
  --dry-run=server \
  --field-manager=ci \
  -f rendered.yaml

kubectl apply \
  --server-side \
  --field-manager=ci \
  -f rendered.yaml

kubectl wait \
  --for=condition=available \
  deployment/web \
  --timeout=180s

这里的 <目标版本对应的 kind 节点镜像> 不是可以随意填写的固定值。测试系统应根据实际支持版本维护镜像映射,因为 kind 节点镜像、容器运行时和 Kubernetes 版本之间存在对应关系。

测试应覆盖两类资源:

静态 API 兼容

验证:

  • GVK 是否可发现;
  • YAML 是否通过服务端 schema;
  • 是否存在弃用 Warning;
  • 是否通过准入;
  • 是否有 RBAC 错误。

动态行为兼容

验证:

  • Deployment 是否产生预期 ReplicaSet 和 Pod;
  • Service 是否有 EndpointSlice;
  • Ingress 是否完成地址分配并正确路由;
  • Job 是否按预期结束;
  • PDB、HPA、NetworkPolicy 等关联对象是否仍具有原语义;
  • 控制器和 Webhook 是否持续运行。

只验证第一类,不能排除第二类故障。

4. Schema 工具可以提前发现错误,但不能替代 API Server

可以使用基于 Kubernetes OpenAPI schema 的工具检查渲染结果,例如在 CI 中引入 schema 校验工具。它们擅长发现:

  • 字段拼写错误;
  • 类型错误;
  • 不允许的字段;
  • 某版本资源不存在于指定 schema。

但工具使用的 schema 版本、CRD schema、Webhook 逻辑和云厂商差异可能与真实集群不同。因此推荐分层:

模板渲染
  ↓
静态 schema 检查
  ↓
目标版本 API Server dry-run
  ↓
实际 apply
  ↓
控制器与业务行为验证

每一层解决的问题不同,不应以某一层结果替代其他层。


六、升级门禁:把发现结果转化为可执行规则

升级门禁的作用是阻止“已知会在目标版本失败”的变更进入升级窗口,而不是阻止所有版本差异。

1. 门禁输入应来自四类数据

一次升级前至少收集:

  1. 目标版本清单:当前版本、目标版本、未来支持版本;
  2. 静态产物清单:所有 Helm、Kustomize、Operator 输出;
  3. 运行时请求清单:弃用指标、审计日志、控制器日志;
  4. 对象和 CRD 状态:当前 Served 版本、Storage 版本、存量迁移状态。

可以形成一张表:

来源 GVK/GVR 调用方 当前状态 目标版本状态 动作
Helm chart extensions/v1beta1/Ingress CI 发布 当前可用但弃用 不存在 阻断并迁移
Operator batch/v1beta1/CronJob controller 运行时调用 不存在 升级 Operator
CRD example.com/v1beta1 API Server Served=true 仍 Served 迁移客户端并规划移除
Webhook admissionregistration.k8s.io/v1beta1 admission 当前可用 不存在 修改配置

2. 推荐的门禁算法

可以把门禁实现为以下步骤:

第一步:计算目标版本的 Discovery 集合

对每个支持的 Kubernetes 版本 vv,得到 S(v)S(v)

第二步:解析交付产物

从渲染后的 YAML 中得到 GG。如果清单中包含未知或缺失 apiVersionkind,直接失败。

第三步:检查交集

计算:

D=GvTS(v)D = G - \bigcap_{v \in T} S(v)

如果 DD 非空,则说明至少有一个发布 GVK 不在所有目标版本中,门禁失败。

第四步:检查弃用窗口

即使 GVK 仍在交集内,如果目标版本已经将其标记为 Deprecated,也应产生高优先级告警,并根据组织策略决定:

  • 允许合并但要求创建迁移任务;
  • 只允许紧急修复;
  • 直接阻断新使用。

第五步:检查运行时调用

如果 apiserver_requested_deprecated_apis 在迁移窗口内仍有请求,则不能宣称迁移完成。应按 ServiceAccount 或 userAgent 定位调用方。

第六步:检查动态行为

至少在一个目标版本测试集群中完成 apply、调谐和业务验证。对于跨多个 Kubernetes 版本运行的产品,则应在每个支持版本上执行关键测试。

3. 一个简单的 CI 门禁示意

下面的命令表达了门禁思想:

set -euo pipefail

helm template web ./chart \
  -f values-ci.yaml \
  > rendered.yaml

if grep -nE \
  'extensions/v1beta1|apps/v1beta1|apps/v1beta2|batch/v1beta1|policy/v1beta1|networking.k8s.io/v1beta1' \
  rendered.yaml; then
  echo "发现禁止使用的 Kubernetes API 版本" >&2
  exit 1
fi

kubectl apply \
  --server-side \
  --dry-run=server \
  --field-manager=ci \
  -f rendered.yaml

kubectl diff \
  --server-side \
  --field-manager=ci \
  -f rendered.yaml

这个脚本适合作为一个初级门禁,但它有明显边界:

  • 版本列表是人工维护的;
  • 只扫描当前文件,不扫描控制器二进制中的 GVK;
  • 不能识别所有动态生成的请求;
  • grep 可能误报注释或文档;
  • 它依赖当前 kubectl 指向的集群,而不是自动验证所有目标版本。

生产级门禁应使用结构化 YAML 解析、目标版本 Discovery、渲染矩阵和运行时指标,而不是只依赖正则表达式。


七、升级过程中的 Version Skew 与弃用风险

Version Skew 是 Kubernetes 组件之间的版本偏差。它不是“只要客户端版本不一样就一定失败”,而是需要遵守 Kubernetes 对控制面、节点、kubectl、控制器和扩展组件规定的兼容范围。

1. API Server 不是唯一需要升级的组件

一个 API 迁移可能涉及:

kube-apiserver
kube-controller-manager
kube-scheduler
kubelet
kubectl / 客户端库
Operator
Admission Webhook
Ingress Controller
CSI、CNI 等扩展组件

例如:

  • API Server 已移除旧 GVK,但 Operator 还在使用旧客户端;
  • API Server 接受新 GVK,但旧 Ingress Controller 不理解新字段;
  • Webhook 配置使用新 API,但 Webhook 服务程序只支持旧 AdmissionReview 版本;
  • kubectl 支持的资源类型与集群 Discovery 不一致;
  • 升级过程中 HA API Server 节点版本暂时不同,客户端观察到的 Discovery 结果短时间不一致。

升级期间不应把某一次 kubectl 成功看作整个 HA 控制面的长期保证。应使用目标版本的支持客户端,并在控制面升级完成后重新执行 Discovery、dry-run 和行为验证。

2. 先迁移消费者,再移除提供者

通常更安全的顺序是:

升级或改造客户端/控制器/Webhook
        ↓
迁移源码、模板和运行时对象
        ↓
观察弃用请求为零
        ↓
验证目标版本兼容性
        ↓
升级控制面
        ↓
升级节点和扩展组件

这里的“消费者”包括所有发起 API 请求的组件,不只是发布 YAML 的 CI。

但迁移顺序并非所有资源都一样:

  • 内置 API 通常由 Kubernetes 控制面提供转换;
  • CRD 需要先准备版本、schema 和转换 Webhook;
  • Webhook 本身必须先支持目标 AdmissionReview 和对象版本;
  • Operator 往往需要先升级到支持目标集群版本的版本;
  • 托管 Kubernetes 可能由云厂商控制控制面升级时间和可用 API 集合。

八、常见失败表现与诊断路径

1. no matches for kind

错误示例:

error: resource mapping not found for name: web namespace: production
no matches for kind "Ingress" in version "extensions/v1beta1"

诊断顺序:

kubectl api-versions | grep -E 'extensions|networking.k8s.io'
kubectl api-resources | grep -i ingress
kubectl explain ingress --api-version=networking.k8s.io/v1

如果新版本存在,应迁移 GVK 和字段结构;如果新版本也不存在,可能是:

  • 当前上下文连接到了错误集群;
  • API Group 写错;
  • CRD 未安装;
  • 聚合 API 服务异常;
  • 权限或 Discovery 缓存出现问题。

2. the server does not allow this method

常见原因是请求方法、资源作用域或子资源路径不符合目标 API。应检查:

kubectl api-resources -o wide
kubectl auth can-i list deployments -n production
kubectl auth can-i update deployments/status -n production

API 版本迁移可能改变了客户端访问的资源或子资源,RBAC 规则也必须随之核对。RBAC 的 apiGroupsresources 不是根据 Kind 自动转换的。

3. Webhook 阻断所有写请求

常见错误:

failed calling webhook
context deadline exceeded

诊断:

kubectl get mutatingwebhookconfiguration,validatingwebhookconfiguration
kubectl get svc,endpoints -n <webhook-namespace>
kubectl logs -n <webhook-namespace> deploy/<webhook-deployment>

重点检查:

  • Webhook rules 是否仍引用旧 API 版本;
  • clientConfig.service 是否能解析;
  • TLS CA 与服务端证书是否匹配;
  • timeoutSeconds 是否过短;
  • failurePolicy 是否为 Fail
  • 升级期间 Webhook Deployment 是否与 API Server 同时不可用。

failurePolicy: Ignore 可以降低升级时的阻断风险,但会降低准入保证,不能无条件当作修复方案。生产上应根据校验的重要性、升级窗口和回退路径做取舍。

4. 对象能读,控制器却不工作

可能出现:

unable to recognize ...
the server could not find the requested resource
failed to list ...

这通常说明:

  • kubectl 使用的是新版本,但控制器仍使用旧 GVR;
  • 控制器的 discovery cache 过期;
  • RBAC 缺少目标 API Group 或资源权限;
  • CRD 的转换 Webhook 失败;
  • 扩展控制器不支持目标版本。

诊断应从控制器日志中的 list/watch 路径、ServiceAccount 和镜像版本入手,而不是只查看对象是否存在。

5. 迁移后对象被意外重置

使用完整 YAML 回写对象时,可能产生:

  • 字段所有权冲突;
  • 省略字段被清除;
  • 默认值变化;
  • 注解和标签被覆盖;
  • statusspec 混写;
  • 多个控制器互相争夺字段。

建议:

  1. 从声明式源文件重新生成迁移后的清单;
  2. 不直接把 kubectl get -o yaml 的完整输出当作新源文件;
  3. 先执行 kubectl diff --server-side
  4. 对重要对象使用独立字段管理器;
  5. 检查控制器日志和事件;
  6. 对生产资源先在灰度命名空间验证。

九、生产升级前后的验证与恢复

1. 升级前检查

升级前至少确认:

kubectl version
kubectl get nodes
kubectl get --raw /version | jq .
kubectl api-versions
kubectl api-resources

然后检查:

  • 所有发布产物已完成目标版本 dry-run;
  • 弃用 API 请求指标在观察窗口内为零或有明确豁免;
  • Operator、Webhook、Ingress、CSI、CNI 等扩展组件支持目标版本;
  • CRD 已完成版本规划;
  • 关键对象和集群状态已经备份;
  • 升级后验证脚本可以独立运行;
  • 业务有明确的流量和功能探针。

备份的价值不仅是恢复业务 YAML,还包括理解对象关系、CRD 定义、RBAC、Webhook 和控制器配置。单纯备份几个 Deployment,无法还原完整控制面依赖。

2. 升级后检查

升级后重新执行 Discovery,因为 API 集合可能已经改变:

kubectl api-versions
kubectl api-resources

检查控制器和节点:

kubectl get nodes
kubectl get pods -A
kubectl get events -A --sort-by=.lastTimestamp

检查关键对象:

kubectl get deployment,service,ingress -A
kubectl get crd

检查运行时弃用请求:

sum by (group, version, resource) (
  apiserver_requested_deprecated_apis
)

随后执行真实业务探针,例如:

  • HTTP 请求和状态码;
  • 消息队列生产与消费;
  • Job 完成;
  • 数据库连接;
  • Ingress 路由;
  • 自动扩缩容;
  • 网络策略;
  • 持久卷挂载和读写。

3. 回滚不能假设为普通版本回退

API 被移除后,回滚具有特殊风险:

  1. 新版本对象可能使用旧 API Server 不认识的字段;
  2. 新版本的默认值或字段语义可能已发生变化;
  3. 存储版本迁移可能使降级后的组件无法读取对象;
  4. 控制面降级本身通常不等价于业务版本回滚;
  5. 托管 Kubernetes 往往不支持用户任意降级控制面。

因此,升级回滚应优先设计为:

控制面升级前保留稳定版本的业务发布产物
        ↓
升级后发现问题
        ↓
回滚业务变更、流量和扩展组件
        ↓
必要时恢复备份或按发行版支持的灾难恢复流程处理

而不是临时把 apiVersion 改回旧版本。旧版本 API 已经被移除时,这个操作必然失败;即便旧 API 仍存在,也可能触发字段丢失或语义变化。


十、云厂商差异和真实边界

Kubernetes API 的基本对象模型、Discovery、API Server 转换和版本生命周期由 Kubernetes 规范定义,但生产环境仍可能存在发行版差异:

  • 云厂商控制面升级窗口由平台决定;
  • 某些控制面指标、审计日志或 API Server 参数不可见;
  • 云厂商预装的 Ingress、LoadBalancer、CSI 和 Webhook 可能有独立兼容矩阵;
  • 托管服务可能提前禁用或延后提供某些 API;
  • Kubernetes 发行版可能携带额外的 API 组;
  • 聚合 API Server 的 Discovery 和可用性不一定由主 API Server 单独决定。

所以目标版本不能只写成“上游 Kubernetes v1.x”,还应记录:

发行版/云厂商
控制面版本
节点版本
预装扩展组件版本
CRD 版本
Webhook 版本
支持的 kubectl 和客户端库范围

对于生产升级,最终兼容结论应来自目标发行版的实际测试,而不是只根据上游 YAML 示例推断。


十一、一个可执行的治理闭环

可以把整个过程收敛为以下状态机:

stateDiagram-v2
    [*] --> Inventory: 收集清单、运行时请求和组件
    Inventory --> Classified: 区分 Served、Deprecated、Storage
    Classified --> Migration: 生成新 GVK 与字段映射
    Migration --> StaticTest: 渲染和 schema 检查
    StaticTest --> ServerDryRun: 目标集群服务端试运行
    ServerDryRun --> BehaviorTest: 部署并验证控制器行为
    BehaviorTest --> RuntimeObserve: 观察弃用请求和错误
    RuntimeObserve --> GatePass: 满足目标版本门禁
    RuntimeObserve --> Migration: 仍有旧调用或语义错误
    GatePass --> Upgrade: 执行控制面和节点升级
    Upgrade --> PostCheck: 重新 Discovery、验证业务
    PostCheck --> [*]
    PostCheck --> Recovery: 发现不可接受故障
    Recovery --> PostCheck: 按支持的恢复方案处理

关键路径不是“文件改完就升级”,而是:

  1. Inventory:发现所有静态和运行时 API 使用;
  2. Classified:判断 API 是正常、弃用、停止 Served 还是存储版本问题;
  3. Migration:同时迁移 GVK、字段和依赖组件;
  4. ServerDryRun:在真实 API Server 上验证结构、准入和权限;
  5. BehaviorTest:验证控制器和数据面行为;
  6. RuntimeObserve:确认旧调用已消失;
  7. GatePass:将结论固化为升级门禁;
  8. PostCheck:升级后重新发现和验证;
  9. Recovery:按预先验证过的恢复流程处理,而不是临时降级。

十二、最终判断标准

一次 API 弃用迁移只有在以下条件同时满足时,才可以认为完成:

  • 发布仓库和所有渲染产物不再生成目标版本不支持的 GVK;
  • 控制器、Operator、Webhook 和脚本不再调用已弃用或即将移除的 GVR;
  • 所有目标 Kubernetes 版本都能通过服务端 dry-run;
  • API 版本变化涉及的字段语义已经验证;
  • CRD 的 servedstorage、转换 Webhook 和存量对象状态一致;
  • RBAC、审计、监控和准入规则已同步检查;
  • 升级后 API Discovery 与升级前假设一致;
  • 关键业务行为、扩展组件和数据面功能通过验证;
  • 回滚或恢复路径不依赖已经被移除的旧 API。

可以把核心原则压缩成一句话:

API 弃用治理的完成标志,不是 YAML 中出现了新的 apiVersion,而是目标版本 API Server、客户端、控制器、Webhook、存储对象和业务行为共同证明迁移已经成立。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。