上一篇(核心概念导读)描述了 Kubernetes 的对象体系和控制器循环。这一篇进入 API Server 内部。API Server 不是普通的 HTTP 代理;它是整个集群的唯一写入路径,也是所有控制器、kubelet、用户工具共享的信息总线。

声明式 API 经常被描述为"说你想要什么,而不是怎么做"。这个描述准确,但不完整。Kubernetes 的声明式 API 背后有一套精确的语义:冲突检测、字段所有权、幂等写入。理解这些语义,才能解释为什么 kubectl apply 不是简单的 HTTP PUT。

本文只问一个问题:一个 apply 请求经过了哪些关卡?

请求管道全貌

一个 kubectl apply 请求从客户端到 etcd 的完整路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
kubectl apply -f deployment.yaml

│ HTTPS (mTLS)

┌──────────────────────────────────────────────────────────┐
│ kube-apiserver │
│ │
1. Authentication ──► 身份:User / ServiceAccount JWT │
│ │ │
2. Authorization ──► RBAC 决策(SubjectAccessReview) │
│ │ │
3. Mutating Admission Webhooks │
│ │ 注入 sidecar,补全 default 字段 │
│ │ │
4. Object Schema Validation │
│ │ OpenAPI structural schema + CEL │
│ │ │
5. Validating Admission Webhooks │
│ │ 只读策略检查,可拒绝,并行执行 │
│ │ │
6. Write to etcd ──► resourceVersion 乐观锁事务 │
│ │
└──────────────────────────────────────────────────────────┘


etcd
key: /registry/apps/deployments/default/nginx
val: protobuf(Deployment)

每个关卡都是独立插件。任何一个关卡拒绝请求,API Server 立即返回 4xx,后续关卡不再执行。

GVK 三元组与资源发现

Kubernetes API 用 Group/Version/Kind(GVK)三元组唯一标识一种资源类型。例如:

  • apps/v1/Deployment:Group=apps,Version=v1,Kind=Deployment
  • core/v1/Pod:Group 为空(core group),Version=v1,Kind=Pod
  • networking.k8s.io/v1/Ingress:扩展 Group

GVK 到 REST 路径的映射规则:

1
2
3
4
apps/v1/Deployment(命名空间级)→ /apis/apps/v1/namespaces/{ns}/deployments/{name}
apps/v1/Deployment(集群级) → /apis/apps/v1/deployments
v1/Pod → /api/v1/namespaces/{ns}/pods/{name}
v1/Node(集群级) → /api/v1/nodes/{name}

core group(空 Group)用 /api 而非 /apis,这是历史遗留设计。

API 发现端点 /apis/api 返回所有已注册资源的元数据。kubectl api-resources 就是解析这两个端点。每个条目包含:资源名(deployments)、Kind(Deployment)、是否命名空间级、支持的动词(getlistwatchcreateupdatepatchdelete)。

多版本共存是 API Server 的核心设计。同一资源可以同时有 v1beta1v1 两个版本。API Server 内部维护一个 hub version(通常是最新稳定版),所有版本在写入前转换成 hub version 存储,读取时按请求版本转换输出。这个 conversion 可以由 API Server 内置代码完成,也可以委托给 conversion webhook(CRD 场景常用)。

Authentication:确认身份

所有请求首先要证明"你是谁"。API Server 支持多种认证机制,按配置顺序依次尝试,任一成功即通过:

  • X.509 客户端证书:kubectl 默认使用 ~/.kube/config 中的客户端证书,Subject 的 CN 字段作为用户名,O 字段作为 Group
  • Bearer token:静态 token 文件(不推荐生产使用)
  • ServiceAccount token(JWT):Pod 内部自动挂载,由 API Server 或外部 OIDC 签发,支持 --bound-service-account-tokens 绑定特定 Pod 生命周期
  • OIDC:对接外部身份提供商(Dex、Keycloak、Azure AD),token 携带标准 OIDC claims

认证成功后,请求携带一个 UserInfo:用户名、UID、所属 Groups。这个身份信息向后传递给 Authorization 阶段。认证失败返回 401 Unauthorized。

匿名请求(未携带凭据)在 API Server 开启 --anonymous-auth 时以 system:anonymous 身份处理,通常只允许访问健康检查端点。

Authorization:RBAC 决策

Authorization 阶段决策"你能做什么"。Kubernetes 默认使用 RBAC(Role-Based Access Control)。

RBAC 四个核心对象:

  • Role:定义单个命名空间内的权限规则
  • ClusterRole:定义集群级别的权限规则(跨命名空间,或访问集群级资源如 Node)
  • RoleBinding:把 Role 或 ClusterRole 绑定到 Subject(User/Group/ServiceAccount),作用域是单个命名空间
  • ClusterRoleBinding:把 ClusterRole 绑定到 Subject,作用域是整个集群

