3.15. Kubernetes 系统扩展:CRD、聚合 API 与 Operator

Kubernetes 的内置资源覆盖不了所有领域。CRD 让你声明新的资源类型,聚合 API 让独立的 API Server 接入同一套发现与鉴权,Operator 则把运维知识写成控制器。本章沿着这三条路径扩展控制平面,并补上多 control-plane 的高可用基础。

层级:

L3 · 平台

难度:

进阶

预计:

90 分钟

前置:

核心对象、RBAC、集群部署

3.15.1. 你将学到

  • CRD 的 v1 结构、版本字段、structural schema、printer columns 与 status 子资源。

  • 控制器/Operator 的 reconcile 循环,finalizer、ownerReference 与 leader election 的作用。

  • 用 APIService 把外部 API Server 接入 kube-apiserver,以及它需要的 TLS 与 RBAC 委托。

  • RBAC 聚合规则,以及多 control-plane + etcd 的高可用要点。

3.15.2. 扩展控制平面的三条路径

CRD 是最轻的扩展:向 kube-apiserver 注册新的 group/version/kind,对象存进 etcd,读写、watch、RBAC、发现都由 kube-apiserver 直接处理,不需要额外进程。代价是只能使用通用 API 语义,无法自定义存储或特殊子资源行为。

聚合 API 把请求代理给一个独立运行的 API Server。这个 server 自己实现 REST 语义、存储和业务逻辑,但通过 APIService 注册到主 apiserver,客户端仍然只面对一个 API 端点。它适合需要 protobuf、特殊存储或复杂生命周期的场景,代价是要自己处理 TLS、鉴权委托和高可用。

架构图

方式

新增资源类型

数据存储

适用场景

CRD

是,声明式

etcd

绝大多数领域资源

聚合 API

是,独立 server

自定义

特殊语义、性能或协议需求

Operator

否,组合 CRD 与控制器

通过 CRD 存 etcd

有状态应用的运维自动化

3.15.3. CRD:把自定义资源变成一等公民

apiextensions.k8s.io/v1 要求每个版本提供 structural schema:必须有 type: object,未知字段默认被裁剪,x-kubernetes-preserve-unknown-fields 只应在必要时使用。v1beta1 在 v1.22 已移除,旧清单必须迁移;versions 列表中只能有一个 storage: true,其余版本靠 conversion webhook 转换。

additionalPrinterColumns 决定 kubectl get 的列,subresources.status 把 spec 与 status 分开:普通更新写 spec,控制器通过 /status 子资源写状态,两者不会互相覆盖。subresources.scale 还能让 HPA 和 kubectl scale 识别自定义资源;v1.31 支持用 CEL 的 x-kubernetes-validations 写跨字段校验。

3.15.4. 控制器与 Operator

控制器从 informer 缓存里读对象,把 key 放进 workqueue,然后执行 reconcile:读取实际状态,计算与期望状态的差异,发起变更,再返回。这个过程是 level-triggered 而不是 edge-triggered,所以重复执行必须幂等;失败就重新入队,直到成功或达到重试上限。

Operator 是“CRD + 控制器 + RBAC + 部署清单”的打包,核心是把领域知识编码进 reconcile。删除外部资源前要先清理,靠的是 finalizer:控制器给对象加上 finalizer,删除时先做清理,完成后移除 finalizer,对象才真正消失。ownerReferences 让垃圾回收器在父对象删除时级联清理子对象;多个副本运行时必须开启 leader election,否则会重复执行同一动作。

3.15.5. 聚合 API Server

聚合 API 的关键是 APIService:它声明 group、version 和一个 Service,kube-apiserver 收到该组请求后先完成认证,再把请求代理给这个 Service。扩展 apiserver 必须能验证用户身份,因此需要 system:auth-delegator 权限去调用 TokenReview 和 SubjectAccessReview;还需要读取 kube-system/extension-apiserver-authentication ConfigMap,拿到客户端 CA 配置。

TLS 是第二个硬要求。生产环境在 APIService 里写 caBundle,让 kube-apiserver 校验扩展 server 的证书;insecureSkipTLSVerify: true 只适合实验。Available condition 为 False 时,对应组的资源会从 kubectl api-resources 消失,请求返回 503。

3.15.6. RBAC 聚合

聚合 ClusterRole 不直接写 rules,而是用 aggregationRule 选择一组带标签的 ClusterRole,控制器把它们的 rules 合并进来。内置的 admin、edit、view 就是这样实现的:给自定义角色打上 rbac.authorization.k8s.io/aggregate-to-edit: "true",它的权限就会自动进入 edit。聚合角色不能手工改 rules,下一次同步会覆盖;要调整权限,只能改被选中的角色或标签。

3.15.7. 控制平面高可用

多 control-plane 的前提是一个稳定接入地址:kubeadm init --control-plane-endpoint 指向 VIP 或负载均衡器,后续 kubeadm join --control-plane 都连这个地址。apiserver 无状态,controller-manager 与 scheduler 通过 leader election 保证同一时刻只有一个实例工作。

stacked etcd 与 control-plane 同机部署,3 个节点容忍 1 个故障,5 个容忍 2 个;2 个节点没有意义,因为 quorum 需要多数派。规模较大时改用 external etcd,把 etcd 独立到低延迟磁盘上。etcd 要定期做 snapshot 并实际演练恢复;只有备份文件、没有恢复流程,等于没有备份。

3.15.8. 动手实验

实验一:定义 User CRD、状态子资源与打印列

目标:注册 users.auth.ilinux.io,创建自定义资源,用 schema 拒绝非法输入,并通过 status 子资源更新状态。

