API Server:集群的统一入口¶
kube-apiserver 是控制面中公开 Kubernetes HTTP API 的核心组件。kubectl、Controller、Scheduler 和 Kubelet 都通过它读取或更新 API 对象;通过校验的写请求由其存储层写入 etcd。API Server 通过 etcd Watch 更新内存中的 Watch Cache,完整的数据流和客户端续接语义见 etcd Watch 机制。
这里以创建 default/vllm-demo Deployment 为例,说明一条写请求在 API Server 中的处理过程。kubectl 提交资源后,API Server 依次执行身份认证、授权、Mutating Admission、资源对象校验和 Validating Admission,再通过存储层将 Deployment 写入 etcd。未启用服务端 dry-run 时,返回 201 Created 表示 Deployment 已完成持久化。
Deployment 资源清单¶
vllm-demo Deployment 中需要关注以下配置:
- Pod 选择器:
spec.selector.matchLabels与spec.template.metadata.labels都使用app: vllm-demo,满足 Deployment selector 必须匹配 Pod template label 的校验规则。 - 运行时与 GPU:
runtimeClassName: nvidia选择节点上的nvidiaruntime handler。limits.nvidia.com/gpu: "1"为 Pod 申请一张 GPU。 - 模型缓存:
vllm-model-cachePVC 挂载到/root/.cache/huggingface,Pod 重建后可以继续使用已经下载的模型文件。 - vLLM 进程:容器使用
vllm/vllm-openai:v0.27.1镜像,执行vllm serve Qwen/Qwen3-0.6B --port 8000。
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-demo
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: vllm-demo
template:
metadata:
labels:
app: vllm-demo
spec:
runtimeClassName: nvidia
containers:
- name: vllm
image: vllm/vllm-openai:v0.27.1
command: ["vllm", "serve"]
args: ["Qwen/Qwen3-0.6B", "--port", "8000"]
ports:
- containerPort: 8000
resources:
limits:
nvidia.com/gpu: "1"
volumeMounts:
- name: model-cache
mountPath: /root/.cache/huggingface
volumes:
- name: model-cache
persistentVolumeClaim:
claimName: vllm-model-cache
使用 kubectl apply 提交 Deployment:
首次创建 Deployment 时,请求目标是:
下图展示了 Deployment 创建请求从 TLS 连接到持久化的主要路径,包含身份认证、授权、准入、资源对象校验和存储。
flowchart LR
K["kubectl / REST client"] --> T["TLS 与 RequestInfo"]
T --> AU["Authentication\n调用者是谁"]
AU --> AZ["Authorization\n能否创建 Deployment"]
AZ --> MA["Mutating Admission\n允许修改对象"]
MA --> V["资源对象校验\nDeployment 固有规则"]
V --> VA["Validating Admission\n允许拒绝对象"]
VA --> S["Registry / Storage"]
S --> E["etcd"]
E --> R["201 Created"]
Deployment 写入 etcd 后,后续组件继续通过 API Server 读取和更新对象。工作负载对象的创建见 Controller:从 Deployment 到 Pod,节点选择见 Scheduler:Pod 的节点选择,节点执行见 Kubelet:节点上的 Pod 管理。
身份认证¶
客户端先与 API Server 建立 TLS 连接。kubectl 从 kubeconfig 读取 API 地址、CA 和客户端凭据,认证模块据此得到用户名与用户组。API Server 支持 ServiceAccount Token、客户端证书、OIDC Token 和认证 Webhook 等认证方式。
认证模块完成凭据校验后,使用 user.Info 保存用户名、UID、用户组和附加属性。Kubernetes v1.36.4 的源码定义包含四个方法:
type Info interface {
GetName() string
GetUID() string
GetGroups() []string
GetExtra() map[string][]string
}
user.DefaultInfo 是这个接口的常用实现。例如,一个经过 OIDC 认证的用户可以表示为:
&user.DefaultInfo{
Name: "alice@example.com",
UID: "00u123456",
Groups: []string{"platform-admins", "system:authenticated"},
Extra: map[string][]string{
"example.com/tenant": []string{"ai-platform"},
},
}
Name、UID、Groups 和 Extra 分别对应四个 getter。具体值由认证模块填写,客户端证书认证通常提供用户名和用户组,OIDC 或认证 Webhook 还可以提供 UID 与附加属性。API Server 将这份信息放入请求上下文,授权、审计和准入随后读取它。
以下命令通过当前 kubeconfig 查询自己的 user.Info,输出位于 status.userInfo:
认证失败通常返回 401 Unauthorized;允许匿名请求的集群会将未认证请求识别为匿名用户,并送入授权阶段。
请求授权¶
授权阶段判断当前身份能否执行请求中的操作。
RBAC 使用 Role 保存权限规则,使用 RoleBinding 将这些权限授予用户、用户组或 ServiceAccount。下面的 Role 允许在 default namespace 创建 Deployment;RoleBinding 将这项权限授予用户 alice@example.com。
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: vllm-deployment-creator
namespace: default
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: alice-vllm-deployment-creator
namespace: default
subjects:
- kind: User
name: alice@example.com
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: vllm-deployment-creator
apiGroup: rbac.authorization.k8s.io
这组配置只允许 alice@example.com 在 default namespace 创建 Deployment。
集群管理员可以模拟 Alice 的身份,验证这项权限:
使用 --as 需要当前身份具备 impersonate 权限。Alice 使用自己的 kubeconfig 时可以省略该参数。RBAC 允许请求后,API Server 继续执行 Admission 和类型校验;授权拒绝时返回 403 Forbidden。
准入控制¶
准入控制位于授权之后、对象持久化之前,主要处理创建、更新和删除等资源写操作。Mutating Admission 可以修改请求对象,Validating Admission 根据对象和策略决定是否放行。GET、LIST 和 WATCH 等读取操作直接绕过准入控制。
API Server 先运行 Mutating Admission,再完成资源对象校验,随后运行 Validating Admission。任一准入插件拒绝请求,当前对象都不会写入 etcd。完整的处理顺序和插件列表见 Kubernetes 中的准入控制。
准入阶段¶
准入控制分为修改和校验两个阶段。Mutating Admission 先修改或拒绝对象;通过修改阶段的对象再进入 Validating Admission。校验阶段保留对象内容,可以放行、拒绝、告警或记录审计信息;任一拒绝结果都会终止当前请求。
下图在 Kubernetes 官方流程图中补充了 MutatingAdmissionPolicy。四个准入扩展点的相对顺序是 MutatingAdmissionPolicy、Mutating Webhook、ValidatingAdmissionPolicy 和 Validating Webhook。
图:Kubernetes v1.36 Admission Control 阶段。在 Kubernetes 官方流程图基础上补充 MutatingAdmissionPolicy,CC BY 4.0。
MutatingAdmissionPolicy¶
Kubernetes v1.36 提供 Stable 的 MutatingAdmissionPolicy。它在 API Server 进程内执行 CEL,使用 ApplyConfiguration 或 JSON Patch 修改对象,不需要部署 HTTPS Webhook。具体 API 和补丁类型见 Mutating Admission Policy。
配置示例:为 Deployment 添加注解
这组配置为 default 命名空间中新建的 Deployment 添加 admission.example.com/managed-by 注解。Policy 定义修改内容,Binding 激活 Policy,并用 namespaceSelector 限定生效范围。
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicy
metadata:
name: add-managed-by-annotation.example.com
spec:
failurePolicy: Fail
reinvocationPolicy: Never
matchConstraints:
resourceRules:
# 这条 Policy 只修改新建的 apps/v1 Deployment。
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["deployments"]
mutations:
- patchType: ApplyConfiguration
applyConfiguration:
# ApplyConfiguration 合并目标 annotation,保留对象中已有的其他 annotation。
expression: >-
Object{
metadata: Object.metadata{
annotations: {
"admission.example.com/managed-by": "mutating-admission-policy"
}
}
}
---
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingAdmissionPolicyBinding
metadata:
name: add-managed-by-annotation.example.com
spec:
# Policy 只有被 Binding 引用后才会执行。
policyName: add-managed-by-annotation.example.com
matchResources:
# Kubernetes 自动为 Namespace 添加这个不可变标签。
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: default
Policy 和 Binding 写入集群后,服务端 dry-run 也会执行这条准入规则。下面的命令返回修改后的 Deployment,但不会把测试对象写入 etcd:
kubectl apply \
-f examples/chapter-01/02-02-admission/01-mutating-admission-policy/policy.yaml
kubectl apply \
--dry-run=server \
-f examples/chapter-01/02-02-admission/01-mutating-admission-policy/input.yaml \
-o yaml
输出中的 metadata.annotations 包含新增注解。MutatingAdmissionPolicy 必须配有引用它的 MutatingAdmissionPolicyBinding 才会生效。
可运行输入、Kubernetes v1.36.1 实测输出和清理命令见 MutatingAdmissionPolicy 示例。
Mutating Webhook¶
MutatingAdmissionWebhook 根据 MutatingWebhookConfiguration 中的资源、操作、命名空间和对象选择器匹配请求,再按顺序调用外部 HTTPS Webhook。API Server 使用 AdmissionReview 发送请求;Webhook 可以返回 JSON Patch 修改对象,也可以拒绝请求。
每个 Webhook 都可以配置超时和调用失败时的处理方式。timeoutSeconds 默认为 10 秒,failurePolicy 默认为 Fail。配置了 reinvocationPolicy: IfNeeded 的 Webhook 还可能在对象被后续 Webhook 修改后再次执行。
配置与代码示例:为 Pod 添加标签
这个示例只匹配 default 命名空间的 Pod 创建请求。API Server 调用 /mutate,Webhook 返回 JSON Patch,为 Pod 添加 admission.example.com/managed=true 标签。
配置中的连接信息需要与 Webhook 服务保持一致:
- Service:
clientConfig.service指向admission-demo/admission-webhook,Service 的 443 端口转发到程序监听的 8443 端口。 - CA:应用配置前,把
${CA_BUNDLE}替换为签发服务端证书的 PEM CA 证书经过 base64 编码后的内容。 - 服务端证书:SAN 包含
admission-webhook.admission-demo.svc,API Server 才能校验 Service 的身份。
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: pod-labeler.example.com
webhooks:
- name: pod-labeler.example.com
admissionReviewVersions: ["v1"]
sideEffects: None
failurePolicy: Fail
matchPolicy: Equivalent
reinvocationPolicy: IfNeeded
timeoutSeconds: 5
clientConfig:
service:
namespace: admission-demo
name: admission-webhook
path: /mutate
port: 443
# 部署脚本生成 CA;注册前将占位符替换为 Secret 中的 base64 数据。
caBundle: "${CA_BUNDLE}"
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: default
rules:
# 只对 Pod CREATE 返回 JSON Patch,不处理后续 UPDATE。
- operations: ["CREATE"]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
scope: Namespaced
main.go 注册 /mutate 和 /validate 两个 HTTPS endpoint,并从挂载目录读取 TLS 证书:
package main
import (
"log"
"net/http"
"time"
)
func main() {
mux := newServeMux()
server := &http.Server{
Addr: ":8443",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
}
log.Println("admission webhook listening on :8443")
// kube-apiserver 只调用 HTTPS Webhook;证书通常由 Secret 挂载到 /tls。
log.Fatal(server.ListenAndServeTLS("/tls/tls.crt", "/tls/tls.key"))
}
func newServeMux() *http.ServeMux {
mux := http.NewServeMux()
// 两个 WebhookConfiguration 使用不同 path,共用同一个 HTTPS 服务。
mux.Handle("/mutate", admissionHandler(mutatePod))
mux.Handle("/validate", admissionHandler(validatePod))
// HTTPS 健康检查不进入 AdmissionReview 解码逻辑。
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
})
return mux
}
公共 HTTP 入口负责 AdmissionReview 协议:
- 响应中的
uid与request.uid保持一致。 - 请求和响应均使用
admission.k8s.io/v1。 Patch保存 JSON Patch 原始字节。Go 编码AdmissionReview时会把[]byte转为 API 所需的 base64 字符串。
package main
import (
"encoding/json"
"fmt"
"log"
"mime"
"net/http"
admissionv1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type admissionFunc func(*admissionv1.AdmissionRequest) admissionv1.AdmissionResponse
func admissionHandler(handle admissionFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "only POST is supported", http.StatusMethodNotAllowed)
return
}
mediaType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil || mediaType != "application/json" {
http.Error(w, "Content-Type must be application/json", http.StatusUnsupportedMediaType)
return
}
var review admissionv1.AdmissionReview
// 限制请求体大小,避免异常请求占用过多内存。
body := http.MaxBytesReader(w, r.Body, 1<<20)
if err := json.NewDecoder(body).Decode(&review); err != nil {
http.Error(w, fmt.Sprintf("decode AdmissionReview: %v", err), http.StatusBadRequest)
return
}
if review.Request == nil {
http.Error(w, "AdmissionReview.request is required", http.StatusBadRequest)
return
}
response := handle(review.Request)
// AdmissionResponse 必须回显 request.uid,API Server 用它匹配请求和响应。
response.UID = review.Request.UID
responseReview := admissionv1.AdmissionReview{
TypeMeta: metav1.TypeMeta{
APIVersion: admissionv1.SchemeGroupVersion.String(),
Kind: "AdmissionReview",
},
Response: &response,
}
// allowed=false 也是一次成功的 Webhook 调用,因此业务拒绝仍返回 HTTP 200。
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(responseReview); err != nil {
log.Printf("encode AdmissionReview response: %v", err)
}
})
}
func allowed() admissionv1.AdmissionResponse {
return admissionv1.AdmissionResponse{Allowed: true}
}
func denied(code int32, message string) admissionv1.AdmissionResponse {
return admissionv1.AdmissionResponse{
Allowed: false,
Result: &metav1.Status{
Code: code,
Message: message,
},
}
}
/mutate handler 处理没有 label map、已有其他 label 和目标 label 已存在三种情况。JSON Pointer 中 label key 的 / 写成 ~1。
package main
import (
"encoding/json"
"net/http"
admissionv1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
)
const managedLabel = "admission.example.com/managed"
type jsonPatchOperation struct {
Operation string `json:"op"`
Path string `json:"path"`
Value any `json:"value,omitempty"`
}
func mutatePod(request *admissionv1.AdmissionRequest) admissionv1.AdmissionResponse {
var pod corev1.Pod
if err := json.Unmarshal(request.Object.Raw, &pod); err != nil {
return denied(http.StatusBadRequest, "decode Pod: "+err.Error())
}
if pod.Labels[managedLabel] == "true" {
// reinvocationPolicy=IfNeeded 可能再次调用 Webhook,重复执行时不再生成补丁。
return allowed()
}
var patch jsonPatchOperation
if pod.Labels == nil {
// JSON Patch 不能向不存在的父路径添加子字段,先创建完整的 labels map。
patch = jsonPatchOperation{
Operation: "add",
Path: "/metadata/labels",
Value: map[string]string{managedLabel: "true"},
}
} else {
// JSON Pointer 用 ~1 表示 label key 中的斜杠。
patch = jsonPatchOperation{
Operation: "add",
Path: "/metadata/labels/admission.example.com~1managed",
Value: "true",
}
}
patchBytes, err := json.Marshal([]jsonPatchOperation{patch})
if err != nil {
return denied(http.StatusInternalServerError, "encode JSON Patch: "+err.Error())
}
patchType := admissionv1.PatchTypeJSONPatch
return admissionv1.AdmissionResponse{
Allowed: true,
PatchType: &patchType,
// Patch 保存原始 JSON 字节;外层 AdmissionReview 编码时会自动转换为 base64。
Patch: patchBytes,
}
}
TLS、Service、WebhookConfiguration、输入 Pod 和实测输出见 Mutating Webhook 示例。
应用场景:Istio sidecar 自动注入
Istio 的 sidecar 模式会在加入网格的 Pod 中运行一个 istio-proxy 容器。这个容器基于 Envoy,接收重定向后的入站和出站 TCP 流量,并执行服务路由、负载均衡、重试、mTLS 和遥测采集。各 Pod 中的代理从 istiod 获取配置,共同组成 Istio 的数据面。流量重定向规则由 istio-init 或 Istio CNI 写入,具体方式取决于安装配置。详细的组件关系见 Istio Architecture。
自动注入发生在 Pod 的创建阶段。Istio 使用 MutatingWebhookConfiguration 注册 sidecar injector;API Server 根据 namespace 和 Pod label 匹配 Webhook,把 AdmissionReview 发送给 istiod。Webhook 返回的 JSON Patch 会在 PodSpec 中加入 istio-proxy、卷、环境变量和流量重定向所需的配置。直接创建的 Pod,以及由 ReplicaSet、StatefulSet、DaemonSet 或 Job 等控制器创建的 Pod,都按相同规则匹配。
运行下面的示例前,先按照 Install with Istioctl 安装 Istio。命令为 demo namespace 启用自动注入,再创建一个只有 NGINX 容器的 Pod:
kubectl create namespace demo
kubectl label namespace demo istio-injection=enabled --overwrite
kubectl run web \
--namespace demo \
--image nginx:1.27-alpine \
--port 80
kubectl wait \
--namespace demo \
--for=condition=Ready pod/web \
--timeout=120s
注入成功后,原本只有一个应用容器的 Pod 会显示 2/2,容器列表中包含 web 和 istio-proxy:
kubectl get pod web --namespace demo
kubectl get pod web \
--namespace demo \
-o jsonpath='{.spec.containers[*].name}{"\n"}'
Namespace label 只影响之后创建的 Pod。已有 Pod 需要重新创建,才能经过 sidecar injector。注入标签、revision label 和手动注入方式见 Installing the Sidecar。
资源对象校验¶
Mutating Admission 结束后,API Server 调用当前资源的 RESTCreateStrategy.Validate 校验修改后的对象。这些规则属于 Kubernetes API 对象本身的固定约束,由对应资源的服务端实现提供,不是 Admission Controller,也不由集群管理员配置。
对于 apps/v1 Deployment,deploymentStrategy.Validate 调用 ValidateDeployment。校验内容包括副本数不能为负数、selector 必须有效、Deployment strategy 取值合法,以及 Pod template 满足 Deployment 的约束。
spec.selector 与 spec.template.metadata.labels 的匹配检查位于 ValidatePodTemplateSpecForReplicaSet。两者不匹配时,API Server 返回 422 Invalid,Deployment 不会写入 etcd。kubectl 先把 YAML 转换为 API 请求,字段类型和未知字段由 API Server 在更早的解码阶段处理;资源对象校验检查解码后对象的字段取值及字段之间的关系。
ValidatingAdmissionPolicy¶
ValidatingAdmissionPolicy 让集群管理员用 CEL 校验提交给 Kubernetes API 的对象,例如要求对象包含指定标签、限制字段取值,或者检查多个字段之间的关系。校验在 API Server 进程内完成,不需要部署外部 Webhook。
Policy 定义匹配资源和 CEL 校验表达式,ValidatingAdmissionPolicyBinding 决定规则的生效范围与处理动作。Deny 拒绝请求,Warn 向客户端返回警告,Audit 为审计事件添加注解。
CEL 表达式可以读取 object、oldObject 和 request.userInfo 等请求信息。校验依赖镜像扫描结果或其他外部系统数据时,可以使用 Validating Webhook。
配置示例:Deployment 副本数规则
这组配置校验 default 命名空间中 Deployment 的创建和更新请求。Policy 定义 CEL 表达式,Binding 激活 Policy、限定命名空间,并通过 Deny 指定校验失败时的处理动作。
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: deployment-replica-limit.example.com
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
# 创建和修改 Deployment 都需要重新校验副本数。
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
validations:
# CEL 返回 false 时,Binding 中的 Deny 动作使 API Server 拒绝请求。
- expression: object.spec.replicas <= 5
message: Deployment replicas must not exceed 5
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: deployment-replica-limit.example.com
spec:
# Policy 定义校验内容,Binding 决定生效范围和失败动作。
policyName: deployment-replica-limit.example.com
validationActions: [Deny]
matchResources:
# 只校验 default 命名空间中的 Deployment。
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: default
Policy 和 Binding 写入集群后,可以用服务端 dry-run 检查拒绝结果,测试对象不会写入 etcd:
kubectl apply \
-f examples/chapter-01/02-02-admission/03-validating-admission-policy/policy.yaml
kubectl apply \
--dry-run=server \
-f examples/chapter-01/02-02-admission/03-validating-admission-policy/input-denied.yaml
object 是当前 Admission 请求中的 Deployment,表达式结果为 false 时,Binding 中的 Deny 使 API Server 返回拒绝。Policy 没有对应的 Binding 时不生效。完整字段和执行规则见 Validating Admission Policy。
ValidatingAdmissionPolicy:校验 Deployment 副本数 提供了两个可直接运行的 Deployment 清单:replicas: 3 用于验证请求放行,replicas: 6 用于验证 Deny 拒绝。README 还记录了 Kubernetes v1.36.1 的实际命令输出和清理步骤。
Validating Webhook¶
ValidatingAdmissionWebhook 根据 ValidatingWebhookConfiguration 匹配请求,并行调用外部 HTTPS Webhook。Webhook 读取修改阶段生成的最终对象、userInfo 和请求信息,再返回允许或拒绝结果;对象修改由前面的 Mutating Admission 完成。
外部服务可以结合镜像仓库、漏洞扫描结果或其他系统状态执行校验。匹配规则、AdmissionReview 格式和 TLS 配置见 动态准入控制。
配置与代码示例:Pod 安全字段校验
这个示例校验 default 命名空间中 Pod 的创建和更新请求。pods/ephemeralcontainers 子资源也在匹配范围内,因此通过 kubectl debug 添加的 Ephemeral Container 会进入 /validate。Webhook 检查三类容器的 securityContext.privileged 字段,并返回允许或拒绝结果。
clientConfig 与 Mutating Webhook 使用同一个 Service 和 CA,只把调用路径改为 /validate。Validating Webhook 没有 reinvocationPolicy,也不在响应中返回对象补丁。
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: pod-security.example.com
webhooks:
- name: pod-security.example.com
admissionReviewVersions: ["v1"]
sideEffects: None
failurePolicy: Fail
matchPolicy: Equivalent
timeoutSeconds: 5
clientConfig:
service:
namespace: admission-demo
name: admission-webhook
path: /validate
port: 443
# API Server 使用这个 CA 校验 admission-webhook.admission-demo.svc 的证书。
caBundle: "${CA_BUNDLE}"
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: default
rules:
# Ephemeral Container 通过独立子资源更新,需要显式匹配 pods/ephemeralcontainers。
- operations: ["CREATE", "UPDATE"]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods", "pods/ephemeralcontainers"]
scope: Namespaced
/validate handler 解码 AdmissionReview.object 中的 Pod。发现 privileged: true 时,它返回 allowed: false、status.code: 403 和具体容器名称;通过校验时返回 allowed: true。两种结果对应的 HTTP 响应状态都是 200 OK。
package main
import (
"encoding/json"
"fmt"
"net/http"
admissionv1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
)
func validatePod(request *admissionv1.AdmissionRequest) admissionv1.AdmissionResponse {
var pod corev1.Pod
if err := json.Unmarshal(request.Object.Raw, &pod); err != nil {
return denied(http.StatusBadRequest, "decode Pod: "+err.Error())
}
for _, container := range pod.Spec.InitContainers {
if isPrivileged(container.SecurityContext) {
return denied(http.StatusForbidden, fmt.Sprintf("init container %q cannot be privileged", container.Name))
}
}
for _, container := range pod.Spec.Containers {
if isPrivileged(container.SecurityContext) {
return denied(http.StatusForbidden, fmt.Sprintf("container %q cannot be privileged", container.Name))
}
}
// pods/ephemeralcontainers 是独立子资源,WebhookConfiguration 需要显式匹配它。
for _, container := range pod.Spec.EphemeralContainers {
if isPrivileged(container.SecurityContext) {
return denied(http.StatusForbidden, fmt.Sprintf("ephemeral container %q cannot be privileged", container.Name))
}
}
return allowed()
}
func isPrivileged(context *corev1.SecurityContext) bool {
// 未设置 privileged 与显式设置 false 都符合这条示例策略。
return context != nil && context.Privileged != nil && *context.Privileged
}
普通 Pod、Privileged Pod 与 Privileged Ephemeral Container 的输入和实测输出见 Validating Webhook 示例。
应用场景:Kyverno Pod 策略校验
Kyverno 安装 ValidatingWebhookConfiguration,由 Admission Controller 根据策略检查 AdmissionReview.object。下面的 ValidatingPolicy 将普通容器、Init Container 和 Ephemeral Container 合并到 allContainers;任一容器设置 securityContext.privileged: true 时,策略返回拒绝。
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
name: disallow-privileged-containers
spec:
validationActions:
- Deny
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods", "pods/ephemeralcontainers"]
variables:
- name: allContainers
expression: >-
object.spec.containers +
object.spec.?initContainers.orValue([]) +
object.spec.?ephemeralContainers.orValue([])
validations:
- expression: >-
variables.allContainers.all(
c,
c.?securityContext.?privileged.orValue(false) == false
)
message: Privileged containers are not allowed in Pods
validationActions: [Deny] 使 Pod 的创建或更新直接失败。相同机制还可以限制镜像 registry、必填 label、HostPath、Linux capabilities 和资源配置。
Webhook 接收的是 API Server 解码后的对象,因此这类策略检查字段是否存在、字段值和对象之间的关系。YAML 的缩进、键顺序和注释不会进入 AdmissionReview.object,原始文本格式需要由 YAML linter 检查;Kyverno CLI 可以在提交前执行同一组对象规则。
示例使用 Kyverno 当前的 ValidatingPolicy API;旧版 ClusterPolicy 已在当前文档中列为 Deprecated。字段定义和更多规则见 Kyverno ValidatingPolicy。
内置 Admission Controller¶
内置 Admission Controller 编译在 kube-apiserver 中。API Server 通过 --enable-admission-plugins 和 --disable-admission-plugins 管理这些插件。
常见插件包括:
- LimitRanger:根据命名空间中的
LimitRange补充默认资源请求,并校验资源上下限。 - ResourceQuota:检查本次写入是否超过命名空间的资源配额。
- PodSecurity:按照命名空间配置的 Pod Security Standards 执行
enforce、audit和warn。 - ServiceAccount:Pod 未指定
serviceAccountName时填入default,并校验引用的 ServiceAccount。 - RuntimeClass:校验 Pod 引用的 RuntimeClass,并应用对应的 Pod 开销与调度约束。
- NodeRestriction:限制 Kubelet 可以修改的 Node 和 Pod 对象范围。
各插件的默认启用状态、执行阶段和配置参数记录在 Admission Controller 参考文档。
规则来源与处理方式¶
Deployment 自身的固定约束与集群策略使用不同的处理机制:
| 需求 | 处理方式 |
|---|---|
| Deployment 固有约束,例如 selector 与 Pod template label 必须匹配 | 资源对象校验(RESTCreateStrategy.Validate) |
| 命名空间级资源默认值、配额、Pod 安全和节点权限 | 内置 Admission Controller |
| 使用声明式规则修改对象 | MutatingAdmissionPolicy |
| 通过外部服务注入 sidecar 或补充动态配置 | Mutating Webhook |
| 使用声明式规则校验对象 | ValidatingAdmissionPolicy |
| 结合镜像扫描结果或其他外部数据校验对象 | Validating Webhook |
API 版本转换¶
apiVersion 指定客户端请求和响应采用的资源结构;storage version 指定 API Server 写入 etcd 的资源结构。两者不同时,API Server 在读写过程中完成版本转换。同一资源可以同时提供多个对外版本,而 etcd 中的新写入统一使用当前 storage version。
API Server 从不同位置获得内置资源和 Custom Resource 的类型定义,因此两类资源使用不同的转换实现:
-
Kubernetes 内置资源:Deployment、Pod 等资源的外部 Go 类型、内部 Go 类型和转换函数都随 Kubernetes 源码编译进
kube-apiserver。例如,客户端提交的 Deployment 对应apps/v1.Deployment,API Server 内部使用apps/__internal.Deployment。这些类型和转换函数注册在runtime.Scheme中;API Server 通过这份注册表完成请求版本、内部类型和 storage version 之间的转换。整个过程在 API Server 进程内完成,相关概念见 Storage Versions。 -
CRD 提供的 Custom Resource:标准 CRD 不能把资源专用的 Go 类型和转换函数注册到
kube-apiserver的runtime.Scheme。API Server 使用unstructured.Unstructured保存解码后的字段,也就是以通用的键值结构表示对象;CRD schema 仍然负责字段默认值、裁剪和校验。spec.conversion.strategy只支持None和Webhook:None在 API Server 进程内执行,但只修改apiVersion,不属于 typed conversion;Webhook通过ConversionReview调用外部 HTTPS 服务。Kubebuilder 生成的 Go 类型和 Hub/Spoke 转换方法运行在 Webhook 进程中,不会被动态加载到kube-apiserver。具体行为见 Versions in CustomResourceDefinitions。
如果自定义 API 必须在提供该 API 的进程内完成 typed conversion,可以实现 Extension API Server,并通过 API Aggregation Layer 接入 Kubernetes。外部版本、内部类型和转换函数会注册到 Extension API Server 自己的 runtime.Scheme;这种资源由 Aggregated API 提供,不再是 CRD。
CRD 转换能力的版本历史
| Kubernetes 版本 | CRD 的版本与转换能力 |
|---|---|
v1.7–v1.10 |
CRD 只有一个 spec.version,不存在跨版本转换。 |
v1.11–v1.12 |
CRD 开始支持多个版本,但不提供字段转换,只适合 schema 保持一致的版本升级。 |
v1.13–v1.14 |
Conversion Webhook 进入 Alpha,CRD 的转换策略增加 None 和 Webhook。 |
v1.15 |
Conversion Webhook 进入 Beta。 |
v1.16 |
Conversion Webhook 进入 Stable。 |
早期的多版本 CRD 允许同一种资源通过多个 API version 访问。API Server 在版本之间只替换 apiVersion,其他字段保持不变;字段改名、嵌套结构调整和语义变化都无法转换。这个行为相当于现在的 conversion.strategy: None。
版本转换解决的问题¶
API 发布后仍会继续演进,常见变化包括字段改名、字段拆分、嵌套结构调整和默认值变化。已经部署的客户端可能继续提交旧版本,etcd 中也可能保留由旧 storage version 写入的对象。
以 ModelServer 为例,v1beta1 和 v1 表达相同的模型、服务副本和端口,但字段结构不同:
| 语义 | v1beta1 |
v1 |
|---|---|---|
| 模型 | spec.modelRef |
spec.model.name |
| 副本数 | spec.scale.replicas |
spec.replicas |
| HTTP 端口 | spec.http.port |
spec.server.port |
客户端通过 v1beta1 创建对象时,API Server 调用 Conversion Webhook,把对象转换成当前 storage version v1 后写入 etcd。客户端再次通过 v1beta1 读取时,API Server 从 etcd 取得 v1 对象,再调用 Webhook 转换为 v1beta1 响应。
CRD 版本字段¶
CRD 在 spec.versions 中分别控制 REST endpoint 和 etcd 写入格式:
served:true时,API Server 为该版本提供 REST endpoint,并把它发布到 API discovery。storage:指定 Custom Resource 写入 etcd 时使用的版本。一个 CRD 必须且只能有一个storage: true的版本。status.storedVersions:记录 etcd 中可能仍然存在的 storage version。完成数据迁移后才能从这里和spec.versions中移除旧版本。
同一个版本可以同时设置 served: false 和 storage: false。API Server 不会为它安装 REST handler,也不会用它写入 etcd;CRD 仍需保留该版本的 structural schema。
转换机制¶
| 资源与策略 | 转换位置 | 适用范围 |
|---|---|---|
| Kubernetes 内置资源 | API Server 进程内的 typed conversion | Deployment、Pod 等编译进 Kubernetes 的 API |
CRD None |
API Server 只替换 apiVersion |
各版本字段结构和语义相同 |
CRD Webhook |
外部 HTTPS Conversion Webhook | 字段改名、拆分、合并或语义变化 |
内置资源的 typed conversion¶
Deployment 的外部版本是 apps/v1.Deployment,API Server 使用的内部类型是 apps/__internal.Deployment。请求按 apps/v1 解码并应用该版本的默认值,再通过 runtime.Scheme 转换成内部类型。Storage Codec 写入 etcd 前把内部对象转换成 storage version;读取时先恢复内部类型,再按客户端访问的 served version 编码响应。
flowchart LR
W["apps/v1 wire object"] --> D["Decode + apps/v1 defaults"]
D --> I["apps/__internal Deployment"]
I --> R["Registry / REST Strategy"]
R --> S["Storage Codec"]
S --> E["etcd storage version"]
E --> S2["Storage Codec"]
S2 --> I2["apps/__internal Deployment"]
I2 --> O["requested served version"]
相关代码按职责分布在以下目录:
| 路径 | 作用 |
|---|---|
staging/src/k8s.io/api/apps/v1/types.go |
对外发布的 apps/v1 Go 类型 |
pkg/apis/apps/types.go |
API Server 使用的 apps/__internal 类型 |
pkg/apis/apps/v1/zz_generated.conversion.go |
conversion-gen 生成的字段转换 |
pkg/apis/apps/v1/conversion.go |
生成器无法表达的手写兼容逻辑 |
pkg/apis/apps/install/install.go |
将内部类型、外部版本和转换函数注册到同一个 Scheme |
内置资源的 served version 与 API Server 能解码的历史版本不完全相同。旧 endpoint 被移除后,请求无法再访问该版本;API Server 仍可保留相应类型和转换函数,用于读取升级前写入的历史数据。
CRD 的 None 策略¶
None 是 CRD 的默认转换策略。API Server 保留对象字段,只把返回对象的 apiVersion 改成目标版本。这个方式不处理字段改名或结构变化,适用于各版本 schema 相同的 CRD。
版本之间存在字段差异时,None 可能让目标版本得到缺失字段,或者让 unknown-field pruning 删除无法识别的字段。此类 API 需要 Conversion Webhook。
CRD 的 Webhook 策略¶
Webhook 策略通过 CRD 的 spec.conversion 指向 HTTPS 服务。API Server 使用 ConversionReview 传递转换请求:
request.objects保存待转换的源对象。request.desiredAPIVersion指定目标版本。- 响应按输入顺序返回相同数量的对象,并保留 name、namespace 和 UID。
{
"apiVersion": "apiextensions.k8s.io/v1",
"kind": "ConversionReview",
"request": {
"uid": "2b9f5bb9-6fc5-4b3f-a730-4e9d39b0f0a4",
"desiredAPIVersion": "ai.course.example.com/v1",
"objects": [
{
"apiVersion": "ai.course.example.com/v1beta1",
"kind": "ModelServer",
"metadata": {"name": "qwen-demo", "namespace": "default"},
"spec": {
"modelRef": "Qwen/Qwen3-0.6B",
"scale": {"replicas": 1},
"http": {"port": 8000}
}
}
]
}
}
Conversion Webhook 只负责不同 API 版本之间的等价转换。字段合法性由各版本的 OpenAPI schema 和 Admission 规则处理;转换失败不应被用作准入策略。
Conversion Webhook 的 Hub 设计¶
CRD 需要转换字段结构时,三种方案都配置 conversion.strategy: Webhook,API Server 按相同协议调用外部 Webhook。下表比较的是 Webhook 内部的三种 Hub/Spoke 组织方案,并不是三种并列的 CRD 转换机制。
| Hub 方案 | Hub 的位置 | 版本演进方式 |
|---|---|---|
| 固定的非公开 CRD 版本 | Hub 保留在 CRD 的 spec.versions 中,并设置 served: false、storage: false |
对外版本和 storage version 持续演进,所有版本都与固定 Hub 相互转换 |
| 最新公开版本 | 当前最新版本同时作为 Hub,通常也作为 storage version | 发布新版本时更换 Hub 并更新转换代码;同时切换 storage version 时迁移存量对象 |
| Webhook 内部类型 | Hub 只定义在 Webhook Server 的 Go 代码中,不出现在 CRD 中 | 对外版本和 storage version 可以独立演进,源版本先转为内部类型,再转为目标版本 |
三种方案在 CRD 中都启用同一种转换策略:
下图中的双向箭头表示 Webhook Server 内部维护的转换函数。API Server 只提交源对象和目标 apiVersion,不参与 Hub/Spoke 转换。
固定的非公开 CRD 版本¶
固定 Hub 保留在 CRD 的 spec.versions 中,通过 served: false 关闭 REST endpoint,通过 storage: false 排除 etcd 写入。对外版本和 storage version 可以继续演进,每个新版本只需增加与固定 Hub 之间的双向转换。
flowchart LR
B["v1beta1<br/>served=true"] <--> H["v1alpha1 Hub<br/>declared in CRD<br/>served=false<br/>storage=false"]
V["v1<br/>served=true<br/>storage=true"] <--> H
最新公开版本¶
最新公开版本同时作为 Hub,通常也作为 storage version。发布 v2 后,Hub 从 v1 切换到 v2,原来的 v1 变成 Spoke,Webhook 需要补充旧版本与新 Hub 之间的转换。如果同时把 storage version 切换到 v2,还要另行迁移以旧版本保存的存量对象。
flowchart LR
subgraph R1["发布 v2 之前"]
B1["v1beta1 Spoke"] <--> H1["v1 Hub<br/>served=true<br/>storage=true"]
end
subgraph R2["发布 v2 之后"]
B2["v1beta1 Spoke"] <--> H2["v2 Hub<br/>served=true<br/>storage=true"]
V1["v1 Spoke"] <--> H2
end
H1 -. "Hub 随最新版本切换" .-> H2
Webhook 内部类型¶
内部 Hub 只存在于 Webhook Server 的 Go 代码中,不列入 CRD 的 spec.versions。Webhook 把源版本转换成内部类型,再由内部类型转换成目标版本;对外版本和 storage version 的变化不会改变 Hub 的 API 可见性。
flowchart LR
subgraph C["CRD spec.versions"]
B["v1beta1<br/>served=true"]
V["v1<br/>served=true<br/>storage=true"]
end
subgraph W["Conversion Webhook process"]
H["internal.ModelServer Hub<br/>not declared in CRD"]
end
B <--> H
V <--> H
Kubebuilder Multi-Version API 介绍了 controller-runtime 的 Hub/Spoke 接口;Kubernetes 多版本 API 转换最佳实践 对三种 Hub 结构给出了进一步比较。
Webhook conversion 的实现¶
本仓库的 Kubebuilder 示例采用固定的非公开 CRD 版本作为 Hub。v1alpha1 保留在 CRD 中,但不提供 REST endpoint,也不作为 storage version;v1beta1 和 v1 分别实现与该 Hub 之间的双向转换。
| 版本 | served |
storage |
作用 |
|---|---|---|---|
v1alpha1 |
false |
false |
CRD 中声明但不对外提供的固定 Hub |
v1beta1 |
true |
false |
使用分组字段的公开版本 |
v1 |
true |
true |
稳定公开版本和当前 etcd 存储版本 |
flowchart LR
S["Source<br/>v1beta1.ModelServer"] --> C1["v1beta1.ConvertTo()"]
C1 --> H["v1alpha1.ModelServer<br/>conversion.Hub"]
H --> C2["v1.ConvertFrom()"]
C2 --> T["Target<br/>v1.ModelServer"]
v1alpha1 在 controller-runtime 中实现 conversion.Hub。API Server 发送的 ConversionReview 只包含源对象和目标 apiVersion,不会请求 v1alpha1 endpoint。controller-runtime 根据源对象和目标版本构造两个 Spoke,在 Webhook 进程内依次调用源版本的 ConvertTo() 和目标版本的 ConvertFrom()。反向转换使用相同的 Hub 和相反的 Spoke 顺序。
实现位于 examples/chapter-01/02-03-crd-conversion。项目使用 Kubebuilder v4.15.0、controller-runtime v0.24.1 和 controller-tools v0.21.0。
以下命令为 v1alpha1 生成 Hub,为两个公开版本生成 Spoke 方法和 /convert Webhook 配置:
kubebuilder create webhook \
--group ai \
--version v1alpha1 \
--kind ModelServer \
--conversion \
--spoke v1beta1,v1
Kubebuilder v4.15.0 会给 Hub 自动添加 +kubebuilder:storageversion。本例删除该 marker,给 v1alpha1 添加 +kubebuilder:unservedversion:
// +kubebuilder:object:root=true
// +kubebuilder:unservedversion
// +kubebuilder:subresource:status
// ModelServer 是转换专用 Hub,不提供 REST API,也不作为 storage version。
type ModelServer struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ModelServerSpec `json:"spec"`
Status ModelServerStatus `json:"status,omitempty"`
}
v1 根类型使用 +kubebuilder:storageversion,生成 CRD 时成为唯一 storage version:
// +kubebuilder:object:root=true
// +kubebuilder:storageversion
// +kubebuilder:subresource:status
// ModelServer 是 v1 API 的根类型,也是当前 storage version。
type ModelServer struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ModelServerSpec `json:"spec"`
Status ModelServerStatus `json:"status,omitempty"`
}
v1alpha1 通过空的 Hub() 方法实现 conversion.Hub。v1beta1 和 v1 实现 conversion.Convertible:
package v1alpha1
import "sigs.k8s.io/controller-runtime/pkg/conversion"
var _ conversion.Hub = &ModelServer{}
// Hub 将 v1alpha1 标记为 ModelServer 的唯一转换中心。
func (*ModelServer) Hub() {}
v1beta1 的转换函数展示了两个方向的字段映射。类型断言失败时返回明确错误,ObjectMeta 使用 DeepCopy,避免转换结果与源对象共享 map 或 slice:
package v1beta1
import (
"fmt"
"sigs.k8s.io/controller-runtime/pkg/conversion"
v1alpha1 "github.com/cr7258/course/examples/chapter-01/02-03-crd-conversion/api/v1alpha1"
)
var _ conversion.Convertible = &ModelServer{}
// ConvertTo 把 v1beta1 的分组字段写入 Hub 的规范化表示。
func (src *ModelServer) ConvertTo(dstRaw conversion.Hub) error {
dst, ok := dstRaw.(*v1alpha1.ModelServer)
if !ok {
return fmt.Errorf("expected *v1alpha1.ModelServer, got %T", dstRaw)
}
dst.ObjectMeta = *src.ObjectMeta.DeepCopy()
dst.Spec.ModelID = src.Spec.ModelRef
dst.Spec.ReplicaCount = src.Spec.Scale.Replicas
dst.Spec.HTTPPort = src.Spec.HTTP.Port
dst.Status.Phase = src.Status.Phase
return nil
}
// ConvertFrom 把 Hub 的规范化字段恢复成 v1beta1 的分组字段。
func (dst *ModelServer) ConvertFrom(srcRaw conversion.Hub) error {
src, ok := srcRaw.(*v1alpha1.ModelServer)
if !ok {
return fmt.Errorf("expected *v1alpha1.ModelServer, got %T", srcRaw)
}
dst.ObjectMeta = *src.ObjectMeta.DeepCopy()
dst.Spec.ModelRef = src.Spec.ModelID
dst.Spec.Scale.Replicas = src.Spec.ReplicaCount
dst.Spec.HTTP.Port = src.Spec.HTTPPort
dst.Status.Phase = src.Status.Phase
return nil
}
manager 将三个版本注册到同一个 Scheme。Hub 即使不提供 REST endpoint,也必须注册,否则 controller-runtime 无法构造 Hub 对象:
utilruntime.Must(aiv1alpha1.AddToScheme(scheme))
utilruntime.Must(aiv1beta1.AddToScheme(scheme))
utilruntime.Must(aiv1.AddToScheme(scheme))
Webhook manager 为 ModelServer 注册 controller-runtime 的 conversion handler:
package v1alpha1
import (
ctrl "sigs.k8s.io/controller-runtime"
aiv1alpha1 "github.com/cr7258/course/examples/chapter-01/02-03-crd-conversion/api/v1alpha1"
)
// SetupModelServerWebhookWithManager 为 ModelServer 注册 conversion handler。
func SetupModelServerWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr, &aiv1alpha1.ModelServer{}).
Complete()
}
CRD 的 conversion 配置指向 Webhook Service 的 /convert。Conversion Webhook 使用 CRD 字段,不创建 MutatingWebhookConfiguration 或 ValidatingWebhookConfiguration:
# The following patch enables a conversion webhook for the CRD
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: modelservers.ai.course.example.com
spec:
conversion:
strategy: Webhook
webhook:
clientConfig:
service:
namespace: system
name: webhook-service
path: /convert
conversionReviewVersions:
- v1
下图展示了客户端通过 v1beta1 更新对象、API Server 以 v1 写入 etcd,再把响应转换回 v1beta1 的过程:
sequenceDiagram
participant C as Client
participant A as API Server
participant W as Conversion Webhook
participant E as etcd
C->>A: PUT ai.course.example.com/v1beta1
A->>W: ConversionReview(objects=v1beta1, desiredAPIVersion=v1)
Note over W: v1beta1.ConvertTo(v1alpha1 Hub)
Note over W: v1.ConvertFrom(v1alpha1 Hub)
W-->>A: convertedObjects=v1
A->>E: store ai.course.example.com/v1
E-->>A: stored v1 object + revision
A->>W: ConversionReview(objects=v1, desiredAPIVersion=v1beta1)
Note over W: v1.ConvertTo(v1alpha1 Hub)
Note over W: v1beta1.ConvertFrom(v1alpha1 Hub)
W-->>A: convertedObjects=v1beta1
A-->>C: v1beta1 response
读取方向使用相同协议。客户端访问 v1beta1 endpoint 时,API Server 从 etcd 读取 v1,再请求 Webhook 转换为 v1beta1。LIST 请求可以在一份 ConversionReview 中携带多个对象,Webhook 必须保持输入顺序和对象数量。
Conversion Webhook 位于 Custom Resource 的读写路径。服务不可用、证书失效或返回对象不符合协议时,需要转换的读取和写入都会失败。生产部署需要覆盖以下运行条件:
- Webhook 使用多个可用副本。
- 服务端证书能够自动轮换。
- 转换延迟和超时受到监控。
- 协议错误和调用失败触发告警。
仓库中的 verify-kind.sh 创建独立的 crd-conversion-demo Kind 集群,生成临时 TLS 证书并部署 Webhook。脚本检查以下结果:
v1alpha1不出现在 API discovery,v1beta1和v1都有 REST endpoint。- 对象通过
v1beta1创建后,可以通过v1读取。 - 对象通过
v1更新后,v1beta1和v1返回相同副本数。 - control-plane 中的 etcd 原始值使用
ai.course.example.com/v1。 - CRD 的
status.storedVersions只有v1。
Kubernetes v1.36.1 的实测结果如下。输入对象、完整输出、依赖版本和清理命令记录在 CRD 多版本转换示例。
v1alpha1 REST endpoint: not served
Created with v1beta1: modelRef=Qwen/Qwen3-0.6B scale.replicas=1 http.port=8000
Read with v1: model.name=Qwen/Qwen3-0.6B replicas=1 server.port=8000
Updated with v1: replicas=3
Read after update: v1beta1.scale.replicas=3 v1.replicas=3
etcd apiVersion: ai.course.example.com/v1
CRD status.storedVersions: v1
Conversion webhook verification passed.
Storage version 迁移¶
把 storage: true 从 v1beta1 切换到 v1,只影响之后的写入。etcd 中已有对象仍保持 v1beta1 编码,CRD 的 status.storedVersions 会继续记录曾经用作存储的版本。
切换 storage version 后不必立即重写旧对象。只要 CRD 仍保留旧版本,并且 Conversion Webhook 支持新旧版本之间的转换,API Server 就能读取旧对象,并按客户端请求的版本返回。此时旧对象仍依赖旧版本 schema 和对应的转换代码,版本迁移还没有完成。
从 CRD 的 spec.versions 删除旧版本,或者从 Webhook 删除旧版本的转换代码之前,必须先把已有对象重写为当前 storage version。Kubernetes v1.36 提供 Beta 的 StorageVersionMigration API,默认关闭。启用时,需要在 kube-apiserver 和 kube-controller-manager 打开 StorageVersionMigrator feature gate,并在 kube-apiserver 启用 storagemigration.k8s.io/v1beta1 API。
无法启用该功能时,可以按照 CustomResourceDefinition 的版本 中的手动迁移流程,列出并重新写入全部现有对象,使 API Server 按当前 storage version 保存。确认迁移完成后,再更新 status.storedVersions。
仅切换 storage version 不会让 status.storedVersions 自动删除旧条目。对象重写完成后检查该字段;如果迁移工具没有更新它,再显式保留当前版本:
kubectl patch crd modelservers.ai.course.example.com \
--subresource=status \
--type=merge \
-p '{"status":{"storedVersions":["v1"]}}'
存储迁移与客户端迁移是两项独立工作。仍有客户端访问旧 API 时,可以继续保留该 served version;所有客户端完成迁移后,再把旧版本设为 served: false。旧版本既不再 served,也不再出现在 status.storedVersions 后,才可以从 CRD schema 和 Webhook 转换代码中删除。
对象版本¶
以下命令读取 vllm-demo Deployment 的对象标识和版本字段:
kubectl get --raw \
'/apis/apps/v1/namespaces/default/deployments/vllm-demo' \
| jq '{
name: .metadata.name,
uid: .metadata.uid,
resourceVersion: .metadata.resourceVersion,
generation: .metadata.generation
}'
metadata.uid 标识一个对象实例;同名对象删除重建后会得到新的 UID。resourceVersion 的顺序、读取路径与续接方式见 etcd Watch 机制。对于 Deployment,PrepareForUpdate 在 spec 或 metadata.annotations 变化时递增 generation。
核心源码¶
API Server 在启动时组装服务、注册资源路由,并创建准入插件与存储对象。请求到达后,HTTP 过滤器补充请求信息并执行认证、流控和授权;资源 Handler 负责解码与准入,Registry 组织资源校验和读写,Storage 实现最终的数据访问。
以下调用关系以 Kubernetes v1.36.4 源码为依据,写入路径使用内置的 apps/v1 Deployment。源码片段保留关键字段和调用,使用 ... 省略与当前机制无关的分支。
- 服务组成与启动:
CreateServerChain组装 APIExtensionsServer、KubeAPIServer 和 APIAggregator,PrepareRun与Run完成服务准备和监听。 - API 注册与请求路由:资源提供者创建 REST Storage,
InstallREST根据它实现的接口安装 GET、LIST、CREATE、UPDATE 等操作对应的 HTTP Handler。 - HTTP 过滤器链:
DefaultBuildHandlerChain将请求信息、认证、审计、流控和授权包装在路由外层,请求通过这些过滤器后进入资源 Handler。 - 对象解码与创建:
createHandler把请求体解码为内部对象,执行 Mutating Admission,并把对象和校验回调交给资源的Create方法。 - 准入插件管理与调用:
Plugins保存插件工厂,NewFromPlugins创建有序插件链;创建请求在 Handler 中调用Admit,在 Registry 中通过回调调用Validate。 - REST Strategy 与通用 Registry:Deployment Strategy 提供资源规则,
Store复用通用创建、更新和读取流程,并连接底层 Storage。 - 对象持久化与响应:Storage 对对象编码并执行 etcd 写入,再把保存结果交回 Handler;服务端 dry-run 执行准入与校验后返回对象。
- 更新、补丁与并发控制:PUT 与 PATCH 汇入
Store.Update,通过GuaranteedUpdate处理当前对象、更新回调和条件写入。 - 读取、监听与 Watch Cache:
CacheDelegator根据请求语义选择缓存或底层存储,Cacher提供缓存读取与事件流,后台 LIST/WATCH 持续更新缓存。
服务组成与启动¶
kube-apiserver 在同一进程内组合 CRD、内置资源和聚合 API 三套服务,通过委派链把请求交给对应的处理器。服务构建完成后,进程准备 OpenAPI 与健康检查,再启动 HTTPS 监听和启动后钩子。
服务构建与委派
CreateServerChain 按依赖顺序创建三个服务,每次把已经创建的服务作为下一层的委派目标:
- APIExtensionsServer:安装 CRD API,并为自定义资源提供处理入口;委派链末端连接未找到资源的处理器。
- KubeAPIServer:安装 Pod、Deployment 等内置资源 API;将 APIExtensionsServer 作为委派目标。
- APIAggregator:管理 APIService 并接入扩展 API 服务;将 KubeAPIServer 作为委派目标,最终返回给进程入口。
构建顺序是 APIExtensionsServer → KubeAPIServer → APIAggregator。请求从最外层 APIAggregator 进入,本层未接管的路径沿相反方向委派;命中内置资源的处理器后,执行对应的 REST 操作。聚合 API 的代理路径由 APIService 配置决定。
三个构造调用分别接收下一层 Handler,并返回当前层服务对象:
func CreateServerChain(config CompletedConfig) (*aggregatorapiserver.APIAggregator, error) {
notFoundHandler := notfoundhandler.New(
config.KubeAPIs.ControlPlane.Generic.Serializer,
genericapifilters.NoMuxAndDiscoveryIncompleteKey,
)
// 先创建委派链最内层的 CRD 服务。
apiExtensionsServer, err := config.ApiExtensions.New(
genericapiserver.NewEmptyDelegateWithCustomHandler(notFoundHandler),
)
...
crdAPIEnabled := config.ApiExtensions.GenericConfig.MergedResourceConfig.ResourceEnabled(
apiextensionsv1.SchemeGroupVersion.WithResource("customresourcedefinitions"),
)
// 内置资源服务把未接管的路径委派给 CRD 服务。
kubeAPIServer, err := config.KubeAPIs.New(apiExtensionsServer.GenericAPIServer)
...
// 聚合服务位于最外层,接收进程的请求入口。
aggregatorServer, err := controlplaneapiserver.CreateAggregatorServer(
config.Aggregator,
kubeAPIServer.ControlPlane.GenericAPIServer,
apiExtensionsServer.Informers.Apiextensions().V1().CustomResourceDefinitions(),
crdAPIEnabled,
apiVersionPriorities,
)
...
return aggregatorServer, nil
}
三个服务通过进程内 Handler 连接。通用服务的构造代码 将下一层的 UnprotectedHandler() 传给 NewAPIServerHandler,后者把它设置为路由未匹配时的处理入口。委派使用下一层的路由 Handler,已执行的外层过滤器链无需再次运行。
监听与启动后钩子
Run 读取选项、完成配置并创建委派链,再调用返回对象的 PrepareRun 和 Run:
func Run(ctx context.Context, opts options.CompletedOptions) error {
...
config, err := NewConfig(opts)
...
completed, err := config.Complete()
...
server, err := CreateServerChain(completed)
...
// API 安装完成后,准备服务运行所需的路由和检查。
prepared, err := server.PrepareRun()
...
// 运行返回的最外层聚合服务。
return prepared.Run(ctx)
}
PrepareRun:聚合服务调用通用服务的PrepareRun。通用实现先递归准备委派目标,再安装 OpenAPI、healthz、livez和readyz;聚合服务随后准备聚合 OpenAPI。RunWithContext:preparedAPIAggregator.Run将调用交给通用服务,后者组织运行及关闭阶段,并调用NonBlockingRunWithContext启动监听。NonBlockingRunWithContext:先通过SecureServingInfo.Serve启动 HTTPS 服务,再调用RunPostStartHooks,为每个钩子启动独立 goroutine,启动 Informer、控制器等后台任务。
启动后钩子与 HTTP 服务并发运行;是否可以接收正常流量,由就绪检查反映。监听端口已启动与全部初始化任务完成是两个不同的状态。
API 注册与请求路由¶
内置资源在启动时把 REST Storage 注册到 API 组,再由安装器根据 Storage 实现的 Go 接口创建 HTTP 路由。以 Deployment 为例,POST /apis/apps/v1/namespaces/default/deployments 最终匹配创建路由,并调用持有 Deployment Storage 的处理函数。
资源与 Storage 映射
StorageProvider.NewRESTStorage 创建 apps API 组信息,调用 v1Storage 收集资源,再把返回值写入 APIGroupInfo.VersionedResourcesStorageMap["v1"]。这个字段的类型是 map[string]map[string]rest.Storage,两层键分别为 API 版本和资源路径。
deployments、deployments/status 和 deployments/scale 是三个独立的映射项,分别连接主资源、状态子资源和扩缩容子资源的 REST 实现。deploymentstore.NewStorage 在内部调用 NewREST,再组装这些实现;请求路由安装器接收的是这组资源处理接口。
v1Storage 在 Deployment API 启用时创建这些 Storage,并按资源路径写入映射:
func (p StorageProvider) v1Storage(
apiResourceConfigSource serverstorage.APIResourceConfigSource,
restOptionsGetter generic.RESTOptionsGetter,
) (map[string]rest.Storage, error) {
storage := map[string]rest.Storage{}
if resource := "deployments"; apiResourceConfigSource.ResourceEnabled(
appsapiv1.SchemeGroupVersion.WithResource(resource),
) {
deploymentStorage, err := deploymentstore.NewStorage(restOptionsGetter)
if err != nil {
return storage, err
}
// 为主资源和子资源分别提供 REST 实现。
storage[resource] = deploymentStorage.Deployment
storage[resource+"/status"] = deploymentStorage.Status
storage[resource+"/scale"] = deploymentStorage.Scale
}
...
return storage, nil
}
InstallAPIs 收集各资源提供者返回的 APIGroupInfo,将具名 API 组交给 InstallAPIGroups。随后按以下层次完成安装:
InstallAPIGroups:检查 API 组与版本信息,准备 OpenAPI 类型信息,再调用installAPIResources。installAPIResources:遍历组内启用的版本,创建对应的APIGroupVersion,将资源映射、编解码器等配置交给版本级安装器。APIGroupVersion.InstallREST:组合/apis/apps/v1前缀,创建APIInstaller,安装资源路由和版本发现接口,再把 WebService 加入GoRestfulContainer。APIInstaller.Install:按排序后的资源路径遍历 Storage,逐项调用registerResourceHandlers。
接口能力与处理函数
registerResourceHandlers 读取资源作用域、类型和 Storage 的接口能力,再决定可以安装哪些操作。例如,rest.Creater 提供创建能力,rest.Getter 提供单对象读取能力,rest.Lister 提供列表读取能力;安装器据此生成 POST、GET 等路由。
对于 Deployment 的创建操作,安装器为 POST 选择 restfulCreateResource,再通过 ws.POST(action.Path).To(handler) 将处理函数绑定到资源路径。RequestScope 随处理函数一起传入,保存资源身份、对象类型、编解码器、名称与命名空间解析方式和字段管理等请求处理上下文。
安装器通过接口类型断言取得创建能力,再将它绑定到 POST 路由的闭包中:
func (a *APIInstaller) registerResourceHandlers(
path string, storage rest.Storage, ws *restful.WebService,
) (*metav1.APIResource, *storageversion.ResourceInfo, error) {
...
// 资源实现的接口决定可以安装哪些操作。
creater, isCreater := storage.(rest.Creater)
namedCreater, isNamedCreater := storage.(rest.NamedCreater)
lister, isLister := storage.(rest.Lister)
getter, isGetter := storage.(rest.Getter)
...
for _, action := range actions {
...
switch action.Verb {
...
case request.MethodPost:
var handler restful.RouteFunction
if isNamedCreater {
handler = restfulCreateNamedResource(namedCreater, reqScope, admit)
} else {
handler = restfulCreateResource(creater, reqScope, admit)
}
...
}
}
...
}
func restfulCreateResource(
r rest.Creater, scope handlers.RequestScope, admit admission.Interface,
) restful.RouteFunction {
return func(req *restful.Request, res *restful.Response) {
// 请求命中路由后,进入通用创建 Handler。
handlers.CreateResource(r, &scope, admit)(res.ResponseWriter, req.Request)
}
}
注册阶段建立“路径、HTTP 方法、REST 实现”的对应关系;请求到达时,闭包把原始 HTTP 请求、响应写入器和预先保存的 Storage 交给 handlers.CreateResource。对象解码、准入和持久化由后续创建路径完成。
HTTP 过滤器链¶
请求进入资源 Handler 前,先经过一组嵌套的 http.Handler。这些过滤器解析请求身份、补充 Context、执行访问检查或排队,并在条件满足时调用下一层。DefaultBuildHandlerChain 从资源路由向外包装 Handler,因此源码中的包装顺序与请求进入顺序相反。
包装顺序与请求顺序
DefaultBuildHandlerChain 先用 WithAuthorization 包装 apiHandler,再向外加入 APF、用户模拟、审计和认证等过滤器。例如先构造 Authorization(apiHandler),再构造 Authentication(Authorization(apiHandler)) 时,请求会先执行 Authentication。
图中保留的主要执行顺序为:WithAuditInit → WithRequestInfo → WithAuthentication → WithAudit → WithConstrainedImpersonation → WithPriorityAndFairness → WithAuthorization → apiHandler。其中 APF 排队发生在操作授权检查之前;日志、超时、Tracing 等过滤器穿插在完整链中。
APF 已配置、受约束用户模拟开启时,主要包装调用如下。ConstrainedImpersonation 在所述版本中默认开启,其他分支及辅助过滤器用 ... 省略。
func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler {
handler := apiHandler
...
// 先包装靠近资源路由的授权层。
handler = genericapifilters.WithAuthorization(handler, c.Authorization.Authorizer, c.Serializer)
...
if c.FlowControl != nil {
workEstimatorCfg := flowcontrolrequest.DefaultWorkEstimatorConfig()
requestWorkEstimator := flowcontrolrequest.NewWorkEstimator(
c.StorageObjectCountTracker.Get, c.FlowControl.GetInterestedWatchCount,
workEstimatorCfg, c.FlowControl.GetMaxSeats,
)
...
handler = genericfilters.WithPriorityAndFairness(
handler, c.LongRunningFunc, c.FlowControl,
requestWorkEstimator, c.RequestTimeout/4,
)
...
}
...
if c.FeatureGate.Enabled(genericfeatures.ConstrainedImpersonation) {
handler = impersonation.WithConstrainedImpersonation(
handler, c.Authorization.Authorizer, c.Serializer,
)
...
}
...
handler = genericapifilters.WithAudit(
handler, c.AuditBackend, c.AuditPolicyRuleEvaluator, c.LongRunningFunc,
)
...
failedHandler := genericapifilters.Unauthorized(c.Serializer)
failedHandler = genericapifilters.WithFailedAuthenticationAudit(
failedHandler, c.AuditBackend, c.AuditPolicyRuleEvaluator,
)
...
handler = genericapifilters.WithAuthentication(
handler, c.Authentication.Authenticator, failedHandler,
c.Authentication.APIAudiences, c.Authentication.RequestHeaderConfig,
)
...
// 后包装的层先收到请求,为内层提供请求信息与审计上下文。
handler = genericapifilters.WithRequestInfo(handler, c.RequestInfoResolver)
...
handler = genericapifilters.WithAuditInit(handler)
return handler
}
Context 与短路返回
过滤器通过请求 Context 传递已经解析的数据。WithRequestInfo 调用 NewRequestInfo,将 URL 和 HTTP 方法转换为结构化的资源操作。例如,向 /apis/apps/v1/namespaces/default/deployments 发送 POST,会得到 API 组 apps、版本 v1、命名空间 default、资源 deployments 和逻辑操作 create。
过滤器把 RequestInfo 写入新 Context,再调用内层 Handler:
func WithRequestInfo(handler http.Handler, resolver request.RequestInfoResolver) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
info, err := resolver.NewRequestInfo(req)
...
// 使用携带 RequestInfo 的新 Context 继续执行内层 Handler。
req = req.WithContext(request.WithRequestInfo(ctx, info))
handler.ServeHTTP(w, req)
})
}
认证成功后,WithAuthentication 把用户信息写入同一个请求的 Context。后续用户模拟、APF 和授权过滤器据此读取请求者及资源操作;授权通过后才继续进入 apiHandler。
发生拒绝时,过滤器直接写入响应并返回。认证链未返回有效身份时调用 failedHandler,由 Unauthorized 生成 401 Unauthorized 响应;包装它的 WithFailedAuthenticationAudit 按审计配置记录这条失败路径。操作授权明确拒绝时返回 403 Forbidden。这些分支都在资源 Handler 执行之前结束请求。
对象解码与创建¶
创建 Handler 把 HTTP 请求体转换成资源的内部对象,执行 Mutating Admission,再把对象和校验回调交给 REST Storage。以 Deployment 为例,客户端提交 apps/v1 对象,Handler 和 Registry 通过 runtime.Object 接口传递内存中的 *apps.Deployment,具体资源规则由后续的 Deployment Strategy 执行。
请求上下文与对象解码
同一套 Handler 需要服务不同资源、版本和子资源。RequestScope 保存这条资源路由所需的处理能力:
Resource、Kind、Subresource标识当前资源、对象类型和子资源。Namer从请求中取得名称与命名空间;Serializer参与输入、输出格式协商。Creater、Convertor、Defaulter、Typer提供对象创建、版本转换、默认值填充和类型识别能力。HubGroupVersion指定内存处理使用的版本;Deployment 使用内部版本。FieldManager维护字段管理信息,例如metadata.managedFields。
每次请求的取消信号、用户身份和命名空间保存在请求的 context.Context 中。RequestScope 则由路由安装时配置,供该路由的 Handler 使用。
CreateResource 通过适配器调用 createHandler。Handler 读取请求体和 CreateOptions 后,使用资源的 New 方法取得对象容器,再通过 DecoderToVersion 把解码目标设为 HubGroupVersion。runtime.Object 提供类型信息访问和深拷贝接口,允许通用 Handler 接收不同的 API 对象。
createHandler 的解码路径 根据 fieldValidation 选择解码器。Warn 和 Strict 都使用严格解码器,后续错误分支再决定把未知字段等问题作为警告还是错误处理。
func createHandler(r rest.NamedCreater, scope *RequestScope, admit admission.Interface, includeName bool) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
...
defaultGVK := scope.Kind
original := r.New()
// 按字段校验模式选择普通或严格解码器。
validationDirective := fieldValidation(options.FieldValidation)
decodeSerializer := s.Serializer
if validationDirective == metav1.FieldValidationWarn || validationDirective == metav1.FieldValidationStrict {
decodeSerializer = s.StrictSerializer
}
// 解码目标是后续内存处理使用的版本。
decoder := scope.Serializer.DecoderToVersion(decodeSerializer, scope.HubGroupVersion)
...
obj, gvk, err := decoder.Decode(body, &defaultGVK, original)
...
}
}
创建调用与校验回调
解码完成后,Handler 检查对象的命名空间,清理客户端提供的系统元数据,并把对象、用户、资源、操作类型和 dry-run 标志封装成准入属性。FieldManager.UpdateNoErrors 更新字段管理信息,随后调用 MutationInterface.Admit。
Validating Admission 通过回调传给资源的 Create 方法。AdmissionToValidateObjectFunc 返回一个 ValidateObjectFunc:执行这个回调时,它依据传入对象重新构造准入属性,补齐可能由 Registry 生成的名称,再调用 ValidationInterface.Validate。
requestFunc 保存创建操作;定义闭包时尚未执行 r.Create。调用顺序是更新字段管理信息、执行 Admit,然后执行闭包,把对象和新构造的校验回调传入 Registry。
admissionAttributes := admission.NewAttributesRecord(
obj, nil, scope.Kind, namespace, name, scope.Resource,
scope.Subresource, admission.Create, options,
dryrun.IsDryRun(options.DryRun), userInfo,
)
// 定义创建操作;校验回调由 Registry 在合适的阶段执行。
requestFunc := func() (runtime.Object, error) {
return r.Create(
ctx, name, obj,
rest.AdmissionToValidateObjectFunc(admit, admissionAttributes, scope),
options,
)
}
...
result, err := finisher.FinishRequest(ctx, func() (runtime.Object, error) {
liveObj, err := scope.Creater.New(scope.Kind)
if err != nil {
return nil, fmt.Errorf("failed to create new object (Create for %v): %v", scope.Kind, err)
}
obj = scope.FieldManager.UpdateNoErrors(liveObj, obj, managerOrUserAgent(options.FieldManager, req.UserAgent()))
admit = fieldmanager.NewManagedFieldsValidatingAdmissionController(admit)
// 变更准入通过后才调用资源创建接口。
if mutatingAdmission, ok := admit.(admission.MutationInterface); ok && mutatingAdmission.Handles(admission.Create) {
if err := mutatingAdmission.Admit(ctx, admissionAttributes, scope); err != nil {
return nil, err
}
}
...
result, err := requestFunc()
...
return result, err
})
创建成功后,transformResponseObject 按协商的媒体类型和响应版本编码结果,返回 201 Created。服务端 dry-run 也经过这条响应路径,是否持久化由传入存储层的 dry-run 标志决定。
准入插件管理与调用¶
API Server 在启动时按配置创建准入插件,并把实例保存到同一条有序插件链中。处理请求时,Admit 和 Validate 分别遍历这条链,选择支持当前操作且实现相应接口的插件。一个插件可以同时参与两个阶段。
插件注册与初始化
RegisterAllAdmissionPlugins 汇总内置插件的注册入口。各插件通过 Plugins.Register 把名称和工厂函数写入注册表,工厂函数负责根据插件配置创建实例。
AdmissionOptions.ApplyTo 取得启用的插件列表,读取配置,准备 Client、Informer 等共享依赖,再调用 NewFromPlugins。插件的排列遵循选项中的推荐顺序和启用、禁用配置。
NewFromPlugins 逐个读取配置并初始化插件;InitPlugin 调用工厂、注入依赖,再执行初始化检查。完成的链经过重入包装和指标包装后,保存到 Config.AdmissionControl。
func (ps *Plugins) NewFromPlugins(pluginNames []string, configProvider ConfigProvider, pluginInitializer PluginInitializer, decorator Decorator) (Interface, error) {
handlers := []Interface{}
...
for _, pluginName := range pluginNames {
pluginConfig, err := configProvider.ConfigFor(pluginName)
if err != nil {
return nil, err
}
// 创建实例、注入依赖并校验初始化结果。
plugin, err := ps.InitPlugin(pluginName, pluginConfig, pluginInitializer)
if err != nil {
return nil, err
}
if plugin != nil {
if decorator != nil {
handlers = append(handlers, decorator.Decorate(plugin, pluginName))
} else {
handlers = append(handlers, plugin)
}
...
}
}
...
// 两个准入阶段共用同一组有序插件实例。
return newReinvocationHandler(chainAdmissionHandler(handlers)), nil
}
请求阶段的插件分派
创建请求在 Handler 内调用 Admit,完成资源策略处理后,Registry 再通过 createValidation 回调调用 Validate。两次调用共享插件配置和实例,接收到的对象状态则取决于各自的执行阶段。
chainAdmissionHandler 先调用 Handles 检查操作类型,再检查插件是否实现当前阶段的接口。匹配的插件按配置顺序执行,任一插件返回错误都会立即终止本次调用。
func (admissionHandler chainAdmissionHandler) Admit(ctx context.Context, a Attributes, o ObjectInterfaces) error {
for _, handler := range admissionHandler {
if !handler.Handles(a.GetOperation()) {
continue
}
// 只分派给支持该操作的变更插件。
if mutator, ok := handler.(MutationInterface); ok {
err := mutator.Admit(ctx, a, o)
if err != nil {
return err
}
}
}
return nil
}
func (admissionHandler chainAdmissionHandler) Validate(ctx context.Context, a Attributes, o ObjectInterfaces) error {
for _, handler := range admissionHandler {
if !handler.Handles(a.GetOperation()) {
continue
}
// 校验阶段复用插件列表,并选择校验接口。
if validator, ok := handler.(ValidationInterface); ok {
err := validator.Validate(ctx, a, o)
if err != nil {
return err
}
}
}
return nil
}
reinvoker.Admit 在首次调用后检查 ShouldReinvoke()。需要重入时,它标记重入上下文并再次调用变更链,插件根据自身策略和上下文决定是否重新处理对象。reinvoker.Validate 则直接转发给校验链。
这里的有序执行指准入插件链;Mutating Webhook、Validating Webhook 各自的请求分派由对应插件实现。ValidationInterface.Validate 的接口约定要求校验过程保持对象内容不变。
REST Strategy 与通用 Registry¶
通用 Registry 负责对象创建的公共流程,资源 Strategy 提供对象类型自身的处理规则。Deployment 的 REST Storage 嵌入 genericregistry.Store,把 deployment.Strategy 配置为 CreateStrategy;创建时,Store 调用 Strategy 清理字段、校验内容,再执行准入校验和底层存储操作。
资源策略与存储配置
NewREST 给 Store 配置 NewFunc、NewListFunc 和各类 Strategy,再通过 CompleteWithOptions 补齐存储选项。NewFunc 返回内部类型 *apps.Deployment,让通用流程能够创建与当前资源对应的对象。
创建 Deployment 时,deploymentStrategy 提供以下处理:
PrepareForCreate清空status,把metadata.generation设为1,并按功能开关处理 Pod template 中的字段。Validate调用ValidateDeployment,检查 Deployment 和 Pod template 的资源约束。WarningsOnCreate收集对象名称、Pod template 等方面的警告。Canonicalize提供校验后的规范化入口;Deployment 在此版本中的实现为空。
例如,客户端即使提交 metadata.generation: 99,创建策略仍会将它设为 1。这一赋值发生在 Handler 的 Mutating Admission 之后、Validating Admission 之前。
资源对象校验
rest.BeforeCreate 先检查系统元数据、名称和命名空间,再依次执行资源准备、资源校验、通用元数据校验、警告生成和规范化。任一校验返回错误,创建流程立即结束。
func BeforeCreate(strategy RESTCreateStrategy, ctx context.Context, obj runtime.Object) error {
...
// 先应用资源创建规则,再校验处理后的对象。
strategy.PrepareForCreate(ctx, obj)
if errs := strategy.Validate(ctx, obj); len(errs) > 0 {
return errors.NewInvalid(kind.GroupKind(), objectMeta.GetName(), errs)
}
// 资源专用校验通过后检查通用元数据。
if errs := genericvalidation.ValidateObjectMetaAccessor(objectMeta, strategy.NamespaceScoped(), validatePathSegment, field.NewPath("metadata")); len(errs) > 0 {
return errors.NewInvalid(kind.GroupKind(), objectMeta.GetName(), errs)
}
for _, w := range strategy.WarningsOnCreate(ctx, obj) {
warning.AddWarning(ctx, "", w)
}
strategy.Canonicalize(obj)
return nil
}
准入校验与存储调用
Store.create 在进入 BeforeCreate 之前填充 UID、创建时间等系统元数据;设置 generateName 且未指定 name 时,还会生成对象名称。资源策略处理完成后,Store 把对象的深拷贝交给 createValidation,由 Handler 传入的回调执行 Validating Admission。
校验通过后,Store 根据对象名称和请求命名空间计算存储键,取得 TTL,分配结果对象 out,最后调用 Storage.Create。代码中的 dryrun.IsDryRun 把创建选项转换成存储接口的 dry-run 标志。
func (e *Store) create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
...
if err := rest.BeforeCreate(e.CreateStrategy, ctx, obj); err != nil {
return nil, err
}
// 资源准备和校验完成后,执行 Handler 传入的准入校验。
if createValidation != nil {
if err := createValidation(ctx, obj.DeepCopyObject()); err != nil {
return nil, err
}
}
name, err := e.ObjectNameFunc(obj)
if err != nil {
return nil, err
}
key, err := e.KeyFunc(ctx, name)
if err != nil {
return nil, err
}
...
ttl, err := e.calculateTTL(obj, 0, false)
if err != nil {
return nil, err
}
out := e.NewFunc()
if err := e.Storage.Create(ctx, key, obj, out, ttl, dryrun.IsDryRun(options.DryRun)); err != nil {
...
return nil, err
}
...
return out, nil
}
对普通 Deployment 创建请求,主要顺序是 Admit → PrepareForCreate → Validate → createValidation → Storage.Create。其中 Validate 执行资源固有约束,createValidation 连接配置好的 Validating Admission;两者在不同位置检查经过相应阶段处理后的对象。
对象持久化与响应¶
资源校验和 Validating Admission 通过后,Registry 把内部对象交给存储层。普通创建请求按存储版本编码对象,写入 etcd,再把带有 resourceVersion 的对象交回 HTTP Handler;服务端 dry-run 复用前面的处理阶段,在存储接口处跳过写入。
下图展示创建成功后的存储与响应路径。图中的 etcd3.store.Create 经由 storage.Interface 调用;启用 Watch Cache 时,CacheDelegator.Create 将写请求直接转发到底层存储。
存储编码与原子创建
存储层接收的是内部对象,etcd 保存的是字节序列。runtime.Encode(s.codec, obj) 使用配置好的存储 Codec 完成版本转换与序列化;TransformToStorage 再对这些字节执行存储转换。启用静态数据加密时,加密发生在这一转换阶段;使用 identity 转换器时,内容保持明文。
OptimisticPut 把“键的当前修改 revision 等于预期值”和写入放入同一事务。创建时传入预期值 0,只允许键尚不存在时写入;已有同名对象会使条件失败。Registry 随后把存储层的键已存在错误转换为 API 的 AlreadyExists,HTTP 状态为 409 Conflict。
etcd3.store.Create 依次编码对象、执行条件写入并构造返回对象:
func (s *store) Create(
ctx context.Context, key string, obj, out runtime.Object, ttl uint64,
) error {
...
// 清理持久化前需要重置的元数据。
if err := s.versioner.PrepareObjectForStorage(obj); err != nil {
return fmt.Errorf("PrepareObjectForStorage failed: %v", err)
}
// 使用存储 Codec 编码内部对象。
data, err := runtime.Encode(s.codec, obj)
...
newData, err := s.transformer.TransformToStorage(
ctx, data, authenticatedDataString(preparedKey),
)
...
// 预期 revision 为 0,只创建尚不存在的键。
txnResp, err := s.client.Kubernetes.OptimisticPut(
ctx, preparedKey, newData, 0,
kubernetes.PutOptions{LeaseID: lease},
)
...
if !txnResp.Succeeded {
return storage.NewKeyExistsError(preparedKey, 0)
}
if out != nil {
// 解码返回对象,并写入本次事务的版本。
err = s.decoder.Decode(data, out, txnResp.Revision)
...
}
return nil
}
这里传给 decoder.Decode 的是转换前的编码数据 data,无需在创建成功后重新 GET 对象。解码器 调用 Codec 还原对象,再通过 Versioner 将事务 revision 写入返回对象的 metadata.resourceVersion。
例如,两个客户端同时创建 default/vllm-demo 时,两个请求都可能完成前置校验;条件事务只允许其中一个创建该键。另一个请求收到 AlreadyExists,而不会覆盖前者。
服务端 dry-run
dryRun=All 请求仍执行解码、默认值、准入和资源校验。Store.create 把 dry-run 状态传给 DryRunnableStorage.Create,由这个包装层选择返回对象还是实际写入。
Storage.Get 用于发现已存在的对象;随后 copyInto 用存储 Codec 编码并解码对象,构造返回值。它不会调用底层 Create,也不会取得一次新写入的 etcd revision。这个存在性检查没有创建事务的并发保证,dry-run 成功不能预留对象名称。
func (s *DryRunnableStorage) Create(
ctx context.Context, key string, obj, out runtime.Object,
ttl uint64, dryRun bool,
) error {
if dryRun {
// 发现同名对象时拒绝创建。
if err := s.Storage.Get(ctx, key, storage.GetOptions{}, out); err == nil {
return storage.NewKeyExistsError(key, 0)
}
// 构造返回对象,跳过底层写入。
return s.copyInto(obj, out)
}
return s.Storage.Create(ctx, key, obj, out, ttl)
}
图中“执行准入与校验”表示整个 dry-run 请求保留这些阶段;具体执行点位于 Handler 和 Registry,存储包装层负责跳过写操作。Webhook 的副作用声明也必须允许 dry-run 请求。
响应编码
存储结果沿调用栈返回 createHandler。创建 Handler 在成功分支设置 201 Created,再交给 transformResponseObject 转换和编码 HTTP 响应。存储编码使用存储版本,响应编码则使用请求对应的 API 版本与协商出的媒体类型。
201 Created 的含义需要结合请求判断:普通创建成功表示对象已写入;服务端 dry-run 返回同样的状态码时,表示这条创建请求完成了允许 dry-run 的处理阶段,对象并未持久化。
etcd 的事务、revision 和存储转换细节见 etcd:Kubernetes 的状态存储。这里的写入成功也不要求所有 API Server 的 Watch Cache 已经同步到该版本;缓存通过各自的后台监听继续追赶。
更新、补丁与并发控制¶
PUT 提交更新后的对象,PATCH 提交基于当前对象计算变化的规则;两者最终都进入 Store.Update。Store.Update 把对象计算、版本检查和资源校验组合成回调,交给 GuaranteedUpdate 在存储层执行条件更新。
下图展示已有对象发生变化时的普通持久化路径。对象内容未变、服务端 dry-run,以及允许“更新时创建”的分支有各自的处理条件。
更新对象与补丁回调
PUT 的 UpdateResource 解码请求体,并通过 UpdatedObjectInfo 把待更新对象和后续转换传给资源接口。PATCH 的 patcher.patchResource 根据 Content-Type 选择 JSON Patch、Merge Patch、Strategic Merge Patch 或 Apply 的实现,再把 applyPatch、applyAdmission 等转换组合成 UpdatedObjectInfo。
实际补丁计算发生在 objInfo.UpdatedObject(ctx, existing) 中。每次调用都以这轮存储回调收到的 existing 为基础,因此存储竞争导致重试时,可以针对新的当前对象重新计算补丁。
Store.Update 随后检查版本,调用 rest.BeforeUpdate 执行资源 Strategy 的准备和校验,再调用 updateValidation 执行 Validating Admission。图中 SSA 节点是 UpdatedObject 内部的一条调用分支,普通 PUT 和其他补丁类型使用各自的对象转换方式。
客户端版本冲突
客户端提交 metadata.resourceVersion,表示这次更新建立在哪个已观察到的版本上。Store.Update 读取当前对象和新对象的版本,再按资源 Strategy 决定是否允许省略版本。
例如,客户端读取到 resourceVersion: "120",而对象在提交前已经更新为 "125",继续携带 "120" 的更新会得到 409 Conflict。客户端需要重新读取对象并重新计算希望提交的内容。
已有对象的版本检查位于传给 GuaranteedUpdate 的回调中:
// 在传给 GuaranteedUpdate 的回调内读取双方版本。
existingResourceVersion, err := e.Storage.Versioner().ObjectResourceVersion(existing)
...
obj, err := objInfo.UpdatedObject(ctx, existing)
...
newResourceVersion, err := e.Storage.Versioner().ObjectResourceVersion(obj)
...
doUnconditionalUpdate := newResourceVersion == 0 &&
e.UpdateStrategy.AllowUnconditionalUpdate()
...
if doUnconditionalUpdate {
// Strategy 允许省略版本时,使用当前存储版本。
err = e.Storage.Versioner().UpdateObject(obj, res.ResourceVersion)
...
} else {
if newResourceVersion == 0 {
...
return nil, nil, apierrors.NewInvalid(qualifiedKind, name, fieldErrList)
}
if newResourceVersion != existingResourceVersion {
return nil, nil, apierrors.NewConflict(
qualifiedResource, name, fmt.Errorf(OptimisticLockErrorMsg),
)
}
}
省略 resourceVersion 不会对所有资源产生同样结果。Deployment 的 AllowUnconditionalUpdate 返回 true,允许使用当前版本完成无条件更新;不允许这种行为的 Strategy 会把缺少版本视为无效请求。显式携带过期版本时,仍然执行冲突检查。
存储条件更新与重试
客户端版本检查通过后,对象仍可能在提交事务前被其他请求修改。etcd3.store.GuaranteedUpdate 因此使用 origState.rev 作为 OptimisticPut 的预期修改 revision,把最终的“版本仍然一致”检查与写入放到同一事务中。
如果条件比较失败,GetOnFailure: true 要求事务返回当前键值。getState 从 txnResp.KV 恢复对象与版本,循环继续执行更新回调。这个分支使用事务返回的数据,不需要再调用一次 GET 才能开始重算。完整循环见 GuaranteedUpdate。
for {
...
// 回调基于本轮对象计算新值,并执行资源层检查。
ret, ttl, err := s.updateState(origState, tryUpdate)
...
data, err := runtime.Encode(s.codec, ret)
...
newData, err := s.transformer.TransformToStorage(ctx, data, transformContext)
...
txnResp, err := s.client.Kubernetes.OptimisticPut(
ctx, preparedKey, newData, origState.rev,
kubernetes.PutOptions{
GetOnFailure: true,
LeaseID: lease,
},
)
...
if !txnResp.Succeeded {
// 使用条件事务返回的当前对象,重新执行回调。
origState, err = s.getState(
ctx, txnResp.KV, preparedKey, v,
ignoreNotFound, skipTransformDecode,
)
...
origStateIsCurrent = true
continue
}
err = s.decoder.Decode(data, destination, txnResp.Revision)
...
return nil
}
内部 CAS 重试只解决“计算新值到提交事务之间又发生了修改”的竞争。重新执行回调时,客户端携带的旧 resourceVersion 仍可能触发 409 Conflict,资源校验或准入也可能拒绝重新计算后的对象。
如果编码结果与已确认的当前存储内容相同,且存储转换没有要求重写,GuaranteedUpdate 的无变化分支 直接返回原对象版本。成功更新请求并不一定产生一次新的 etcd 写入。
SSA 字段所有权
Server-Side Apply 使用声明式补丁与 fieldManager 标识字段管理者。Apply 补丁实现 调用 FieldManager.Apply,基于当前对象和 managedFields 合并字段、计算所有权,再把得到的对象交回更新回调。
当请求尝试改变另一个管理者拥有的字段并产生所有权冲突时,FieldManager.Apply 把字段合并冲突转换为 API 冲突错误。例如,管理者 autoscaler 已拥有 spec.replicas,另一个管理者以不同值应用该字段时,可能收到字段所有权的 409 Conflict。
force=true 用于接管冲突字段,不会跳过身份认证、授权、资源校验和准入,也不能替代客户端的版本检查。字段所有权冲突、客户端版本冲突和存储层 CAS 竞争分别发生在合并、资源更新回调和事务提交阶段。
Apply 还会设置 forceAllowCreate,允许目标对象不存在时进入创建分支;这与图中已有对象的更新路径不同。普通 Deployment PUT 的 Strategy 则不允许通过更新创建对象。
读取、监听与 Watch Cache¶
GET、LIST 和 WATCH 都先进入资源 Handler 与 Registry,再由存储接口取得单个对象、对象集合或事件流。启用 Watch Cache 的资源使用 CacheDelegator 选择读取路径;Cacher 保存当前对象与有限事件历史,并向多个 API 客户端分发变化。
下图中的实线表示请求调用,绿色虚线表示后台同步的数据流。缓存与底层存储的选择由请求语义、缓存状态和可用历史共同决定。
读取入口与资源查询
GetResource 把查询参数解码成 GetOptions,再调用资源的 Get。通用 Store.Get 根据 namespace 和名称构造键,并把 resourceVersion 交给存储层。
LIST 与 WATCH 共用 ListResource。当 opts.Watch 或路由的 forceWatch 为真时,Handler 调用 handleWatch;其余请求调用 handleList。
Store.List 与 Store.Watch 把 Label Selector、Field Selector、namespace 范围等条件组织为 SelectionPredicate。ListPredicate 将查询交给存储的 GetList,WatchPredicate 则调用存储的 Watch。这层转换让缓存和 etcd 存储实现接收相同的资源查询条件。
GET 的缓存选择
CacheDelegator.Get 对未指定 resourceVersion 的 GET 直接调用底层存储,以取得最新对象。指定版本且缓存满足服务条件时,它调用 Cacher.Get;后者通过 WaitUntilFreshAndGet 等待缓存达到要求后读取对象。
resourceVersion=0 允许读取当前可用版本。指定非零版本 N 时,缓存需要至少推进到 N,返回对象自身的版本则取决于该对象最近一次修改;例如,缓存已观察到集合版本 200,其中一个未再修改的 Deployment 仍可能携带对象版本 "150"。
下面保留 GET 的主要分支。缓存未初始化时,启用 ResilientWatchCacheInitialization 的路径将请求转交底层存储;它不会把“设置了版本”无条件解释为“立即读取缓存”。
func (c *CacheDelegator) Get(
ctx context.Context, key string, opts storage.GetOptions,
objPtr runtime.Object,
) error {
...
if opts.ResourceVersion == "" {
return c.storage.Get(ctx, key, opts, objPtr)
}
if utilfeature.DefaultFeatureGate.Enabled(features.ResilientWatchCacheInitialization) {
if !c.cacher.Ready() {
return c.storage.Get(ctx, key, opts, objPtr)
}
}
// 省略版本解析及关闭该特性时的等待路径。
...
return c.cacher.Get(ctx, key, opts, objPtr)
}
LIST 的版本与快照
LIST 的选择逻辑集中在 ShouldDelegateList。NotOlderThan 可以由达到所需版本的缓存提供;Exact 和分页 continue 需要进一步判断对应历史快照是否可用。未指定版本的一致性 LIST,则检查缓存一致性读取及底层 Watch Progress 支持。
CacheDelegator.GetList 先执行这些语义判断,再决定调用 storage.GetList 还是 cacher.GetList。即使开始走缓存路径,历史快照过期或一致性读取等待超时,也可能触发向底层存储的委托。
func (c *CacheDelegator) GetList(
ctx context.Context, key string, opts storage.ListOptions,
listObj runtime.Object,
) error {
...
result, err := delegator.ShouldDelegateList(opts, c.cacher)
if err != nil {
return err
}
if result.ShouldDelegate {
return c.storage.GetList(ctx, key, opts, listObj)
}
// 省略版本解析及缓存未就绪时的处理。
...
err = c.cacher.GetList(ctx, key, opts, listObj)
...
if err != nil {
if errors.IsResourceExpired(err) &&
utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) {
return c.storage.GetList(ctx, key, opts, listObj)
}
...
}
...
return nil
}
图中的“缓存满足请求语义”包含这些判断及等待条件,不表示所有 LIST 都由缓存立即返回。具体参数组合、历史快照和缓存未就绪时的行为见 etcd:Kubernetes 的状态存储。
WATCH 事件与后台同步
前台的 CacheDelegator.Watch 将请求交给 Cacher.Watch。Cacher.Watch 根据起始版本和过滤条件建立 watcher,衔接可用的历史事件与之后的新事件;Handler 通过 serveWatchHandler 把事件编码到客户端连接。
缓存自身的数据来源是另一条后台路径。Cacher 初始化 创建 NewListerWatcher 和 Reflector,startCaching 再运行 Reflector.ListAndWatch。listerWatcher.List 与 Watch 访问底层存储,Reflector 将取得的对象和事件应用到 Watch Cache。
图中的 etcd3.store.GetList / Watch → Reflector → Watch Cache 表示对象和事件的数据方向;实际调用由 Reflector 发起。创建和更新请求直接写底层存储,随后由这条监听路径异步更新缓存。
事件历史有保留窗口。客户端要求的起始版本早于缓存能够衔接的范围时,getAllEventsSinceLocked 返回 ResourceExpired,对应状态码 410 Gone。客户端需要重新取得集合状态和版本,再建立新的监听。
在 Cacher 的这条路径中,历史过期会由 newErrWatcher 转为 ERROR 事件;事件中的 Status 携带过期原因和 410,不能仅根据 HTTP 连接最初的 200 OK 判断监听始终成功。缓存尚未就绪、请求版本高于缓存进度和历史版本过期也属于不同状态,分别走就绪检查、等待或过期处理。
这些路径适用于启用 Watch Cache 的资源。关闭缓存的资源直接使用底层存储;聚合 API 的读写与缓存行为由扩展 API Server 自行实现。
API Server 启动参数¶
以下命令读取 API Server 容器的启动参数,并筛出认证、授权、Admission 和审计配置:
api_pod="$(kubectl get pod -n kube-system \
-l component=kube-apiserver \
-o jsonpath='{.items[0].metadata.name}')"
kubectl get pod "$api_pod" -n kube-system \
-o jsonpath='{range .spec.containers[0].command[*]}{.}{"\n"}{end}' \
| grep -E -- \
'--authorization-|--authentication-|--enable-admission-plugins|--disable-admission-plugins|--audit-'
启动参数显示 API Server 的配置。实验:搭建 nvkind 集群并追踪 vLLM 部署通过 API Audit 记录单次请求的调用者、授权结果和处理阶段。
相关资料¶
- Kubernetes 组件
- Controlling Access to the Kubernetes API
- Authentication
- Authorization
- 使用 RBAC 鉴权
- Admission Controllers
- Dynamic Admission Control
- Validating Admission Policy
- Mutating Admission Policy
- CustomResourceDefinition Versioning
- CRD Webhook Conversion
- Kubebuilder Conversion Concepts
- Kubebuilder Conversion Implementation
- Kubernetes API Concepts
- Kubernetes API Reference v1.36
- vLLM OpenAI-compatible Server
- vLLM v0.27.1 OpenAI 镜像入口