每个请求生成一个 SubjectAccessReview,Authorizer 遍历该用户相关的所有 Binding,检查是否有规则匹配当前请求的 Group+Resource+Verb+Namespace 组合。任一规则匹配即允许(allow-wins),没有匹配则拒绝(implicit deny)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 允许 default 命名空间的 ServiceAccount 读取 Pod
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
namespace: default
name: pod-reader
rules:
- apiGroups: [""] # core group
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: read-pods
namespace: default
subjects:
- kind: ServiceAccount
name: default
namespace: default
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io

除 RBAC 外,API Server 还支持 Node Authorizer(专门处理 kubelet 的权限,只允许 kubelet 访问其所在节点上的对象)和 Webhook Authorizer(把决策委托给外部服务,OPA 等策略引擎可以通过这个接口接入)。

Admission:修改与校验的双层闸门

Admission 分为两个阶段,顺序固定,不可互换。

Mutating Admission Webhooks

Mutating webhook 收到当前对象,可以返回一个 JSON Patch(RFC 6902),API Server 将 patch 应用到对象上,然后将修改后的对象传给下一个 webhook。常见用途:

  • Sidecar 注入:Istio 的 istio-sidecar-injector 在每个符合条件的 Pod 创建时注入 istio-proxy 容器和 init container
  • 默认值补全:为缺少 imagePullPolicy 的容器设置默认值
  • 标签/注解标准化:自动添加环境标识、团队归属等标准 label

多个 Mutating webhook 串行执行,每个 webhook 收到的是前一个修改后的对象。reinvocationPolicy: IfNeeded 控制是否在其他 webhook 修改对象后重新调用本 webhook(解决相互依赖的注入场景)。

Object Schema Validation

Mutating 阶段结束后,API Server 对最终对象进行 OpenAPI Schema 校验。内置资源使用硬编码的 Go struct 校验;CRD 使用 spec.validation.openAPIV3Schema,支持 x-kubernetes-validations(CEL 表达式)做跨字段约束:

1
2
3
x-kubernetes-validations:
- rule: "self.minReplicas <= self.maxReplicas"
message: "minReplicas must be <= maxReplicas"

Validating Admission Webhooks

Validating webhook 是只读的:收到最终对象,只能返回 allow 或 deny(附带拒绝原因字符串)。多个 Validating webhook 并行执行,任一返回 deny 整个请求失败。常见用途:

  • 强制执行团队策略:所有 Deployment 必须有 resource limits、镜像必须来自私有仓库
  • OPA/Gatekeeper 策略引擎:Constraint 对象定义 Rego 策略,GatekeeperWebhook 注册为 Validating webhook 执行检查
  • Kyverno:类似 Gatekeeper,但用 YAML 语法写策略

写入 etcd:乐观锁保证一致性

通过所有 Admission 关卡后,API Server 将对象序列化为 protobuf(内部存储格式,比 JSON 更紧凑),写入 etcd。写入时携带 resourceVersion——这是 etcd 的 revision 值——用于乐观锁。

乐观锁工作方式:

1
2
3
4
5
6
7
8
9
10
① 客户端 GET 对象 → 拿到 resourceVersion: "1234"
② 客户端修改字段
③ 客户端 PUT/PATCH,携带 resourceVersion: "1234"
④ API Server 调用 etcd 事务:
if current_revision == 1234:
write new_value
else:
return error
⑤ 若中间有其他写入 → revision 已变 → etcd 返回事务失败
⑥ API Server 返回 409 Conflict → 客户端重新 GET 再重试

resourceVersion 是不透明字符串,客户端不应解析其数值,只应原样回传。不同资源的 resourceVersion 不可相互比较——它们对应的是 etcd 全局 revision,但含义是"该对象最后一次写入时的 etcd revision"。

Server-Side Apply:字段所有权

经典的 kubectl apply(Client-Side Apply)在客户端计算 diff,用 kubectl.kubernetes.io/last-applied-configuration annotation 记录上次状态,然后发送 strategic merge patch。这种方式的局限:多个工具同时管理同一对象时,last-applied 相互覆盖,造成字段丢失。

Server-Side Apply(SSA,kubectl apply --server-side)把 diff 计算移到服务端,引入字段所有权:

  • 每个字段由一个 fieldManager(字符串标识符)拥有
  • 对象的 managedFields 记录所有字段归属
  • 若 Manager A 尝试修改 Manager B 拥有的字段,API Server 返回 409 Conflict(--force-conflicts 可强制接管)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# kubectl get deployment nginx -o yaml 中的 managedFields
