YAML 不是 Kubernetes,YAML 只是申请表
初学 Kubernetes 时,我们每天都在写 YAML,很容易产生一种错觉:Kubernetes 就是一堆 YAML。
更准确的理解是:YAML 只是把一个 API 对象序列化成文本。真正重要的是 API Server 中保存的对象,以及控制器围绕这些对象持续运行的控制循环。
下面这个文件不是一段“创建脚本”,而是一份期望状态声明:
| |
它表达的是:“集群里应该存在一个名为 demo-pool 的 SubPool,它的暴露模式应该是 Internal。”
每个对象都有四个核心部分
apiVersion:去哪个 API 组找规则
| |
它由 API group 和 version 组成:
- API group:
subrelay.example.com - version:
v1alpha1
Kubernetes 内置核心资源常用 apiVersion: v1,例如 Namespace、Service、ConfigMap 和 Secret。其他资源会带 API group:
| 资源 | apiVersion |
|---|---|
| Deployment、StatefulSet | apps/v1 |
| NetworkPolicy | networking.k8s.io/v1 |
| PodDisruptionBudget | policy/v1 |
| HTTPRoute、Gateway | gateway.networking.k8s.io/v1 |
| SubPool | subrelay.example.com/v1alpha1 |
apiVersion 不是镜像版本,也不是 Kubernetes 集群版本。它决定 API Server 用哪套 schema 解析和校验对象。
kind:这张申请表是什么类型
| |
kind 决定对象的类别。不同 Kind 有不同的 spec 结构。例如 Service 的 spec 里有端口和 selector,StatefulSet 的 spec 里有 Pod template 和 volume claim template。
metadata:对象的身份证
| |
常见 metadata 包括:
| 字段 | 作用 |
|---|---|
name | 对象在作用域内的名称 |
namespace | 对象属于哪个 Namespace |
labels | 供查询、选择和策略匹配使用的键值对 |
annotations | 给工具或控制器使用的非标识性信息 |
ownerReferences | 表示资源归谁管理 |
generation | spec 被修改后递增的代数 |
resourceVersion | API Server 用于并发控制的版本 |
finalizers | 删除前必须完成的收尾任务 |
本项目的 SubPool 是 cluster-scoped,所以它没有 metadata.namespace。即使执行 kubectl -n test get subpool,-n test 也不会让它突然变成 Namespace 内资源。
spec 与 status:愿望和现实
spec 是用户或上层控制器表达的期望状态,status 是控制器观察到的现实状态。
| |
在本项目里,spec 包含镜像、CPU、内存、存储、暴露模式和暂停开关;status 包含 Namespace、CPA Service、hostname、observedGeneration 和 Conditions。
不要手工编辑 status。它属于控制器,不是用户配置区。
kubectl apply 实际做了什么
| |
这条命令大致经历:
kubectl读取 YAML。- 根据当前 kubeconfig 把请求发给 API Server。
- API Server 做认证、授权、schema 校验和 admission。
- 对象被持久化。
- 监听这个对象的 controller 收到事件。
- controller 开始 reconcile。
kubectl 并没有亲自创建 StatefulSet 或 PVC。那些资源是 SubRelay Operator 看到 SubPool 后创建的。
这也是排障时必须分层的原因:
kubectl apply失败:通常还没通过 API Server 校验或授权。SubPool已存在但没有派生资源:看 Operator 日志和 Conditions。- StatefulSet 已创建但 Pod 不 Ready:看 Pod、Event、探针、镜像和存储。
声明式管理与命令式操作
Kubernetes 同时支持两种操作方式。
命令式操作描述“执行什么动作”:
| |
声明式操作描述“最终应该是什么样”:
| |
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:
| |
这只修改 spec.suspend,其他字段保持不变。
服务端校验为什么重要
SubPool 的 CRD 给 API Server 安装了一份 schema。例如:
exposure.mode只能是Internal或Gateway;metadata.name最长 40 个字符,并且必须是 DNS label;- CPU 和内存的 request/limit 必须存在;
storage.size创建后不可修改。
这类静态错误应尽量在对象进入系统前被拒绝。动态依赖则不能只靠 schema,例如 Shared Mihomo 是否有 Ready Endpoint,需要 Operator 运行时检查并写入 Condition。
因此项目用了两层防线:
| |
名称、作用域与完整身份
Namespaced 对象的完整身份通常是:
| |
例如:
| |
不同 Namespace 中可以同时存在名为 cpa 的 Service。因此每个 SubPool 都可以使用固定资源名,而不会互相冲突。
Cluster-scoped 对象没有 Namespace,例如:
| |
这也是 SubPool 名称必须全局唯一的原因。
读取对象时先看哪里
初学者常对着完整 YAML 从第一行看到最后一行,很快被 managedFields 淹没。更有效的顺序是:
| |
阅读时优先关注:
metadata.name、namespace、labels 和 owner references;spec是否等于你的期望;status.conditions的type、status、reason、message;metadata.generation是否已经被status.observedGeneration观察;- 最后才看工具自动维护的内部字段。
项目中可用更紧凑的命令看 Conditions:
| |
删除也是一次状态转换
| |
API Server 通常不会立刻把带 finalizer 的对象物理删除,而是先设置 deletionTimestamp。Operator 看到它后删除专属 Namespace,等待子资源清理完毕,最后移除 finalizer。
因此“执行了 delete”与“对象已经消失”不是同一件事。对象停在 Terminating 时,应检查 finalizer 和负责收尾的 controller,而不是直接 --force。
本项目中的对象阅读练习
可以按下面顺序观察一个号池:
| |
每看一个对象,都回答五个问题:它是谁、在哪里、谁创建它、它选择谁、它当前是否达到期望。只要这五个问题能回答清楚,大部分 Kubernetes YAML 就不再神秘。