Kubernetes 基础体系 · 第 82/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Kubernetes API Aggregation:Extension API Server、发现、认证和可用性

Kubernetes 不只通过内置 kube-apiserver 提供 API。监控指标、服务目录、云资源、策略对象以及 Operator 自定义的复杂资源,都可能需要一个独立进程实现 API。Kubernetes API Aggregation 机制允许这个独立进程以 Extension API Server 的形式接入集群,使客户端可以像访问内置 API 一样访问它:

kubectl
  │
  │ HTTPS 请求
  ▼
kube-apiserver
  │
  │ 根据 API group/version 路由
  ▼
kube-aggregator
  │
  │ Service + TLS + 认证信息转发
  ▼
Extension API Server
  │
  ├── 自己的存储
  ├── Kubernetes API
  └── 外部系统或云厂商 API

这里有三个容易混淆的事实:

  1. Extension API Server 不是 kube-apiserver 的插件线程,而是独立的 HTTP API 服务器。
  2. 它提供的资源通常不存储在主集群的 etcd 中,存储责任由扩展服务器自己决定。
  3. kube-apiserver 负责入口认证和请求转发,但聚合 API 的资源授权通常由 Extension API Server 自己完成。

这套机制适合需要独立 API 语义、独立存储或独立生命周期的扩展。若只是增加一种声明式资源,CRD 通常更简单;若只是修改对象入站请求,则应考虑 Admission Webhook,而不是 API Aggregation。


一、先建立 API 基础:GVK、资源 URL 和 APIService

1. GVK 如何决定请求路径

Kubernetes 对象通常由以下三个字段组合识别:

  • Group:API 组,例如 appsbatchmetrics.k8s.io
  • Version:API 版本,例如 v1v1beta1
  • Kind:对象类型,例如 DeploymentPodMetrics
  • 三者合称 GVK,即 Group-Version-Kind

例如:

apiVersion: metrics.k8s.io/v1beta1
kind: PodMetrics
metadata:
  name: web-7d8f6f7d6b-x7k9p
  namespace: default

其中:

Group   = metrics.k8s.io
Version = v1beta1
Kind    = PodMetrics

客户端访问资源时,Kind 不直接出现在 URL 中,而是使用资源的复数名:

/apis/metrics.k8s.io/v1beta1/namespaces/default/pods
/apis/metrics.k8s.io/v1beta1/namespaces/default/pods/web-7d8f6f7d6b-x7k9p

URL 的基本形式是:

/api/<version>/...

用于核心组,例如:

/api/v1/namespaces/default/pods

非核心 API 组使用:

/apis/<group>/<version>/...

例如:

/apis/apps/v1/namespaces/default/deployments

apiVersion 是对象的 GVK 中 Group 和 Version 的 YAML 表示;它本身不包含资源名。API Server 还必须把 Kind 映射到资源名,例如:

Deployment -> deployments
Pod        -> pods
PodMetrics -> pods

因此,一个 Extension API Server 不只是返回任意 JSON。若希望 kubectl、动态客户端和 Kubernetes 控制器正常使用,它需要遵循 Kubernetes API 的资源、元数据、状态码、发现和并发语义。

2. Extension API Server 是按 Group/Version 接入的

API Aggregation 通过 APIService 对象注册一个 API 组版本。一个 APIService 通常对应一个 group/version,而不是一个 Kind:

v1beta1.metrics.k8s.io

这意味着下面两个资源可以由同一个 Extension API Server 处理:

metrics.k8s.io/v1beta1/nodes
metrics.k8s.io/v1beta1/pods

注册对象类似:

apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
  name: v1beta1.metrics.k8s.io
spec:
  group: metrics.k8s.io
  version: v1beta1
  service:
    namespace: monitoring
    name: metrics-server
    port: 443
  caBundle: <Extension API Server 服务端证书 CA 的 base64>
  groupPriorityMinimum: 100
  versionPriority: 100

metadata.name 的约定格式是:

<version>.<group>

例如:

v1.metrics.example.com
v1beta1.metrics.k8s.io

APIService 的字段含义如下:

  • groupversion:声明这个聚合服务器负责哪个 API group/version。
  • service:指定主集群内的 Service,kube-apiserver 通过它访问扩展服务器。
  • caBundle:用于验证 Extension API Server 的服务端 TLS 证书。
  • groupPriorityMinimum:在 API 组发现信息中的优先级。
  • versionPriority:同一 API 组中不同版本的优先级。
  • url:也可以把请求转发到集群外部 URL,但生产环境通常需要额外处理网络可达性、证书和安全边界。
  • insecureSkipTLSVerify:跳过后端 TLS 证书验证,仅适合临时实验,不应作为生产方案。

groupPriorityMinimumversionPriority 主要影响发现信息中“首选组和版本”的选择,不是把请求转发到某个资源的路由权重。路由首先由 group/version 决定。


二、API Aggregation 的组件关系和完整请求路径