managedFields:
- manager: kubectl
operation: Apply
apiVersion: apps/v1
fieldsV1:
f:spec:
f:replicas: {}
f:template:
f:spec:
f:containers:
k:{"name":"nginx"}:
f:image: {}
- manager: kube-controller-manager
operation: Update
fieldsV1:
f:status:
f:availableReplicas: {}
f:readyReplicas: {}

SSA 适合 GitOps 场景:CI/CD 工具作为一个 fieldManager 只管理声明的字段,Operator 管理另一组字段,互不干扰。

Watch 机制:变更通知总线

Watch 是 API Server 向客户端推送资源变更事件的机制。客户端发起长连接:

1
GET /apis/apps/v1/namespaces/default/deployments?watch=true&resourceVersion=5000

API Server 保持此连接,etcd 有新事件时以 chunked HTTP 或 HTTP/2 stream 推送,每个事件是 JSON 对象:

1
{"type": "MODIFIED", "object": { ...Deployment... }}

事件类型:ADDEDMODIFIEDDELETEDBOOKMARK(仅携带当前 resourceVersion,用于定期刷新客户端已知版本)、ERROR

API Server 内部有一个 watch cache(内存环形缓冲),etcd watch 事件先进入 watch cache,再广播给所有客户端连接,避免大量客户端直接 watch etcd 造成压力。

断线重连:客户端在重连时携带上次收到的 resourceVersion,从该版本续流。若该 revision 已被 etcd compaction 清除,API Server 返回 410 Gone,客户端需要重新 List 打快照再 Watch——这就是 Informer 的 List-Watch 循环。

可观察实验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 实验 1:观察 SSA managedFields
cat > /tmp/nginx-deploy.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.25
EOF

kubectl apply --server-side --field-manager=my-tool -f /tmp/nginx-deploy.yaml
kubectl get deployment nginx -o yaml | grep -A 40 managedFields

# 实验 2:Watch 事件流(-v=8 打印 HTTP 层细节)
kubectl get pods --watch -v=8 2>&1 | grep -E "GET|watch|resourceVersion" &
WATCHER_PID=$!
kubectl run test-watch --image=nginx
sleep 5
kill $WATCHER_PID
kubectl delete pod test-watch

# 实验 3:探索 API discovery
kubectl get --raw /apis/apps/v1 | jq '.resources[] | {name, kind, verbs}'

# 实验 4:检查 RBAC 权限
kubectl auth can-i list pods --as=system:serviceaccount:default:default
kubectl auth can-i create deployments --as=system:serviceaccount:default:default

# 实验 5:触发 resourceVersion 冲突(409)
kubectl get deployment nginx -o jsonpath='{.metadata.resourceVersion}'
# 用伪造的旧 resourceVersion 触发冲突(期望返回 409)
kubectl patch deployment nginx \
--type=merge \
-p '{"metadata":{"resourceVersion":"1"}, "spec":{"replicas":2}}' 2>&1 || true

实验映射到内部机制

