3. CRD 版本控制原理

CRD(Custom Resource Definition)的版本控制是 Operator 开发中最容易被忽视但影响深远的环节。当自定义资源从 v1alpha1 演进到 v1beta1 再到 v1,或者需要同时对外暴露多个版本时,Kubernetes 的版本管理机制决定了资源数据的存储形态、对外接口以及版本间的数据迁移方式。理解 CRD 版本控制的核心三要素——版本定义、存储版本与服务版本、版本转换——是编写生产级 Operator 的必备能力。

3.1 CRD 版本定义

每个 CRD 可以声明一个或多个版本(versions),每个版本包含独立的 Schema(OpenAPI v3 结构)、子资源(subresources)以及附加的校验规则。Kubernetes 要求 CRD 必须至少声明一个版本,且所有版本必须共享同一个 group 和 kind。

字段 说明 是否必填
name 版本名称,如 v1alpha1、v1beta1、v1 是
served 是否作为服务版本对外暴露 是
storage 是否作为存储版本,整个 CRD 只能有一个版本标记为 storage: true 是
schema 该版本的 OpenAPI 结构定义 是
subresources 是否启用 /status 和 /scale 子资源 否
additionalPrinterColumns 自定义 kubectl 输出列 否
关键约束: 一个 CRD 中最多只能有一个版本设置 storage: true。如果未显式指定,默认第一个声明的版本即为存储版本。一旦 CRD 创建后,存储版本不可更改(除非通过 CRD 更新流程并配合转换 Webhook)。
# 一个声明了三个版本的 CRD 示例(简化)
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: applications.example.com
spec:
  group: example.com
  names:
    kind: Application
    plural: applications
  versions:
  - name: v1alpha1
    served: true
    storage: false
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              replicas:
                type: integer
  - name: v1beta1
    served: true
    storage: false
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              replicas:
                type: integer
              version:
                type: string
  - name: v1
    served: true
    storage: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              replicas:
                type: integer
              version:
                type: string
              env:
                type: string

3.2 存储版本与服务版本

这是 CRD 版本控制中最核心的概念区分。存储版本决定了资源数据在 etcd 中以何种结构持久化;服务版本决定了用户通过 API 读写资源时使用哪个版本的 Schema。两者可以相同,也可以不同。

存储版本(Storage Version): 所有版本的资源对象在写入 etcd 时,都会被转换为存储版本的格式。即使你通过 v1alpha1 API 创建资源,etcd 中存储的也是 v1 版本的数据结构。读取时,Kubernetes API Server 会自动将存储版本的数据转换为请求的服务版本返回给客户端。
服务版本(Served Version): 标记为 served: true 的版本对外暴露 API 端点。用户可以通过 /apis/example.com/v1alpha1/ 或 /apis/example.com/v1/ 等路径访问资源。服务版本可以同时存在多个,但每个版本必须提供完整的 Schema 定义。
存储版本 vs 服务版本 数据流 客户端 API Server etcd(存储版本) v1alpha1 请求 转换为 v1 存储 读取 v1 数据 转换为 v1alpha1 返回 数据流说明: 1. 客户端通过 v1alpha1 API 创建资源 → API Server 收到请求 2. API Server 将 v1alpha1 数据转换为存储版本(v1)的格式 → 写入 etcd 3. 客户端通过 v1alpha1 API 读取资源 → API Server 从 etcd 读取 v1 数据 4. API Server 将 v1 数据反向转换为 v1alpha1 格式 → 返回给客户端

3.3 版本转换(Conversion)机制

当存储版本与服务版本不同时,Kubernetes 需要一种机制在两者之间进行数据转换。CRD 支持两种转换方式:None(无转换) 和 Webhook(转换 Webhook)。

转换方式 适用场景 实现复杂度 性能影响
None 所有服务版本的 Schema 与存储版本完全一致(字段名、类型、结构相同) 无 无
Webhook 服务版本与存储版本存在字段差异、重命名、结构调整 高(需编写 HTTPS 服务) 每次读写触发 Webhook 调用
常见陷阱: 很多开发者误以为只要声明了多个版本,Kubernetes 就会自动处理字段映射。实际上,如果版本间 Schema 不同且未配置转换 Webhook,API Server 在尝试将非存储版本的数据写入 etcd 时会直接报错。只有 None 策略下且所有版本 Schema 完全一致时才能正常工作。

3.3.1 转换 Webhook 的工作原理

转换 Webhook 是一个 HTTPS 端点,由 Operator 开发者自行实现并部署。当 API Server 需要将资源对象从一个版本转换为另一个版本时,会向 Webhook 发送 ConversionReview 请求,Webhook 返回转换后的对象。