1. kube-aggregator 在哪里

在标准 Kubernetes 控制面中,API Aggregation 由 kube-apiserver 内部的聚合层提供。历史上也常用名称 kube-aggregator 描述这部分能力,但通常不需要单独部署一个名为 kube-aggregator 的进程。

主 API Server 接收所有客户端请求:

客户端 -> kube-apiserver

对于内置资源,例如:

/api/v1/pods
/apis/apps/v1/deployments

请求由主 API Server 的本地 handler 处理。

对于已注册的聚合 API,例如:

/apis/metrics.k8s.io/v1beta1/pods

主 API Server 根据对应的 APIService 将请求代理到后端 Service。

完整流程可以表示为:

sequenceDiagram
    participant C as 客户端
    participant K as kube-apiserver<br/>认证与聚合层
    participant S as Kubernetes Service
    participant E as Extension API Server
    participant D as 扩展存储或外部系统

    C->>K: GET /apis/example.com/v1/widgets
    K->>K: 校验客户端 TLS、Token 或其他认证
    K->>K: 查询 APIService v1.example.com
    K->>K: 校验后端服务证书
    K->>S: 转发 HTTPS 请求
    S->>E: 负载均衡到一个扩展服务器 Pod
    E->>E: 读取代理认证头并执行授权
    E->>D: 查询或修改资源
    D-->>E: 返回资源或错误
    E-->>S: Kubernetes API 响应
    S-->>K: 转发响应
    K-->>C: 返回状态码、Headers 和 Body

关键点是:客户端通常不直接连接 Extension API Server,而是连接主 API Server。这样客户端只需要信任集群 API Server 的证书,不需要知道扩展服务的网络地址。

2. 注册之后,谁负责哪些工作

一次请求可以拆成几个责任边界:

阶段 主要责任方
客户端到主 API Server 的 TLS kube-apiserver
客户端身份认证 通常是 kube-apiserver
group/version 路由 主 API Server 的聚合层
主 API Server 到扩展服务器的 TLS 聚合层和 Extension API Server
资源路由、参数解析、对象校验 Extension API Server
对扩展资源的授权 通常是 Extension API Server
资源持久化和并发控制 Extension API Server
资源的业务逻辑 Extension API Server

主 API Server 代理请求,并不意味着它理解 Extension API Server 中的每一种 Kind,也不意味着它替扩展资源执行完整的 RBAC 授权。


三、API 发现:客户端如何知道聚合资源存在

1. 发现接口的层级

Kubernetes 客户端通常先访问发现接口,再决定某个资源是否存在、支持哪些动词和版本。

核心组发现接口:

GET /api
GET /api/v1

非核心组发现接口:

GET /apis
GET /apis/<group>
GET /apis/<group>/<version>

注册:

group    = example.com
version  = v1

之后,主 API Server 会在发现结果中暴露:

GET /apis/example.com
GET /apis/example.com/v1

Extension API Server 则应返回该版本支持的资源列表。一个简化的发现响应类似:

{
  "kind": "APIResourceList",
  "apiVersion": "v1",
  "groupVersion": "example.com/v1",
  "resources": [
    {
      "name": "widgets",
      "singularName": "widget",
      "namespaced": true,
      "kind": "Widget",
      "verbs": ["create", "delete", "get", "list", "patch", "update", "watch"],
      "shortNames": ["wdg"]
    }
  ]
}

这份信息至少需要与实际路由保持一致:

发现声明支持 widgets
实际却没有 /apis/example.com/v1/namespaces/<ns>/widgets

这种不一致会导致动态客户端、kubectl api-resources 或控制器在运行时失败。

2. APIService 的优先级不是资源版本转换

例如,同一组下有:

example.com/v1
example.com/v1beta1

可以分别注册两个 APIService,并把 v1versionPriority 设置得更高。这会使客户端更倾向于把 v1 视为首选版本。

APIService 本身不提供类似 CRD conversion webhook 的对象版本转换协议。若一个客户端请求 v1,另一个客户端请求 v1beta1,扩展服务器必须:

  1. 分别提供两个 APIService,或
  2. 只提供一个版本,并明确不支持另一个版本,或
  3. 自己实现跨版本转换。

不能因为两个版本指向相同的后端进程,就自动得到 Kubernetes 原生 API 的版本转换能力。

3. 聚合发现失败时会发生什么

假设 APIService 已创建,但扩展服务器不可达:

kube-apiserver -> Service -> 无可用 Endpoint

则对该 group/version 的发现或实际请求可能返回失败,常见表现包括:

503 Service Unavailable

kubectl api-resources 可能报告某个 APIService 发现失败;而访问内置资源仍然可以成功。具体显示格式会随 Kubernetes 客户端版本变化,因此诊断时应直接查看 APIService 状态和主 API Server 日志,而不能只根据 kubectl 的一行汇总信息判断。