kubectl apply --server-side -f nginx-deploy.yaml 的内部路径:

  1. kubectl 读取 YAML,只保留用户声明的字段(去掉 status、managedFields 等服务端字段)
  2. 发送 PATCH /apis/apps/v1/namespaces/default/deployments/nginx,Content-Type: application/apply-patch+yaml
  3. API Server SSA handler 从 etcd 读取当前对象(若不存在则创建)
  4. 计算 merge:新声明字段归属 my-tool manager;未声明字段保持原有归属
  5. 检测 ownership conflict(其他 manager 拥有同一字段时返回 409,除非 --force-conflicts
  6. 经过 Mutating Webhook → Validation → Validating Webhook 管道
  7. 写入 etcd,返回含 managedFields 的完整对象

Watch 的内部路径:

  1. 客户端发起 Watch 请求,携带 resourceVersion
  2. API Server 的 cacher 检查 watch cache:若 cache 中有该 revision 后的事件,直接从 cache 返回;否则从 etcd 续流
  3. etcd 收到写入后通知 API Server cacher,cacher 更新内存 cache 并广播给所有 watcher
  4. 客户端 Informer 收到事件,写入 DeltaFIFO 队列(见第 03 篇)

练习

  1. kubectl apply --server-side --field-manager=manager-a--field-manager=manager-b 分别管理同一个 Deployment 的不同字段(replicasimage),然后观察 managedFields。再尝试用 manager-a 修改 manager-b 拥有的字段,观察 409 冲突错误。最后用 --force-conflicts 强制接管,观察 managedFields 的变化。

  2. 编写一个最简 Mutating Admission Webhook(Go 或 Python):对所有 Pod 创建请求,自动添加 annotation injected-by: my-webhook。部署到 kind 集群(需要 TLS 证书,可用 cert-manager 或自签发),验证注入效果。观察 webhook 处理延迟对 Pod 创建速度的影响。

  3. -v=9 运行 kubectl get pods,在输出中找到完整的 HTTP 请求/响应,记录 resourceVersionAccept 头(kubectl 优先请求 protobuf 格式:application/vnd.kubernetes.protobuf)、Authorization 头中的 Bearer token。用 jwt.iobase64 -d 解码 ServiceAccount JWT,查看 isssubaudexp 字段,理解 token 绑定机制。

模式提炼

API Server 的设计体现了两个经典的软件工程模式,组合使用:

声明式入口。调用者提交期望终态,不指定操作步骤,系统保证最终收敛。这个模式把"状态描述"和"状态执行"分离到不同的进程(API Server 负责接受,控制器负责执行),使系统极具弹性:控制器可以重启、升级、崩溃恢复,而不丢失任何待处理的意图,因为意图持久化在 etcd 里。

管道过滤器。请求按固定顺序经过多个处理阶段(认证 → 授权 → Mutating → 校验 → Validating → 持久化),每阶段职责单一,失败即短路。各阶段之间通过标准接口(AdmissionReview webhook)解耦,允许外部逻辑以插件形式接入,不需要修改 API Server 核心代码。这与企业应用中 Servlet Filter / gRPC Interceptor 的设计原理相同,区别在于 Kubernetes 的 Admission 阶段还允许修改被处理的对象(Mutating 阶段),而不只是放行或拒绝。

Server-Side Apply 在此之上引入了字段所有权语义。这解决了"谁有权修改哪个字段"的协调问题——不靠团队约定,而靠系统记录和强制。多个 controller 和人工操作可以同时管理同一对象的不同字段,所有权冲突由 API Server 检测并暴露,而非静默覆盖。

工程迁移表

K8s 机制 工程类比 关键相似点
Authentication Spring Security AuthenticationManager 请求在进入业务逻辑前验证身份,支持多种认证方式串联
Authorization (RBAC) Spring Security AccessDecisionManager 基于角色的访问控制,allow-wins,无显式 deny
Mutating Webhook Servlet Filter / AOP @Around Advice 在请求处理前修改输入,结果对调用方透明
Validating Webhook Bean Validation @Valid / JSR-380 只读校验,失败抛异常,多个 validator 可并行
resourceVersion 乐观锁 JPA @Version / 数据库 UPDATE WHERE version=? 写入时携带版本号,版本不匹配拒绝,调用方自行重试
Watch long-poll SSE / WebSocket 服务端推送 服务端保持连接推送增量事件,无需客户端轮询
Server-Side Apply 字段所有权 Git merge conflict markers 谁拥有哪些行有记录,冲突时需要显式解决而非静默覆盖
List + Watch 分离 MySQL binlog position + snapshot 先获取基线快照,再从固定位置追增量,不遗漏

常见误解

误解一:kubectl 直接读写 etcd。

事实是所有 kubectl 操作都经过 API Server 的完整 6 阶段管道,etcd 端口在生产集群中仅对 API Server 开放。这个隔离是 Kubernetes 安全模型的基础:etcd 只信任 API Server,API Server 负责所有的认证、授权和准入控制。从 etcd 直接读写会绕过整个安全层,在任何正式环境都是危险操作。

误解二:Admission Webhook 只是验证,不能修改对象。

Admission 分两轮:Mutating 阶段的 webhook 可以通过返回 JSON Patch 修改对象,这是 sidecar 注入、默认值填充的实现基础。Validating 阶段的 webhook 才是只读的,只能放行或拒绝。两者注册在不同的资源类型上(MutatingWebhookConfiguration vs ValidatingWebhookConfiguration),不会混用。将两者混淆会导致配错配置类型,注入逻辑失效且没有错误提示。

误解三:resourceVersion 是时间戳,可以用来推算写入时间。

resourceVersion 是 etcd 全局写入 revision 的字符串化,语义是单调递增的版本序号,不是时间。同一个对象的两个 resourceVersion 之间的差值代表全集群在这段时间内的总写入次数,与时间无关。不同对象的 resourceVersion 不可比较大小——一个 Pod 的 rv=200 和一个 ConfigMap 的 rv=150 不意味着 Pod 比 ConfigMap 更新。真实修改时间信息在 metadata.managedFields[].time 和 conditions 的 lastTransitionTime 字段里。

误解四:kubectl apply 和 kubectl replace 等价。

kubectl replace 是完整替换(HTTP PUT),覆盖当前对象的全部字段,包括其他 Operator 写入的 annotation 和 controller 维护的 status。kubectl apply 只修改声明的字段,未声明的字段保留。在多 manager 场景(CI/CD + Operator 协同管理同一对象)下,replace 会覆盖 Operator 管理的字段,触发控制器持续修复,造成状态震荡。

系列导航

参考资料