# CRD 中声明转换 Webhook
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: applications.example.com
spec:
  conversion:
    strategy: Webhook
    webhook:
      conversionReviewVersions: ["v1", "v1beta1"]
      clientConfig:
        service:
          namespace: operator-system
          name: conversion-webhook
          path: /convert
        caBundle: <base64-encoded-ca-cert>
  versions:
  - name: v1alpha1
    served: true
    storage: false
    # ...
  - name: v1
    served: true
    storage: true
    # ...
ConversionReview 请求/响应结构: Webhook 接收的请求中包含 request.objects(需要转换的对象列表),响应中必须包含 response.convertedObjects(转换后的对象列表)。如果转换失败,返回 response.result.status: Failure 并附带错误信息。
// ConversionReview 请求示例(Go 结构)
type ConversionReview struct {
  APIVersion string `json:"apiVersion"`
  Kind       string `json:"kind"`
  Request    *ConversionRequest `json:"request,omitempty"`
  Response   *ConversionResponse `json:"response,omitempty"`
}

type ConversionRequest struct {
  UID               string                `json:"uid"`
  DesiredAPIVersion string                `json:"desiredAPIVersion"`
  Objects           []runtime.RawExtension `json:"objects"`
}

type ConversionResponse struct {
  UID              string                `json:"uid"`
  ConvertedObjects []runtime.RawExtension `json:"convertedObjects"`
  Result           StatusResult           `json:"result"`
}
转换 Webhook 调用流程 API Server 转换 Webhook etcd 1. ConversionReview 请求 2. 返回转换后的对象 3. 写入存储版本到 etcd Webhook 转换关键步骤: 1. 客户端通过 v1alpha1 API 创建/更新资源 2. API Server 发现存储版本为 v1,需要转换 3. API Server 构造 ConversionReview 请求,包含 v1alpha1 对象 4. Webhook 将 v1alpha1 转换为 v1 格式,返回 ConversionReview 响应 5. API Server 将转换后的 v1 对象写入 etcd 6. 读取时反向转换:etcd → v1 → Webhook → v1alpha1 → 客户端

3.3.2 版本转换的常见模式

  • 字段重命名: v1alpha1 中的 replicaCount 在 v1 中改为 replicas,Webhook 负责字段映射。
  • 字段类型变更: v1alpha1 中 version 为字符串,v1 中改为枚举结构体,Webhook 需要做类型转换。
  • 字段废弃与移除: v1alpha1 中的废弃字段在 v1 中删除,Webhook 在转换时丢弃该字段。
  • 默认值注入: v1 新增字段在 v1alpha1 中不存在,Webhook 在反向转换(v1 → v1alpha1)时移除该字段;正向转换时注入默认值。
最佳实践: 始终将最稳定、字段最完整的版本设为存储版本(通常是 v1)。早期版本(v1alpha1、v1beta1)作为服务版本对外暴露,通过 Webhook 实现向前兼容。这样即使未来废弃早期版本,etcd 中的数据结构无需迁移。

3.4 版本演进策略与存储版本迁移

当 CRD 需要变更存储版本时(例如从 v1beta1 升级到 v1),必须执行存储版本迁移(Storage Version Migration)。Kubernetes 1.29+ 提供了内置的 StorageVersionMigration API,但更通用的做法是使用 Operator 自身的迁移逻辑或社区工具。

迁移方式 适用版本 操作复杂度 数据一致性
手动 re-create 所有资源 所有版本 低(但风险高) 需停机
使用 kubectl replace 逐个重写 所有版本 中 逐对象一致
Operator 内置迁移 Controller 推荐生产环境 高(需开发) 在线迁移
Kubernetes StorageVersionMigration API ≥1.29 中 自动处理
存储版本迁移风险: 一旦 CRD 的存储版本变更,所有已存在的资源对象在 etcd 中仍然是旧版本格式。如果未完成迁移就删除旧版本的 Webhook 或 Schema,API Server 将无法正确读取这些对象,导致资源不可用。迁移前务必做好备份,并在测试环境充分验证。

3.5 版本控制实战要点

  • 版本命名规范: 遵循 v[主版本][alpha|beta][序号] 模式,如 v1alpha1、v1beta2、v1。alpha 表示不稳定,beta 表示基本稳定,无后缀表示 GA。
  • 存储版本选择: 始终选择最成熟的版本作为存储版本。如果 CRD 只有一个版本,该版本自动成为存储版本。
  • Webhook 高可用: 转换 Webhook 是读写路径上的关键组件,必须部署多副本并配置健康检查。Webhook 故障会导致所有跨版本读写操作失败。
  • 转换幂等性: Webhook 的转换逻辑必须是幂等的——多次转换同一对象应得到相同结果。避免在转换过程中修改非版本相关的业务状态。
  • 测试覆盖: 为每个版本组合编写转换测试,包括正向转换(旧→新)和反向转换(新→旧),确保数据不丢失、不损坏。