发现失败的影响可能比一次业务请求失败更大:

  • 动态客户端无法建立资源映射。
  • 控制器启动时无法确认资源是否存在。
  • kubectl 的资源补全和短名称解析异常。
  • 某些依赖全量发现的客户端会把整个操作视为失败。

因此,“扩展资源不用时先让它发现失败”也不是无害状态。


四、创建一个 APIService:TLS、Service 和路由配置

下面给出一个最小的注册结构。它不能直接运行,因为 caBundle 必须与实际服务证书匹配;这是有意保留的安全前置条件,而不是填入一个伪造证书。

apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
  name: v1.example.com
spec:
  group: example.com
  version: v1
  service:
    namespace: example-system
    name: example-apiserver
    port: 443
  caBundle: LS0tLS1CRUdJTi...
  groupPriorityMinimum: 1000
  versionPriority: 100

扩展服务器应通过 Service 暴露 HTTPS 端口:

apiVersion: v1
kind: Service
metadata:
  name: example-apiserver
  namespace: example-system
spec:
  selector:
    app: example-apiserver
  ports:
    - name: https
      port: 443
      targetPort: https

证书的 SAN 至少应覆盖主 API Server 连接 Service 时使用的 DNS 名称。通常需要覆盖:

example-apiserver.example-system.svc
example-apiserver.example-system.svc.cluster.local

实际证书名称取决于集群 DNS、Service 配置以及聚合层的访问方式。不能只为 Pod IP 签发证书,因为 Service 后端 Pod 会变化。

在实验环境中,有人会这样配置:

spec:
  insecureSkipTLSVerify: true

这会使主 API Server 不验证扩展服务器的服务端证书。风险在于:如果网络路径或 Service 解析被劫持,主 API Server 可能把请求发送给伪造的 HTTPS 服务。它也不能替代客户端到主 API Server 的 TLS。生产环境应使用受信任 CA 的 caBundle,而不是关闭验证。

应用配置后,可以观察注册状态:

kubectl apply -f apiservice.yaml
kubectl get apiservice v1.example.com
kubectl describe apiservice v1.example.com

正常情况下可以看到:

NAME            SERVICE                         AVAILABLE                  AGE
v1.example.com  example-system/example-apiserver  True                     10s

也应验证发现:

kubectl get --raw /apis/example.com
kubectl get --raw /apis/example.com/v1
kubectl api-resources --api-group=example.com

如果服务端发现中声明了 widgets,预期可以看到类似:

NAME      SHORTNAMES   APIVERSION       NAMESPACED   KIND
widgets   wdg          example.com/v1   true         Widget

这个结果只证明发现链路基本成立,不代表 getlistwatch 或写操作一定可用;这些动词仍必须逐项测试。


五、Extension API Server 的认证:两条 TLS 连接和一组代理头

API Aggregation 中至少存在两条独立的 HTTPS 连接:

客户端 --TLS 连接 1--> kube-apiserver
kube-apiserver --TLS 连接 2--> Extension API Server

连接 1 负责客户端与主 API Server 的安全通信。连接 2 负责主 API Server 与扩展服务器之间的安全通信。不要把这两条连接的证书混为一谈。

1. 主 API Server 如何把用户身份传给扩展服务器

主 API Server 先认证客户端。例如客户端可能使用:

  • ServiceAccount Bearer Token
  • 客户端证书
  • OIDC Token
  • Webhook Token Authentication
  • 其他由主 API Server 配置的认证方式

认证成功后,聚合层会把身份转发给 Extension API Server,常见请求头包括:

X-Remote-User: alice
X-Remote-Group: developers
X-Remote-Group: system:authenticated
X-Remote-Extra-<key>: <value>

扩展服务器不能仅仅因为请求带有 X-Remote-User 就信任它。它必须确认请求确实来自受信任的聚合层。

常见做法是:

  1. 主 API Server 使用代理客户端证书连接 Extension API Server。
  2. Extension API Server 信任签发该代理证书的 CA。
  3. Extension API Server 仅接受来自该受信任客户端证书的身份头。
  4. 任何绕过主 API Server 直接访问扩展服务器的请求,都不能伪造有效的代理身份。

主 API Server 侧常见相关配置包括:

--proxy-client-cert-file
--proxy-client-key-file
--requestheader-client-ca-file
--requestheader-allowed-names
--requestheader-username-headers
--requestheader-group-headers
--requestheader-extra-headers-prefix

Extension API Server 侧则需要配置与之匹配的 RequestHeader 认证参数,通常基于 generic API server 框架提供的 delegated authentication 配置。

具体参数名称和默认行为受 Kubernetes 发行版及组件版本影响,部署时必须检查对应版本的 kube-apiserver --help 和 Extension API Server 的配置文档。不能只复制另一套集群的参数,因为控制面证书和信任链通常不同。

2. 为什么需要双向信任

考虑一个错误实现:

Extension API Server:
    收到 X-Remote-User 就直接使用

