6. GVK 与 GVR
一个 YAML 的旅行
你写了一个 Deployment YAML:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
kubectl 收到这个 YAML,干的第一件事是:搞清楚这是个什么东西,该往哪个 API 路径发。
apiVersion: apps/v1 + kind: Deployment 就是这个 YAML 的"身份证"。K8S 用这套身份证来区分成百上千种资源——这个区分机制叫 GVK。
GVK:Group/Version/Kind
apiVersion: apps/v1
│ │
│ └── Version
└────── Group
kind: Deployment
└──── Kind
| 字段 | 含义 | 比方 |
|---|---|---|
| Group | API 组,资源的"部门" | 公司里的部门 |
| Version | API 版本 | 部门里的文件版本 v1/v2 |
| Kind | 资源类型 | 部门里的具体岗位 |
三个拼一起就是 GVK:apps/v1/Deployment。
Group 的分类
Group 有两种:
核心组(core group):没有 group 名,API 路径直接是 /api/v1。Pod、Service、Node、ConfigMap 这些元老级资源都在核心组。
apiVersion: v1 # 没有 group,只有 version
kind: Pod
命名组(named group):有 group 名,API 路径是 /apis/<group>/<version>。后来加的资源都在命名组里。
apiVersion: apps/v1 # group=apps
apiVersion: batch/v1 # group=batch
apiVersion: networking.k8s.io/v1 # group=networking.k8s.io
为什么分 group?跟公司分部门一个道理——资源太多,全堆一个 namespace 里乱。apps 组管工作负载,batch 组管任务,rbac 组管权限,各管各的。
Version 的含义
Version 不是软件版本,是 API 的成熟度:
| 版本前缀 | 含义 | 能上生产吗 |
|---|---|---|
alpha | 实验性,随时可能改 | 不能 |
beta | 基本稳定,可能有微调 | 谨慎 |
v1(无前缀) | 稳定,保证向后兼容 | 能 |
一个资源可以有多个 version 同时存在——比如 v1beta1 和 v1。K8S 内部做版本转换,对外暴露哪个 version 都行,存到 etcd 时统一转成内部版本。
打个比方:API 像合同模板。alpha 是草稿,beta 是送审版,v1 是正式签章版。同一份合同可能同时有草稿和正式版,但法务(API Server)对外只发正式版,内部归档用统一的版本号。
GVR:Group/Version/Resource
GVK 是 YAML 里的写法,到了 HTTP 路径里就变成 GVR。
GVK: apps/v1/Deployment (YAML 里这么写)
GVR: apps/v1/deployments (HTTP 路径里这么拼)
区别就是 Kind vs Resource:
| Kind | Resource | |
|---|---|---|
| 是什么 | 资源类型的名字(驼峰) | REST 路径里的名字(小写复数) |
| 例子 | Deployment、Pod、Service | deployments、pods、services |
| 出现在哪 | YAML 的 kind: 字段 | URL 路径 |
简单记:Kind 是单数,Resource 是复数。Deployment 是一个,deployments 是一类。
API 路径怎么拼
核心组 Pod:
GET /api/v1/namespaces/default/pods/nginx
命名组 Deployment:
GET /apis/apps/v1/namespaces/default/deployments/nginx
CRD(自定义资源)MySQL:
GET /apis/database.example.com/v1/namespaces/default/mysqls/replica
规律:
- 核心组:
/api/<version>/... - 命名组:
/apis/<group>/<version>/... - 都带
namespaces/<ns>/(除非是集群级资源如 Node)
kubectl api-resources
这个命令列出集群里所有 GVR:
$ kubectl api-resources
NAME SHORTNAMES APIVERSION NAMESPACED KIND
pods po v1 true Pod
services svc v1 true Service
deployments deploy apps/v1 true Deployment
daemonsets ds apps/v1 true DaemonSet
jobs batch/v1 true Job
cronjobs cj batch/v1 true CronJob
mysqls database.example.com/v1 true MySQL
每一行就是一个 GVR。看 APIVERSION 列——拼上 KIND 就是 GVK。
用 kubectl explain 查 schema
知道 GVK 后,想看这个资源的字段结构:
$ kubectl explain deployment.spec
KIND: Deployment
VERSION: apps/v1
FIELD: spec <DeploymentSpec>
DESCRIPTION:
Specification of the desired behavior of the Deployment.
FIELDS:
replicas <integer>
Number of desired pods.
selector <LabelSelector> -required-
Label selector for pods.
template <PodTemplateSpec> -required-
Template describes the pods that will be created.
这个结构信息哪来的?来自资源的 OpenAPI Schema——描述资源长什么样的 schema 定义。后面写 CRD 时我们会自己定义这个 schema。
Scheme:GVK 和 Go 类型的映射
Controller 是 Go 写的。YAML 里写 kind: Deployment,Go 里有个 appsv1.Deployment 结构体——这俩怎么对应?
Scheme 就是这个映射表:
scheme := runtime.NewScheme()
// 注册:GVK → Go 类型
scheme.AddKnownTypes(appsv1.SchemeGroupVersion, &appsv1.Deployment{})
scheme.AddKnownTypes(corev1.SchemeGroupVersion, &corev1.Pod{})
// 反序列化时用
obj, err := scheme.New(kind) // kind=Deployment → 返回 &appsv1.Deployment{}
打个比方:Scheme 像 ORM 里的表映射。数据库有 deployments 表,Go 有 Deployment 结构体,Scheme 就是中间那张映射表,告诉框架"这俩是一回事"。
写 Controller 时第一件事就是建 Scheme,把你要管的资源类型注册进去:
var scheme = runtime.NewScheme()
func init() {
clientgoscheme.AddToScheme(scheme) // 注册 K8S 内置资源
mygroupv1.AddToScheme(scheme) // 注册自定义资源
}
没注册的资源,Controller 不认识——读到 kind: MySQL 的 YAML,Scheme 里没有,反序列化直接报错。
RESTMapper:GVK 到 GVR 的桥梁
你知道 GVK(YAML 里的),想发 HTTP 请求要 GVR(URL 里的)。这俩怎么转?
RESTMapper 干这个活:
gvr, err := mapper.RESTMapping(schema.GroupKind{Group: "apps", Kind: "Deployment"}, "v1")
// 返回 gvr = {Group: "apps", Version: "v1", Resource: "deployments"}
为什么需要这一步?因为 Kind → Resource 的转换规则不是死的——大部分是加 s,但也有特殊情况:
| Kind | Resource | 规律 |
|---|---|---|
| Pod | pods | 加 s |
| Service | services | 加 s |
| Ingress | ingresses | 加 es |
| Policy | policies | y → ies |
| EndpointSlice | endpointslices | 加 s |
RESTMapper 就是处理这些边角情况的查表器。
完整链条
一个 YAML 从提交到落库的完整路径:
你写的 YAML
│
▼ apiVersion + kind
GVK: apps/v1/Deployment
│
▼ RESTMapper 转换
GVR: apps/v1/deployments
│
▼ 拼 HTTP 路径
POST /apis/apps/v1/namespaces/default/deployments
│
▼ API Server 收到
├─ Scheme 反序列化 → &appsv1.Deployment{}
├─ Admission 准入校验
├─ Validation schema 校验
└─ 写入 etcd
写 CRD 时要定义什么
预告下一章——写 CRD 时,你本质在做这几件事:
- 定义 GVK:
group: database.example.com,version: v1,kind: MySQL - 定义 schema:
spec里有哪些字段,什么类型 - 注册到 Scheme:让 Controller 能反序列化这个资源
理解了 GVK/GVR,下一章写 CRD 就是水到渠成的事。
前置阅读
- K8S架构 — Starter 第 2 章,API Server 的路由层就是基于 GVK/GVR 设计的
- 标准接口:CNI/CSI/CRI — Starter 第 13 章,三大接口也通过 API 暴露,理解 GVK 后回头看这章会更清楚
思考题
问题 1:apiVersion: v1 和 apiVersion: apps/v1 的区别是什么?为什么 Pod 不用写 group?
问题 2:你写了一个 CRD,kind: MySQL。它的 GVR 是什么?API 路径是什么?
问题 3:Scheme 不注册某个资源类型,会发生什么?
解答
问题 1:v1 是核心组(core group),没有 group 名,API 路径是 /api/v1。apps/v1 是命名组,group 是 apps,API 路径是 /apis/apps/v1。Pod 是 K8S 最早期的资源,那时候还没有 group 的概念,所以放在核心组里。后来加的资源都进了命名组。
问题 2:GVR 是 mysqls(MySQL 的复数,加 s)。假设 group 是 database.example.com,version 是 v1,那 API 路径是 /apis/database.example.com/v1/namespaces/<ns>/mysqls/<name>。
问题 3:Controller 读到这个资源的 YAML 或 JSON,调用 scheme.New(kind) 时返回 nil 或报错——因为它不知道这个 GVK 对应哪个 Go 类型。反序列化直接失败。这就是为什么写 Controller 必须先把要管的资源注册到 Scheme 里。
下一章
理解了 GVK/GVR,下一章我们编写 CRD——定义自己的资源类型,教 K8S 认识"MySQL"这样的新词。