从 YAML 开始理解 Kubernetes 对象

YAML 不是 Kubernetes,YAML 只是申请表

初学 Kubernetes 时,我们每天都在写 YAML,很容易产生一种错觉:Kubernetes 就是一堆 YAML。

更准确的理解是:YAML 只是把一个 API 对象序列化成文本。真正重要的是 API Server 中保存的对象,以及控制器围绕这些对象持续运行的控制循环。

下面这个文件不是一段“创建脚本”,而是一份期望状态声明:

1
2
3
4
5
6
7
apiVersion: subrelay.example.com/v1alpha1
kind: SubPool
metadata:
  name: demo-pool
spec:
  exposure:
    mode: Internal

它表达的是:“集群里应该存在一个名为 demo-poolSubPool,它的暴露模式应该是 Internal。”

每个对象都有四个核心部分

apiVersion:去哪个 API 组找规则

1
apiVersion: subrelay.example.com/v1alpha1

它由 API group 和 version 组成:

  • API group:subrelay.example.com
  • version:v1alpha1

Kubernetes 内置核心资源常用 apiVersion: v1,例如 Namespace、Service、ConfigMap 和 Secret。其他资源会带 API group:

资源apiVersion
Deployment、StatefulSetapps/v1
NetworkPolicynetworking.k8s.io/v1
PodDisruptionBudgetpolicy/v1
HTTPRoute、Gatewaygateway.networking.k8s.io/v1
SubPoolsubrelay.example.com/v1alpha1

apiVersion 不是镜像版本,也不是 Kubernetes 集群版本。它决定 API Server 用哪套 schema 解析和校验对象。

kind:这张申请表是什么类型

1
kind: SubPool

kind 决定对象的类别。不同 Kind 有不同的 spec 结构。例如 Service 的 spec 里有端口和 selector,StatefulSet 的 spec 里有 Pod template 和 volume claim template。

metadata:对象的身份证

1
2
metadata:
  name: demo-pool

常见 metadata 包括:

字段作用
name对象在作用域内的名称
namespace对象属于哪个 Namespace
labels供查询、选择和策略匹配使用的键值对
annotations给工具或控制器使用的非标识性信息
ownerReferences表示资源归谁管理
generationspec 被修改后递增的代数
resourceVersionAPI Server 用于并发控制的版本
finalizers删除前必须完成的收尾任务

本项目的 SubPool 是 cluster-scoped,所以它没有 metadata.namespace。即使执行 kubectl -n test get subpool-n test 也不会让它突然变成 Namespace 内资源。

spec 与 status:愿望和现实

spec 是用户或上层控制器表达的期望状态,status 是控制器观察到的现实状态。

1
2
spec:我希望有一套 Internal 模式、20Gi 存储的号池
status:它已经派生出 subpool-demo-pool,目前代理 Endpoint 尚未就绪

在本项目里,spec 包含镜像、CPU、内存、存储、暴露模式和暂停开关;status 包含 Namespace、CPA Service、hostname、observedGeneration 和 Conditions。

不要手工编辑 status。它属于控制器,不是用户配置区。

kubectl apply 实际做了什么

1
kubectl apply -f examples/subpool-internal.yaml

这条命令大致经历:

  1. kubectl 读取 YAML。
  2. 根据当前 kubeconfig 把请求发给 API Server。
  3. API Server 做认证、授权、schema 校验和 admission。
  4. 对象被持久化。
  5. 监听这个对象的 controller 收到事件。
  6. controller 开始 reconcile。

kubectl 并没有亲自创建 StatefulSet 或 PVC。那些资源是 SubRelay Operator 看到 SubPool 后创建的。

这也是排障时必须分层的原因:

  • kubectl apply 失败:通常还没通过 API Server 校验或授权。
  • SubPool 已存在但没有派生资源:看 Operator 日志和 Conditions。
  • StatefulSet 已创建但 Pod 不 Ready:看 Pod、Event、探针、镜像和存储。

声明式管理与命令式操作

Kubernetes 同时支持两种操作方式。

命令式操作描述“执行什么动作”:

1
2
kubectl create namespace demo
kubectl scale statefulset runtime --replicas=0

声明式操作描述“最终应该是什么样”:

1
kubectl apply -f object.yaml

Operator 项目更依赖声明式思维。用户修改 SubPool.spec,Operator 再把底层资源收敛到新状态。手工修改 Operator 管理的 StatefulSet,可能暂时生效,但下一轮 reconcile 会把它改回来。

可以把这叫作“方向盘归 Operator”:底层对象的期望状态来源不是临时的 kubectl edit,而是 SubPool 和 Operator Configuration。

create、apply、patch、edit 的区别

命令适合场景注意事项
kubectl create明确只创建一次对象已存在时失败
kubectl apply持续声明式维护适合 Git 中的 manifests
kubectl patch修改少数字段适合暂停、恢复等小操作
kubectl edit临时交互修改容易留下不可复现的配置
kubectl replace用完整对象替换对资源版本和不可变字段更敏感

本项目暂停号池使用 merge patch:

1
2
kubectl patch subpool demo-pool --type=merge \
  -p '{"spec":{"suspend":true}}'

这只修改 spec.suspend,其他字段保持不变。

服务端校验为什么重要

SubPool 的 CRD 给 API Server 安装了一份 schema。例如:

  • exposure.mode 只能是 InternalGateway
  • metadata.name 最长 40 个字符,并且必须是 DNS label;
  • CPU 和内存的 request/limit 必须存在;
  • storage.size 创建后不可修改。

这类静态错误应尽量在对象进入系统前被拒绝。动态依赖则不能只靠 schema,例如 Shared Mihomo 是否有 Ready Endpoint,需要 Operator 运行时检查并写入 Condition。

因此项目用了两层防线:

1
2
CRD OpenAPI/CEL:检查格式和不可变规则
Operator reconcile:检查资源预算、共享网关和所有权等动态条件

名称、作用域与完整身份

Namespaced 对象的完整身份通常是:

1
API Group + Kind + Namespace + Name

例如:

1
v1 / Service / subpool-demo-pool / cpa

不同 Namespace 中可以同时存在名为 cpa 的 Service。因此每个 SubPool 都可以使用固定资源名,而不会互相冲突。

Cluster-scoped 对象没有 Namespace,例如:

1
subrelay.example.com/v1alpha1 / SubPool / demo-pool

这也是 SubPool 名称必须全局唯一的原因。

读取对象时先看哪里

初学者常对着完整 YAML 从第一行看到最后一行,很快被 managedFields 淹没。更有效的顺序是:

1
2
3
kubectl get sp demo-pool
kubectl describe sp demo-pool
kubectl get sp demo-pool -o yaml

阅读时优先关注:

  1. metadata.namenamespace、labels 和 owner references;
  2. spec 是否等于你的期望;
  3. status.conditionstypestatusreasonmessage
  4. metadata.generation 是否已经被 status.observedGeneration 观察;
  5. 最后才看工具自动维护的内部字段。

项目中可用更紧凑的命令看 Conditions:

1
2
kubectl get sp demo-pool \
  -o jsonpath='{range .status.conditions[*]}{.type}={.status}{"\t"}{.reason}{"\n"}{end}'

删除也是一次状态转换

1
kubectl delete subpool demo-pool

API Server 通常不会立刻把带 finalizer 的对象物理删除,而是先设置 deletionTimestamp。Operator 看到它后删除专属 Namespace,等待子资源清理完毕,最后移除 finalizer。

因此“执行了 delete”与“对象已经消失”不是同一件事。对象停在 Terminating 时,应检查 finalizer 和负责收尾的 controller,而不是直接 --force

本项目中的对象阅读练习

可以按下面顺序观察一个号池:

1
2
3
4
5
kubectl get sp demo-pool -o yaml
kubectl get namespace subpool-demo-pool -o yaml
kubectl -n subpool-demo-pool get statefulset runtime -o yaml
kubectl -n subpool-demo-pool get service cpa -o yaml
kubectl -n subpool-demo-pool get pvc

每看一个对象,都回答五个问题:它是谁、在哪里、谁创建它、它选择谁、它当前是否达到期望。只要这五个问题能回答清楚,大部分 Kubernetes YAML 就不再神秘。

使用 Hugo 构建
主题 StackJimmy 设计