如果这个服务可以被用户直接访问,攻击者可以构造:

X-Remote-User: system:admin

扩展服务器就会错误地把攻击者当成管理员。

正确的信任条件应近似为:

接受代理身份头
    当且仅当
请求来自通过 TLS 客户端证书认证的可信聚合层

如果扩展服务器使用 Kubernetes Service 暴露,应通过网络策略、Service 访问边界和监听地址限制绕过路径。TLS 客户端证书验证是身份头防伪的核心,NetworkPolicy 只是额外防护,不能替代证书验证。

3. 认证和授权是两个阶段

认证回答:

“请求者是谁?”

授权回答:

“这个身份是否允许对这个资源执行这个动作?”

主 API Server 认证成功后,扩展服务器仍需决定:

alice 是否允许:
GET /apis/example.com/v1/namespaces/default/widgets

扩展服务器可以:

  • 自己维护授权逻辑;
  • 调用主 API Server 的 SubjectAccessReview
  • 使用 generic API server 的 delegated authorization;
  • 结合自己的业务权限系统。

如果采用 SubjectAccessReview,扩展服务器使用的 ServiceAccount 需要有权创建相应的审查对象。例如常见的授权代理配置会授予访问:

authorization.k8s.io/subjectaccessreviews
authentication.k8s.io/tokenreviews

这里的逻辑是:

主 API Server 认证 alice
        │
        ▼
Extension API Server 接收 alice 的身份
        │
        ▼
Extension API Server 调用 SubjectAccessReview
        │
        ▼
主 API Server 根据 RBAC 判断 alice 是否可执行该动作

若只配置了认证而没有授权,结果通常不是“系统自动按照集群 RBAC 工作”,而是扩展服务器可能错误地放行、错误地拒绝,或者对所有请求返回 403 Forbidden

4. 一个可诊断的认证失败例子

如果主 API Server 与扩展服务器之间的代理证书不被信任,常见结果是:

x509: certificate signed by unknown authority

此时:

  • 客户端到主 API Server 的证书可能完全正常;
  • 资源发现仍可能失败;
  • kubectl get --raw /apis/example.com/v1 可能返回 503
  • APIServiceAvailable 条件可能是 False

如果 TLS 成功但代理身份认证失败,扩展服务器可能返回:

401 Unauthorized

如果认证成功但授权失败,则通常是:

403 Forbidden

通过区分 x509401403503,可以快速判断问题处于证书、认证、授权还是可达性阶段。


六、Extension API Server 必须实现什么 API 语义

API Aggregation 只解决“如何接入主 API Server”,不自动为扩展服务器生成资源处理器。扩展服务器仍需要实现 Kubernetes API 的行为。

1. 对象结构:Metadata、Spec 和 Status

一个典型资源可以具有如下结构:

apiVersion: example.com/v1
kind: Widget
metadata:
  name: demo
  namespace: default
  labels:
    app: demo
spec:
  size: 3
status:
  phase: Ready
  observedGeneration: 1

这些字段不是装饰:

  • metadata.name:对象在命名空间中的身份。
  • metadata.namespace:命名空间范围资源的作用域。
  • metadata.uid:对象生命周期中的稳定身份。
  • metadata.resourceVersion:并发控制和 Watch 位置。
  • metadata.generation:Spec 被修改后通常递增。
  • metadata.creationTimestamp:创建时间。
  • metadata.labelsannotations:索引、筛选和附加元数据。
  • spec:用户期望状态。
  • status:服务器或控制器观察到的实际状态。

如果扩展服务器声明支持 Kubernetes 风格对象,却把 metadata.resourceVersion 当作普通字符串随意覆盖,客户端的并发更新和 Watch 续接就会产生错误。

2. CRUD 和 HTTP 状态码

对于一个命名空间资源 widgets,常见路径是:

POST   /apis/example.com/v1/namespaces/default/widgets
GET    /apis/example.com/v1/namespaces/default/widgets
GET    /apis/example.com/v1/namespaces/default/widgets/demo
PUT    /apis/example.com/v1/namespaces/default/widgets/demo
PATCH  /apis/example.com/v1/namespaces/default/widgets/demo
DELETE /apis/example.com/v1/namespaces/default/widgets/demo

通常应遵守以下语义:

操作 成功状态 常见失败
创建 201 Created 409 Conflict422 Unprocessable Entity
获取 200 OK 404 Not Found
列表 200 OK 403 Forbidden
更新 200 OK 409 Conflict404 Not Found
删除 200 OK 或符合 API 约定的删除响应 404 Not Found
Watch 200 OK,持续返回事件 连接关闭或错误事件

错误响应也应使用 Kubernetes 风格的 Status 对象,而不是任意字符串:

{
  "kind": "Status",
  "apiVersion": "v1",
  "status": "Failure",
  "reason": "NotFound",
  "message": "widgets.example.com \"demo\" not found",
  "code": 404
}

这样 client-go、动态客户端和控制器才能正确识别错误原因。

