基于 k8s.io/apiserver 构建扩展 API Server:sample-apiserver(Wardle)从源码到部署全解析
sample-apiserver 是 Kubernetes 官方提供的一个"最小可运行"的演示性 API Server,它演示了如何使用 k8s.io/apiserver 通用库从头构建一个符合 Kubernetes 约定(REST 语义、版本化、验证、鉴权、etcd 存储、OpenAPI)的独立 API 服务器。本文将以其在 Kubernetes 仓库中的源码位置(staging/src/k8s.io/sample-apiserver)与官方 README 为核心骨架,结合仓库内真实源码与 artifacts/ 部署清单,系统讲解它的设计动机、API 类型定义、代码生成、集群内聚合部署(API Aggregation)与单机自签名证书运行(X.509 认证)的完整流程。读完本文,你将能够:判断"CRD vs 扩展 API Server"的技术选型、fork 并定制这套样板代码为自己的 API 类型、看懂它从 main.go 到 registry/strategy 的启动链路,并能在本地把它跑起来。
本文所有路径均相对于 Kubernetes 仓库根目录;代码副本位于 staging/src/k8s.io/sample-apiserver。
sample-apiserver 解决什么问题
如 README.md 所述,sample-apiserver 的核心价值是:
Demonstration of how to use the k8s.io/apiserver library to build a functional API server.
即:演示如何用 k8s.io/apiserver 库构建一个"功能完整"的 API Server。它的目标读者是两类开发者:
- 想构建 Extension API Server 并通过 API Aggregation(API 聚合) 接入 Kubernetes 集群的人;
- 想构建 独立运行的、Kubernetes 风格的 API Server 的人。
示例中的 API 分组名为 wardle.example.com,因此它也常被称为 "wardle server"。仓库中注册了两类演示资源:
Flunder:一个带 spec/status 的命名空间级(namespaced)示例资源,还支持 flunder 与 fischer 两种引用;Fischer:一个**非命名空间级(cluster-scoped)**的示例资源,持有"禁止创建的 Flunder 名称列表"。
这些资源类型在 pkg/apis/wardle/types.go(内部版本)与 pkg/apis/wardle/v1alpha1/types.go、v1beta1 对应文件中定义。
先想清楚:CRD、apiserver-builder 还是 sample-apiserver
README 开篇非常坦诚地提醒使用者,在动手之前先评估另外两条"少走弯路"的路线:
- CRD(Custom Resource Definition):如果只是想往 Kubernetes 集群里"加一种资源",优先考虑 CRD。它的编码量与后续 rebase 维护成本都远低于扩展 API Server。关于两者差异,可参考 Kubernetes 官方文档中 Custom Resources 一节对 CRD 与 API 聚合(aggregation)的阐述。
- apiserver-builder:如果确定要构建扩展 API Server,官方曾孵化过 apiserver-builder 框架,它可以自动生成 apiserver、client 库与安装程序,比"手工 fork 本仓库"更省事。注意该框架曾托管于 kubernetes-incubator 组织。
如果最终仍选择基于 sample-apiserver 开发,README 推荐的模式是:fork 本仓库 → 修改 pkg/apis/.../types.go 加入自己的类型 → 定期 rebase 到上游,从而持续获取 apiserver 库的改进与 bug 修复。
兼容性与来源
README 明确了两个重要约束:
- 本仓库 HEAD 与
k8s.io/apiserver、k8s.io/apimachinery、k8s.io/client-go的 HEAD 保持对齐,因此当你升级这些依赖版本时,需要同步考虑与 sample-apiserver 的配套关系; - 它本质上是 Kubernetes 主仓库
staging/src/k8s.io/sample-apiserver目录被同步到独立外部仓库的产物(staged repository),代码的修改入口在主仓库的 staging 目录,先合入 Kubernetes,再同步发布。因此本仓库只读,不用于直接贡献。贡献相关约定见 staging/src/k8s.io/sample-apiserver/CONTRIBUTING.md。
从当前仓库看,该目录的 go module 名为 k8s.io/sample-apiserver(见 go.mod),因此 README 特别提醒:go-get 或以 vendor 方式引入此包时,路径一律写作 k8s.io/sample-apiserver。
获取代码与准备依赖
如果你已经在按 Kubernetes 社区标准的 fork-and-branch 流程开发,那么本演示代码其实就躺在主仓库的 staging/src/k8s.io/sample-apiserver,其依赖(包括 code-generator)也都处于可用的 GOPATH 布局中,无需额外 clone。
如果要在自己的工作区独立获取该库副本,流程是:
git clone <sample-apiserver 外部仓库地址>
cd sample-apiserver
Go modules 场景下的特殊提醒
README 指出:如果打算修改类型后重新生成代码(generate code),那么 code-generator 仓库必须存在于"旧式"位置(GOPATH 风格)。一个便捷做法是执行 go mod vendor 生成并填充 vendor 目录,让脚本能在传统路径下定位 code-generator。这一要求直接对应 hack/update-codegen.sh 中的探测逻辑:
CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo ../code-generator)}
脚本会优先在仓库自身 vendor/k8s.io/code-generator 下寻找 code-generator,找不到则退回相对路径 ../code-generator。
理解并定制 API 类型:Flunder 与 Fischer
开发扩展 API Server 的第一步是定义你自己的资源类型。sample-apiserver 用一个"动物园命名"(Wardle)展示了两类典型资源形态。
内部版本类型定义
内部版本(internal version)定义在 pkg/apis/wardle/types.go,其中:
Flunder(命名空间级)含Spec与Status,注释标记// +genclient表示需要生成 client,// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object表示需要生成 deepcopy;Fischer(集群级)用// +genclient:nonNamespaced标记非命名空间语义,含DisallowedFlunders []string;FlunderSpec通过FlunderReference/FischerReference两个互斥字段与ReferenceType枚举,演示"一个 spec 里如何做引用建模"。
type FlunderSpec struct {
FlunderReference string // 另一个 flunder 的名字,与 FischerReference 互斥
FischerReference string // 一个 fischer 的名字,与 FlunderReference 互斥
ReferenceType ReferenceType
}
// +genclient
// +genclient:nonNamespaced
type Fischer struct {
metav1.TypeMeta
metav1.ObjectMeta
DisallowedFlunders []string // 被禁止的 Flunder.Name 列表
}
对外版本(v1alpha1 / v1beta1)
对外可序列化版本放在 pkg/apis/wardle/v1alpha1 与 pkg/apis/wardle/v1beta1 子目录。以 v1alpha1/types.go 为例,它给每个字段补充了 JSON / protobuf tag,并通过 // +k8s:prerelease-lifecycle-gen:introduced=1.0、removed=1.10 注释声明 API 生命周期(引入版本 / 移除版本,配合 zz_generated.prerelease-lifecycle.go 供生命周期验证使用)。注意 v1alpha1 与内部版本在 spec 形状上有差异(v1alpha1 把引用收敛为单个 Reference string + 可空 ReferenceType *ReferenceType),这种差异正是通过 conversion(见 v1alpha1/conversion.go 与生成的 zz_generated.conversion.go)与 defaulting(defaults.go / zz_generated.defaults.go)来弥合的——这也说明了为什么要为每个版本维护一套独立的 types 与生成物。
类型注册
pkg/apis/wardle/register.go 定义了组名常量:
const GroupName = "wardle.example.com"
并通过 SchemeBuilder 将 Flunder/FlunderList/Fischer/FischerList 注册进 runtime scheme。所有版本(v1alpha1、v1beta1)的 types 最终都要能被 install 包统一装入全局 Scheme。
何时需要重新生成代码
README 明确:只要修改了任意 pkg/apis/.../types.go 中的 API 类型定义,就必须重新生成派生文件(deepcopy、conversion、defaults、applyconfiguration、clientset、informers、listers、openapi 定义等)。操作步骤:
- 如尚未有
vendor目录,先按上文创建(Go modules 场景); - 以 sample-apiserver 作为当前工作目录,执行
hack/update-codegen.sh(脚本不接受参数)。
该脚本内部(见 hack/update-codegen.sh)按顺序调用 kube::codegen::gen_helpers(deepcopy 等基础生成)、gen_openapi(输出到 pkg/generated/openapi,模型名文件为 zz_generated.model_name.go,并支持通过 API_KNOWN_VIOLATIONS_DIR / --update-report 维护已知违规清单)、gen_client(--with-watch --with-applyconfig,生成 clientset / informers / listers / applyconfiguration,落到 pkg/generated 下)。也就是说,你改完类型后,pkg/generated/ 整棵树的产物都应视作"需要重新生成而非手改"。
当前仓库中的生成产物确实覆盖了这些品类,例如:
- clientset(含 fake 版本,供测试使用);
- informers;
- listers;
- applyconfiguration;
- openapi。
常规构建与部署(Normal Build and Deploy)
启动入口与参数体系:从 main.go 到 RecommendedOptions
程序入口 main.go 非常精简:
func main() {
ctx := genericapiserver.SetupSignalContext()
options := server.NewWardleServerOptions(os.Stdout, os.Stderr)
cmd := server.NewCommandStartWardleServer(ctx, options, false)
code := cli.Run(cmd)
os.Exit(code)
}
真正的组装在 pkg/cmd/server/start.go。这里有几个值得深入理解的设计点:
NewWardleServerOptions通过genericoptions.NewRecommendedOptions(defaultEtcdPathPrefix, ...)一次性获得 kube-apiserver 同款的推荐命令行选项集(etcd 存储、安全端口、认证、授权、准入、审计等),etcd 键前缀为常量defaultEtcdPathPrefix = "/registry/wardle.example.com";NewCommandStartWardleServer把这些选项注册成 cobra 命令的 flags,因此你的自定义 server 天然获得与官方组件一致的--secure-port、--etcd-servers、--client-ca-file、--kubeconfig、--authentication-kubeconfig、--authorization-kubeconfig、--v(日志级别)等参数;- 它还演示了 KEP-4330 的组件版本与 FeatureGate 机制:注册 "Wardle" 组件的 effective version(二进制版本
1.2、emulation version、最低兼容版本),并声明了一个从 Alpha(1.0 默认关)→ Beta(1.1 默认开)→ GA(1.2 锁定为开)演进的示例 FeatureGateBanFlunder(见 start.go 中WardleVersionToKubeVersion的映射逻辑与BanFlunder的版本化规格)。
BanFlunder 这个开关被 Complete() 使用:当它开启时才向准入插件链注册 banflunder 插件(见下)。换言之,你可以把这段样板当作"如何在自定义 server 里挂接版本化 feature gate + 准入插件"的活教材。
API 组装与存储注册
pkg/apiserver/apiserver.go 演示了"如何把一个自定义 API group 安装进通用 apiserver":
init()中install.Install(Scheme)装入所有 wardle 版本类型,并补齐 metav1 与无版本类型;Config.Complete()完成通用配置;New()调用c.GenericConfig.New("sample-apiserver", ...)创建底层GenericAPIServer;- 构造
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(wardle.GroupName, Scheme, ...); - 为
v1alpha1与v1beta1分别建立"资源名 → REST storage"映射:flunders与fischers都对应各自的 etcd REST 实现,其中fischerstorage.NewREST是集群级资源、flunderstorage.NewREST是命名空间级资源(注意 v1alpha1 暴露 flunders+fischers,而 v1beta1 只暴露 flunders); - 最后
s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo)把整个 API group 安装进 HTTP 路由树。
REST 存储的"策略"部分(validation、namespace-scoped、create/update 语义)在 pkg/registry/wardle/flunder/strategy.go 实现:flunderStrategy.NamespaceScoped() 返回 true,Validate 委托给 pkg/apis/wardle/validation/validation.go;底层的 etcd 存取由 pkg/registry/wardle/flunder/etcd.go 用 generic.NewRegistry 把 strategy 接到通用存储后端,从而获得 list/watch/create/update/delete 全套 REST 能力。这即是"从结构上理解 k8s.io/apiserver:types → scheme → registry(strategy+storage) → InstallAPIGroup"这条主线。
认证插件(Authentication plugins)
默认构建只支持非常精简的认证方式集合;k8s.io/client-go 的 plugin/pkg/client/auth 下还有大量认证插件(如 oidc)。如需启用其中某一种(例如 oidc),在 main.go 增加对应包的空导入即可:
import _ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
如果想一次性启用全部认证插件:
import _ "k8s.io/client-go/plugin/pkg/client/auth"
这利用了 Go 包 init() 机制:空导入会触发插件向认证层注册。
构建二进制
以 sample-apiserver 为当前工作目录,执行(输出到容器构建目录):
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o artifacts/simple-image/kube-sample-apiserver
禁用 CGO、交叉编译到 linux/amd64,产出静态二进制 kube-sample-apiserver,可直接打进容器。本地纯测试也可以只执行 export GOOS=linux; go build . 得到当前目录下的 sample-apiserver 二进制(此方式在 docs/minikube-walkthrough.md 有配套说明)。
构建并推送容器镜像
artifacts/simple-image/Dockerfile 极简——基于 fedora 镜像,把二进制放到根目录作为 entrypoint:
FROM fedora
ADD kube-sample-apiserver /
ENTRYPOINT ["/kube-sample-apiserver"]
将 MYPREFIX、MYTAG 替换为你的镜像仓库与标签后执行:
docker build -t MYPREFIX/kube-sample-apiserver:MYTAG ./artifacts/simple-image
docker push MYPREFIX/kube-sample-apiserver:MYTAG
部署进 Kubernetes 集群(API Aggregation)
编辑 artifacts/example/deployment.yaml,把 pod 模板中的 image 改成刚才推送的镜像、把 imagePullPolicy 设为合适值,然后一次性应用整个 artifacts/example 目录:
kubectl apply -f artifacts/example
artifacts/example/ 下各清单的职责(结合仓库文件逐一说明,其中大部分在 minikube walkthrough 中被依次创建):
| 文件 | 作用 |
|---|---|
| ns.yaml | 命名空间 wardle,隔离运行扩展 apiserver |
| sa.yaml | ServiceAccount apiserver,pod 以其身份运行(serviceAccountName: apiserver) |
| auth-delegator.yaml | ClusterRoleBinding,把 system:auth-delegator ClusterRole 授予该 SA,允许它把鉴权委托回主 apiserver |
| auth-reader.yaml | kube-system 下的 RoleBinding,授予 extension-apiserver-authentication-reader,让扩展 server 能读取主 apiserver 的认证配置(configmap) |
| rbac.yaml | ClusterRole aggregated-apiserver-clusterrole:允许 list/watch namespaces、admissionregistration.k8s.io 相关资源以及 flowcontrol.apiserver.k8s.io 的 flowschemas/prioritylevelconfigurations(供共享 informer 与准入控制器使用) |
| rbac-bind.yaml | ClusterRoleBinding,把上述 ClusterRole 绑定给 wardle 命名空间的 apiserver SA |
| deployment.yaml | Deployment wardle-server:sidecar 方式同 Pod 运行 etcd(registry.k8s.io/etcd:v3.7.0)与 kube-sample-apiserver,参数 --etcd-servers=http://localhost:2379;这就是演示用的存储方案 |
| service.yaml | Service api(443→443),把扩展 apiserver 暴露给集群内聚合器(kube-apiserver) |
| apiservice.yaml | APIService v1alpha1.wardle.example.com:告诉主 apiserver 该 API group 存在于哪个 Service,从而完成聚合注册 |
其中 apiservice.yaml 的关键字段为:
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
name: v1alpha1.wardle.example.com
spec:
insecureSkipTLSVerify: true
group: wardle.example.com
groupPriorityMinimum: 1000
versionPriority: 15
service:
name: api
namespace: wardle
version: v1alpha1
创建 APIService 后,kube-apiserver 就会把指向 /apis/wardle.example.com/v1alpha1/... 的请求代理到 wardle/api 这个 Service。
验证:创建并读取 Flunder
仓库在 artifacts/flunders/01-flunder.yaml 提供了一个现成的示例资源:
apiVersion: wardle.example.com/v1alpha1
kind: Flunder
metadata:
name: my-first-flunder
labels:
sample-label: "true"
聚合生效后即可用 kubectl 直接操作新资源:
kubectl create -f artifacts/flunders/01-flunder.yaml
# flunder "my-first-flunder" created
kubectl get flunder my-first-flunder
# NAME KIND
# my-first-flunder Flunder.v1alpha1.wardle.example.com
完整的 minikube 演练(预置条件、创建 namespace/SA/RBAC/Service/APIService 的逐条命令)可参考 docs/minikube-walkthrough.md。
准入控制演示:banflunder
为了演示扩展 server 如何注册自定义准入插件,仓库提供了 banflunder 插件(pkg/admission/plugin/banflunder/admission.go):当某个 Fischer 在 DisallowedFlunders 中列出了一个名字时,禁止创建同名 Flunder。插件通过 wardleinitializer(pkg/admission/wardleinitializer/wardleinitializer.go)被注入 informer 以获取 Fischer 列表,注册与否受上文 BanFlunder FeatureGate 控制。配套的单元测试位于 admission_test.go,是理解"扩展 apiserver 里准入链如何工作"的极佳入口。
独立运行模式(Running it stand-alone)
开发阶段把扩展 server 独立跑起来(不依赖主 kube-apiserver 做认证/授权、不做聚合)会方便很多。README 给出了完整可操作的方案:放弃聚合器内嵌的认证链,改用本地客户端证书的 X.509 认证与授权。
原理:客户端证书被一个自建 CA 信任,并且证书中携带了 system:masters 群组成员身份。由于用 --authorization-skip-lookup 禁用了委派授权查询,唯一被授权的就是 system:masters 这个超级用户组。该模式仍需要一份 kubeconfig(如 ~/.kube/config),但 Kubernetes 集群只被用于共享 informer 读取资源,不参与认证/授权;用 minikube 或主仓库 hack/local-up-cluster.sh 起一个集群即可满足。
第一步:生成自建 CA
openssl req -nodes -new -x509 -keyout ca.key -out ca.crt
第二步:为超级用户签发客户端证书
为用户 development、超级用户组 system:masters 签发由上述 CA 签名的客户端证书:
openssl req -out client.csr -new -newkey rsa:4096 -nodes -keyout client.key -subj "/CN=development/O=system:masters"
openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -sha256 -out client.crt
注意 -subj 中的 /O=system:masters 正是把用户划入超级用户组的关键。
第三步:导出 p12(供 curl 使用)
curl 需要带口令的 p12 格式客户端证书,因此做一次转换:
openssl pkcs12 -export -in ./client.crt -inkey ./client.key -out client.p12 -passout pass:password
第四步:启动 etcd 与 sample-apiserver
etcd &
sample-apiserver --secure-port 8443 --etcd-servers http://127.0.0.1:2379 --v=7 \
--client-ca-file ca.crt \
--kubeconfig ~/.kube/config \
--authentication-kubeconfig ~/.kube/config \
--authorization-kubeconfig ~/.kube/config
三个 kubeconfig 各司其职(README 特别说明):
- 第一个(
--kubeconfig):供共享 informer 访问 Kubernetes 资源; - 第二个(
--authentication-kubeconfig):用于满足委派认证器的配置要求; - 第三个(
--authorization-kubeconfig):用于满足委派授权器的配置要求。
但由于 --client-ca-file 的存在,你的开发用 X.509 证书会被直接接受并认证为 system:masters 成员;system:masters 是超级用户组,因此委派授权会被跳过——认证器与授权器实际上都不会被调用。
第五步:访问扩展 API
用 p12 客户端证书调用 curl:
curl -fv -k --cert-type P12 --cert client.p12:password \
https://localhost:8443/apis/wardle.example.com/v1alpha1/namespaces/default/flunders
或改用 wget(直接用 PEM 证书与私钥):
wget -O- --no-check-certificate \
--certificate client.crt --private-key client.key \
https://localhost:8443/apis/wardle.example.com/v1alpha1/namespaces/default/flunders
备注:部分新版 macOS 的 curl 处理客户端证书存在问题。在 Mac 上可改用 httpie(brew install httpie):
http --verify=no --cert client.crt --cert-key client.key \
https://localhost:8443/apis/wardle.example.com/v1alpha1/namespaces/default/flunders
把样板改造成你自己的 API Server:推荐路径小结
综合 README 与仓库源码,落地一个自定义扩展 API Server 的推荐路径是:
- 评估替代方案:仅需新资源 → CRD;完整 API 语义/版本化/准入/自定义子资源 → Extension API Server。
- fork 本仓库副本(当前仓库只读,作为 staging 镜像用于同步),以
k8s.io/sample-apiserver作为 module 名引入。 - 修改类型:在
pkg/apis/<group>/types.go定义内部类型与对外版本(v1alpha1/v1beta1/...),补全 json/protobuf tag 与+genclient、+k8s:prerelease-lifecycle-gen:*等注释;需要的话实现自己的 strategy(命名空间作用域、校验、默认值、转换)与自定义准入插件。 - 重新生成代码:准备 vendor 后运行
hack/update-codegen.sh,刷新 deepcopy/conversion/clientset/informers/listers/applyconfiguration/openapi。 - 组装与部署:沿用
pkg/apiserver/apiserver.go的InstallAPIGroup模式安装你的 group;用artifacts/example/的清单模板(namespace/SA/RBAC/auth-delegator/auth-reader/Deployment/Service/APIService)接入 API Aggregation,或用 README 的 X.509 独立模式做本地联调。 - 周期性 rebase 到上游,持续跟进
k8s.io/apiserver的功能与安全修复。
总结
sample-apiserver 的价值不在于"能跑一个 wardle 服务",而在于它把 k8s.io/apiserver 的使用方法浓缩成一套可 fork、可改类型、可重新生成、可一键部署的最小工程。从 main.go 的信号上下文与 RecommendedOptions,到 apiserver.go 的 scheme/registry/storage 注册,再到 start.go 对版本兼容性与 FeatureGate 的示范,每一层都对应着你在生产级扩展 API Server 中会遇到的实际问题。无论你的目标是接入 API Aggregation 成为集群的"第二控制面",还是构建完全独立的 Kubernetes 风格 API 服务,这份样板代码连同 artifacts/ 清单、docs/minikube-walkthrough.md 演练,都是官方给出的最佳起点。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00