步骤 1 · 创建 CRD

crd-user.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: users.auth.ilinux.io
spec:
  group: auth.ilinux.io
  scope: Namespaced
  names:
    kind: User
    plural: users
    singular: user
    shortNames: ["u"]
  versions:
  - name: v1alpha1
    served: true
    storage: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            required: ["userID", "groups"]
            properties:
              userID:
                type: integer
                minimum: 1
                maximum: 65535
              groups:
                type: array
                items: {type: string}
          status:
            type: object
            properties:
              phase: {type: string}
    additionalPrinterColumns:
    - {name: UserID, type: integer, jsonPath: .spec.userID}
    - {name: Groups, type: string, jsonPath: .spec.groups}
    - {name: Phase, type: string, jsonPath: .status.phase}
    subresources:
      status: {}
kubectl apply -f crd-user.yaml
kubectl wait --for=condition=Established crd/users.auth.ilinux.io --timeout=60s

步骤 2 · 创建自定义资源

user-admin.yaml
apiVersion: auth.ilinux.io/v1alpha1
kind: User
metadata:
  name: admin
  namespace: default
spec:
  userID: 1
  email: [email protected]
  groups: ["superusers", "administrators"]
kubectl apply -f user-admin.yaml
kubectl get users
kubectl get u admin -o yaml

kubectl get users 的表头就是 printer columns。创建 userID: 0 的对象会被 schema 拒绝,错误里会指出 minimum 约束。

步骤 3 · 写 status 子资源

kubectl patch user admin --subresource=status --type=merge \
  -p '{"status":{"phase":"Active"}}'
kubectl get u admin

直接改 spec 不会影响 status,反之亦然;控制器应使用 /status 端点,否则并发更新会互相覆盖。

实验二:用 APIService 接入示例 apiserver

目标:部署原书的 sample-apiserver,把 wardle.example.com/v1alpha1 注册进集群,并通过聚合层访问 Flunder 资源。

步骤 1 · 应用全部清单

kubectl apply -f book-repo/chapter12/sample-apiserver/
kubectl -n wardle rollout status deploy/wardle-server
kubectl get apiservice v1alpha1.wardle.example.com
kubectl api-resources | grep wardle
kubectl get flunders

目录里的 01–08 依次创建命名空间、ServiceAccount、RBAC、Service、Deployment 与 APIService。05 把 SA 绑定到 system:auth-delegator,06 让它读取 kube-system 的认证配置;少了任何一个,扩展 apiserver 都无法完成认证委托,聚合层会返回 401/403。

步骤 2 · 检查 APIService

apiservice-wardle.yaml
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
  name: v1alpha1.wardle.example.com
spec:
  group: wardle.example.com
  version: v1alpha1
  groupPriorityMinimum: 1000
  versionPriority: 15
  insecureSkipTLSVerify: true
  service:
    name: api
    namespace: wardle
    port: 443

Available 应为 True,kubectl get flunders 应返回示例 server 里的数据。若显示 ServiceUnavailable,先看 kubectl -n wardle logs deploy/wardle-server 和 Endpoints;若返回 401/403,回到步骤 1 检查两个委托绑定。insecureSkipTLSVerify 只用于实验,生产应准备 CA 并写 caBundle。

3.15.8.1. 验证清单

  • CRD 的 Established 为 True,kubectl get users 显示 UserID、Groups、Phase 三列。

  • userID: 0 或缺少 groups 的对象被拒绝,错误信息包含 schema 约束。

  • kubectl patch --subresource=status 能更新 Phase,且不影响 spec。

  • kubectl get apiservice v1alpha1.wardle.example.com 的 Available 为 True。

3.15.9. 常见问题与排错

现象

可能原因

处理方式

no matches for kind "User"

CRD 未 Established,或请求的 version 未 served

看 CRD conditions 与 versions;确认 group/version 拼写

CRD 创建报 must have exactly one version marked as storage

多个版本都写了 storage: true,或一个都没有

只保留一个 storage 版本,其余 served 版本配 conversion

kubectl get users 没有自定义列

printer columns 写错层级,或 CRD 没重新应用

检查 jsonPath 是否以 .spec 开头;重新 apply 后等 Established

status 更新被 spec 覆盖

没走 /status 子资源,或控制器全量 Update

用 --subresource=status;控制器分开写 spec/status

APIService Available=False

Service 无 Endpoints、TLS 校验失败、端口不对

看 conditions、Endpoints 与扩展 server 日志

3.15.10. 进阶挑战

  1. 给 User CRD 增加 v1 版本,配置 conversion webhook,把 v1alpha1 的对象无损转换到 v1。

  2. 用 kubebuilder 写一个最小 User 控制器:创建时生成随机密码写入 Secret,删除时用 finalizer 清理 Secret。

  3. 用 aggregationRule 把自定义权限聚合进 edit,再用 kubectl auth can-i 验证普通编辑者获得了新权限。

3.15.11. 小结

  • CRD 用声明式 schema 扩展 API,资源存 etcd;v1 要求 structural schema,status 子资源隔离 spec 与状态。

  • Operator 用 reconcile 循环持续逼近期望状态,finalizer 负责删除清理,leader election 防止重复执行。

  • 聚合 API 把独立 server 挂到 kube-apiserver 后面,必须解决 TLS、auth-delegator 与 auth-reader 三件事。

  • 多 control-plane 依赖统一接入地址、奇数 etcd 与 leader election;stacked etcd 简单,external etcd 更适合大规模集群。

3.15.12. 延伸阅读