3. resourceVersion 与乐观并发

假设客户端读取对象:

resourceVersion = 41

随后两个客户端同时更新:

客户端 A: 使用 resourceVersion=41 更新
客户端 B: 使用 resourceVersion=41 更新

正确的乐观并发过程是:

  1. A 提交,后端确认当前版本是 41。
  2. A 更新成功,后端版本变为 42。
  3. B 提交,仍声明自己基于版本 41。
  4. 后端发现当前版本已经是 42。
  5. B 返回 409 Conflict,而不是静默覆盖 A 的修改。

形式化地说,更新条件应满足:

请求中的 rv = 当前存储中的 rv

若条件不成立:

更新失败,返回 409

这正是 resourceVersion 的并发控制直觉。它不是业务对象的版本号,也不能被客户端随意递增。

如果 Extension API Server 后端是外部数据库,必须自己实现等价的 compare-and-swap 语义。例如 SQL 后端可以使用:

UPDATE widgets
SET spec = ?, resource_version = resource_version + 1
WHERE namespace = ?
  AND name = ?
  AND resource_version = ?;

受影响行数为 0 时,需要区分对象不存在和版本冲突,并返回正确的 Kubernetes 错误。

4. Watch 不是“定时重复 List”

控制器通常使用:

List -> Watch -> 处理事件 -> 从断点继续 Watch

Watch 事件至少包括:

ADDED
MODIFIED
DELETED
BOOKMARK

客户端可能从某个 resourceVersion 开始 Watch:

GET /apis/example.com/v1/widgets?watch=true&resourceVersion=42

扩展服务器必须保证:

  1. 返回对象的 metadata.resourceVersion 可用于后续续接。
  2. 事件顺序满足后端定义的可观察顺序。
  3. 已删除对象可以返回 DELETED 事件,通常携带最后已知对象。
  4. 历史版本过期时返回可识别的过期错误,客户端再重新 List。
  5. 长连接不会因为服务器内部没有立即事件而被错误关闭。

若实现只是每隔几秒重新查询数据库并把全部对象当成 MODIFIED,控制器可能重复处理、产生不必要负载,甚至无法正确判断删除事件。

5. List 分页和一致性

大型资源列表可能使用:

?limit=100

响应中带有继续令牌:

{
  "metadata": {
    "resourceVersion": "52",
    "continue": "..."
  }
}

扩展服务器需要保证 Continue Token 的语义:

  • 令牌不能被客户端伪造后访问任意数据;
  • 令牌失效时返回明确错误;
  • 分页期间应具有合理的一致性;
  • remainingItemCount 等字段如果实现,必须与实际结果匹配。

是否完整支持分页、Watch、字段选择器、标签选择器和 Table 转换,都会影响 kubectl 和控制器的兼容性。发现接口中声明支持的能力必须与真实实现一致。


七、扩展 API Server 的 Go 实现边界

生产级 Extension API Server 通常基于 Kubernetes 的 generic API server 体系构建,而不是从 net/http 手写所有协议。原因是 generic API server 可以复用:

  • 认证链;
  • RequestInfo 解析;
  • delegated authentication;
  • delegated authorization;
  • API 发现;
  • REST Storage 接口;
  • Watch 编码;
  • Kubernetes 风格错误;
  • OpenAPI 和审计相关能力。

client-go 主要用于访问 Kubernetes API,不是一个“自动把 Go struct 变成聚合 API”的框架。一个扩展服务器往往同时需要两类客户端:

Extension API Server
  ├── 接收聚合 API 请求
  └── 使用 client-go 访问主 Kubernetes API

例如它可能在处理请求时调用:

typedClient.CoreV1().Namespaces().Get(ctx, name, metav1.GetOptions{})

但这只代表扩展服务器作为客户端访问内置资源,并不会自动实现它自己的:

/apis/example.com/v1/widgets

扩展服务器仍需注册自己的 GroupVersion、资源类型和 REST Storage。

1. 处理请求时不要混淆四个版本

一个请求中可能同时出现:

HTTP 路径版本: example.com/v1
对象 apiVersion: example.com/v1
Go 内部类型版本: example.com/v1alpha1
存储格式版本: 数据库自定义格式

它们不一定相同。若扩展服务器提供多个外部版本,应明确:

外部版本 -> 内部版本 -> 存储格式

并在边界处完成转换。不能把 Go 包路径直接当成 Kubernetes API 版本,也不能认为修改 apiVersion 字符串就完成了版本兼容。

2. Admission Webhook 与 Aggregation 的边界

Admission Webhook 和 API Aggregation 都会出现在扩展系统中,但它们处于不同阶段。

例如创建内置 Deployment:

客户端
  -> kube-apiserver
     -> 认证
     -> Admission Webhook
     -> 内置资源 REST Storage
     -> etcd

而访问聚合资源:

客户端
  -> kube-apiserver
     -> 认证
     -> 聚合路由
     -> Extension API Server
        -> 扩展存储

如果目标是给内置 Pod 增加校验,应使用 Validating Admission Webhook;如果目标是修改 Pod,应使用 Mutating Admission Webhook。Webhook 不能让一个新的 API group 出现在发现接口中,也不能替代 Extension API Server 的 CRUD 和 Watch。


八、可用性:APIService 的 Available 状态意味着什么

1. APIService 状态不是扩展业务健康的完整证明

查看状态:

kubectl get apiservice v1.example.com -o yaml

常见状态结构:

status:
  conditions:
    - type: Available
      status: "True"
      reason: Passed
      message: all checks passed

当扩展服务器不可达或发现失败时,可能看到:

status:
  conditions:
    - type: Available
      status: "False"
      reason: ServiceAccessError
      message: failing or incomplete response from server

Available=True 通常说明聚合层能够访问后端并完成基本检查,不能推出以下结论:

  • 所有资源的 GET 都正确;
  • 所有用户都有正确权限;
  • 写入一定成功;
  • Watch 不会断开;
  • 后端数据库没有数据损坏;
  • 多副本之间没有状态分歧。

因此,可用性至少应分层观测:

APIService Available
    ↓
发现接口成功
    ↓
匿名或测试身份认证行为正确
    ↓
授权成功/拒绝符合预期
    ↓
GET、LIST、WATCH、写操作正确
    ↓
扩展后端存储和外部依赖健康

2. 常见失败路径

Service 没有 Endpoint

检查:

kubectl -n example-system get svc example-apiserver
kubectl -n example-system get endpointslice \
  -l kubernetes.io/service-name=example-apiserver
kubectl -n example-system get pods -l app=example-apiserver

如果 EndpointSlice 为空,主 API Server 无法把请求送到 Pod。应检查:

  • Service selector 是否匹配 Pod 标签;
  • Pod 是否 Ready;
  • targetPort 是否存在;
  • 容器是否监听预期端口;
  • Service 是否位于 APIService.spec.service.namespace 指定的命名空间。

TLS SAN 或 CA 不匹配

检查扩展服务器日志和 APIService 状态:

kubectl describe apiservice v1.example.com
kubectl -n example-system logs deploy/example-apiserver

常见错误包括:

x509: certificate is valid for example-apiserver.example.com,
not example-apiserver.example-system.svc

或者:

x509: certificate signed by unknown authority

修复步骤通常是:

  1. 确认 Service DNS 名称。
  2. 重新签发包含正确 SAN 的服务端证书。
  3. 把签发该证书的 CA 编码到 spec.caBundle
  4. 重启或热加载扩展服务器证书。
  5. 重新检查 Available 和发现接口。

端口配置不一致

例如:

spec:
  service:
    port: 443

但 Service 只暴露:

ports:
  - port: 8443

聚合层连接的目标端口就不成立。这里的 APIService.spec.service.port 指向 Service port,不是容器端口;容器端口由 Service 的 targetPort 决定。

扩展服务器启动但发现接口错误

Pod 可能处于 Ready,TLS 也正常,但:

GET /apis/example.com/v1

返回非 APIResourceList 的内容,或 groupVersion 与注册版本不一致。这会导致 APIService 仍不可用,或者客户端发现失败。Readiness Probe 只检查进程存活并不能替代 Kubernetes API 发现检查。

3. 依赖故障如何传播

假设扩展服务器的请求都依赖外部云 API:

kubectl
  -> kube-apiserver
     -> Extension API Server
        -> 云 API

云 API 超时时,最外层可能表现为:

kubectl 命令卡住或最终超时

如果扩展服务器没有设置合理的上下文超时,它可能继续占用连接和 goroutine,导致更多请求排队。正确的处理方式是:

  • 继承并检查 HTTP 请求的 context;
  • 为外部调用设置明确超时;
  • 将依赖错误转换为适当的 5xx 或 Kubernetes Status
  • 对读和写区分重试策略;
  • 不要对非幂等写请求盲目重试。

API Aggregation 本身不会替扩展服务器做业务级熔断,也不会把外部系统自动变成高可用系统。


九、高可用设计:多副本、负载均衡和状态一致性

1. Service 多副本解决的是入口可达性

一个常见部署形态是:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: example-apiserver
  namespace: example-system
spec:
  replicas: 3
  selector:
    matchLabels:
      app: example-apiserver
  template:
    metadata:
      labels:
        app: example-apiserver
    spec:
      containers:
        - name: apiserver
          image: example/apiserver:1.0.0
          ports:
            - name: https
              containerPort: 8443

Service 会把请求分配到多个 Pod,但这只保证入口层面可以有多个后端。若三个 Pod 各自使用本地内存保存资源:

Pod A: widgets = {demo}
Pod B: widgets = {}
Pod C: widgets = {demo, test}

那么 List、Get、Watch 结果会随请求落点变化,资源语义不再一致。

生产可用的状态设计通常是:

多个无状态 Extension API Server Pod
              │
              ▼
      共享且具备并发语义的存储

这个存储可以是数据库、云资源系统或专门的后端服务,但必须明确:

  • 谁是权威数据源;
  • 如何生成和比较 resourceVersion
  • 如何产生 Watch 事件;
  • 如何处理删除和重连;
  • 如何保证多个副本看到一致状态。

2. Watch 连接对扩缩容更敏感

一个 Watch 是长连接:

客户端 -> Pod A

当 Pod A 被终止时,连接会断开。正常的 Kubernetes 客户端会重新 List/Watch,但扩展服务器必须保证:

  1. 断线后的新 List 能看到最新状态;
  2. 客户端使用旧 resourceVersion 继续 Watch 时,后端能判断该版本是否仍可用;
  3. 不会因为 Pod 间本地缓存不一致而漏掉删除或更新事件。

如果扩展服务器用本地缓存加速读取,需要把缓存失效、事件传播和重启恢复作为正式设计,而不是把缓存当成权威存储。

3. 滚动升级和 API 兼容

更新 Extension API Server 时,旧 Pod 和新 Pod 可能同时处理请求。若两个版本的对象编码、字段校验或存储格式不兼容,就可能出现:

旧 Pod 能读取,新 Pod 不能读取
新 Pod 写入,旧 Pod 返回 500
List 成功,但 Watch 事件无法解码

因此升级顺序通常应满足:

先让新代码兼容旧数据
再逐步替换旧实例
最后再清理旧字段或旧存储格式

如果要移除一个已发布的 API group/version,还必须先处理所有客户端、控制器和已存储对象;删除 APIService 会使对应 URL 立即失去路由,不等于平滑弃用。


十、API Aggregation 与 CRD 的选择

1. CRD 更适合什么

如果资源满足以下特征,CRD 往往更合适:

  • 数据可以存储在主集群 etcd;
  • 需要标准 CRUD、List、Watch;
  • 不需要自定义复杂存储协议;
  • 可以使用 CRD schema、默认值和转换机制;
  • 资源 API 与 Kubernetes 对象模型相近。

例如:

Database
Backup
Application

这些对象通常是声明式配置,CRD 加 Operator 就能满足需求。

2. Extension API Server 更适合什么

聚合 API 更适合:

  • 数据来自外部系统,而不是主集群 etcd;
  • 资源数量或查询方式不适合直接存入 etcd;
  • 需要特殊的读取、聚合或计算逻辑;
  • 需要更强地控制 API 的存储和生命周期;
  • 提供类似指标的短生命周期、只读数据。

Metrics Server 就是典型例子:节点和 Pod 指标通常来自实时采集数据,不适合简单地当作普通 Kubernetes 对象持久化。

3. 反例:不要为了“看起来像 Kubernetes”而聚合

假设只需要保存:

kind: Team
spec:
  owner: alice

如果没有外部数据源、特殊一致性要求或独立 API 生命周期,使用 Extension API Server 会额外引入:

  • TLS 证书管理;
  • RequestHeader 认证;
  • APIService 可用性;
  • Service 网络;
  • 发现接口;
  • 自己实现 REST 和 Watch;
  • 独立存储高可用。

这时 CRD 通常具有更少的故障路径。API Aggregation 的价值不在于“可以自定义 URL”,而在于它允许一个独立 API 后端真正拥有资源 API 的实现责任。


十一、从客户端角度验证一条完整链路

下面是一组有层次的检查命令。假设资源注册为 example.com/v1

第一步:确认注册对象

kubectl get apiservice v1.example.com
kubectl describe apiservice v1.example.com

检查:

Available=True

如果是 False,先不要测试资源 CRUD,因为路由或发现基础设施还没有成立。

第二步:确认主 API Server 能返回组发现

kubectl get --raw /apis/example.com

预期是包含 APIGroup 的 JSON,而不是 HTML、空响应或 503

再检查版本发现:

kubectl get --raw /apis/example.com/v1

预期包含:

{
  "groupVersion": "example.com/v1",
  "resources": [...]
}

第三步:确认资源映射

kubectl api-resources --api-group=example.com

如果没有 widgets,应检查:

  • Extension API Server 是否注册了正确的 Kind;
  • 发现响应中的 name 是否是复数资源名;
  • APIService.spec.groupspec.version 是否与发现响应一致;
  • 主 API Server 是否缓存了旧发现结果,必要时等待缓存刷新或重启相关客户端。

第四步:直接测试资源请求

kubectl get --raw \
  /apis/example.com/v1/namespaces/default/widgets

预期是 WidgetList 或符合约定的列表响应。

使用普通客户端命令时:

kubectl get widgets -n default
kubectl get widget demo -n default -o yaml

若发现成功但 kubectl get 返回 404,优先检查资源路由和复数名;若返回 403,优先检查扩展服务器授权;若返回 503,优先检查后端可达性和 APIService 状态。

第五步:验证认证和授权边界

至少测试两种身份:

允许访问的 ServiceAccount
明确不允许访问的普通用户或 ServiceAccount

期望结果不是“所有请求都成功”,而是:

有权限身份 -> 200
无权限身份 -> 403
未认证身份 -> 401 或由主 API Server 拒绝

还要测试绕过主 API Server 直接访问 Pod 或 Service 的情况。如果绕过后可以伪造 X-Remote-User 获得权限,说明扩展服务器错误信任了代理头。


十二、生产故障诊断的顺序

一个可靠的诊断顺序应从外到内,避免直接修改 RBAC 掩盖网络问题。

1. APIService 状态

kubectl get apiservice v1.example.com -o yaml

先看:

status.conditions

关注:

  • type: Available
  • status
  • reason
  • message

2. Service 和 EndpointSlice

kubectl -n example-system get svc example-apiserver -o yaml
kubectl -n example-system get endpointslice \
  -l kubernetes.io/service-name=example-apiserver

确认:

Service 端口存在
EndpointSlice 有地址
Pod Ready
端口与 targetPort 对应

3. Pod 日志和证书

kubectl -n example-system get pods -l app=example-apiserver
kubectl -n example-system logs deploy/example-apiserver

重点查找:

x509
certificate
requestheader
unauthorized
forbidden
timeout
watch
resourceVersion

4. 从主 API Server 的视角检查

kubectl exec 到普通业务 Pod 中测试扩展 Service,并不能完全复现主 API Server 到后端的访问路径。主 API Server 可能运行在控制面节点、静态 Pod 或独立网络中。

应结合:

  • 控制面节点网络;
  • API Server 日志;
  • Service DNS;
  • NetworkPolicy;
  • 防火墙和云安全组;
  • Service 的 ClusterIP 与后端端口。

云厂商托管控制面还可能限制控制面到集群 Service 的网络方式,不能默认认为所有托管集群都拥有相同的聚合网络实现。

5. 最后再检查授权

当 TLS、发现和请求到达都正常后,才检查:

  • 主 API Server 是否传递正确身份;
  • Extension API Server 是否启用 RequestHeader 认证;
  • delegated authorization 的 CA 和地址是否正确;
  • 扩展服务器的 ServiceAccount 是否有权执行 TokenReview 或 SubjectAccessReview;
  • 业务资源的 RBAC 规则是否符合预期。

十三、规范保证、常见实现和版本边界

需要区分三类信息。

Kubernetes API 机制保证的部分

Kubernetes 为 API Aggregation 定义了:

  • 通过 APIService 注册 group/version;
  • 通过主 API Server 代理聚合请求;
  • 提供聚合 API 的发现入口;
  • 使用 Service 或 URL 定位后端;
  • 通过 TLS 和代理身份头连接扩展服务器。

常见实现方式

社区和发行版通常使用:

  • generic API server;
  • RequestHeader 认证;
  • Service 访问扩展服务器;
  • caBundle 验证后端证书;
  • delegated authentication/authorization;
  • client-go 访问主 API Server;
  • Deployment 加 Service 提供扩展服务器副本。

这些是常见实现,不代表所有自定义 API Server 都必须使用完全相同的代码结构。

需要特别检查版本的部分

以下内容容易发生版本偏差:

  • kube-apiserver 的认证参数;
  • generic API server 的配置结构;
  • Discovery 的具体响应字段;
  • Watch 的 Bookmark、分页和缓存行为;
  • APIService 中部分字段的弃用状态;
  • 云厂商控制面到扩展 Service 的网络实现;
  • Kubernetes 客户端对发现缓存和错误的展示方式。

因此,代码和配置应以目标 Kubernetes 版本对应的 API 文档、组件帮助信息和 k8s.io 模块版本为准。不能因为某个旧版本示例使用了 v1beta1,就假定当前稳定版本仍推荐该版本。


十四、核心因果关系

API Aggregation 的工作条件可以简化为:

请求可用
=
客户端能认证
∧ APIService 正确注册
∧ 主 API Server 能发现 group/version
∧ 主 API Server 能访问 Service
∧ 后端 TLS 证书受信任
∧ Extension API Server 能验证代理身份
∧ Extension API Server 能完成授权
∧ 资源路由和 API 语义实现正确
∧ 后端存储或外部依赖可用

其中任意一项失败,都可能使“同一个 kubectl get”呈现完全不同的错误:

注册错误       -> 404 或没有发现资源
后端不可达     -> 503
证书错误       -> x509
认证失败       -> 401
授权失败       -> 403
资源路由错误   -> 404
并发版本冲突   -> 409
后端异常       -> 5xx

理解这条链路后,Extension API Server 就不再是“向 Kubernetes 添加一个 HTTP 路径”,而是一个需要同时满足发现、认证、授权、资源语义、存储一致性和高可用条件的独立 API 控制面组件。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。