我们如何在 Cozystack 中为 API 聚合层构建动态的 Kubernetes API Server
大家好!我是 Andrei Kvapil,你可能在 Kubernetes 和云原生工具社区中认识我,我的 ID 是 @kvaps。在本文中,我想分享我们如何在开源 PaaS 平台 Cozystack 中实现自己的扩展 API 服务器。
Kubernetes 强大的可扩展性功能确实让我惊叹。你可能已经熟悉 controller 的概念以及像 kubebuilder 和 operator-sdk 这样的框架,它们可以帮助你实现控制器。简而言之,它们允许你通过定义自定义资源(CRD)和编写额外的控制器来扩展你的 Kubernetes 集群,这些控制器处理你的业务逻辑,以协调和管理这些类型的资源。这种方法有完善的文档,网上有大量关于如何开发自己的 Operator 的信息。
然而,这并不是扩展 Kubernetes API 的唯一方式。对于更复杂的场景,例如实现命令式逻辑、管理子资源和动态生成响应,Kubernetes API 聚合层提供了一种有效的替代方案。通过聚合层,你可以开发一个自定义的扩展 API 服务器,并将其无缝地集成到更广泛的 Kubernetes API 框架中。
在本文中,我将探讨 API 聚合层、它适合解决的挑战类型、可能不太适用的情况,以及我们如何利用这种模型在 Cozystack 中实现我们自己的扩展 API 服务器。
什么是 API 聚合层?
首先,让我们明确定义,以避免后续的混淆。API 聚合层是 Kubernetes 的一个特性,而扩展 API 服务器是聚合层 API 服务器的一种具体实现。扩展 API 服务器就像标准的 Kubernetes API 服务器一样,只不过它单独运行并处理对特定资源类型的请求。
因此,聚合层允许你编写自己的扩展 API 服务器,轻松地将其集成到 Kubernetes 中,并直接处理对特定组中资源的请求。与 CRD 机制不同,扩展 API 在 Kubernetes 中注册为一个 APIService,告诉 Kubernetes 考虑这个新的 API 服务器,并承认它为某些 API 提供服务。
你可以执行此命令来列出所有已注册的 APIService
kubectl get apiservices.apiregistration.k8s.io
APIService 示例
NAME SERVICE AVAILABLE AGE
v1alpha1.apps.cozystack.io cozy-system/cozystack-api True 7h29m
一旦 Kubernetes API 服务器收到对 v1alpha1.apps.cozystack.io
组中资源的请求,它就会将所有这些请求重定向到我们的扩展 API 服务器,该服务器可以根据我们内置的业务逻辑来处理它们。
何时使用 API 聚合层
API 聚合层有助于解决一些常规 CRD 机制可能不足以应对的问题。让我们来分析一下。
命令式逻辑和子资源
除了常规资源,Kubernetes 还有一种叫做子资源的东西。
在 Kubernetes 中,子资源是你可以通过 Kubernetes API 对主要资源(如 Pod、Deployment、Service)执行的附加操作。它们提供了管理资源特定方面的接口,而不会影响整个对象。
一个简单的例子是 status
,它传统上作为一个单独的子资源暴露出来,你可以独立于父对象访问它。status
字段不应该被更改。
但除了 /status
之外,Kubernetes 中的 Pod 还具有诸如 /exec
、/portforward
和 /log
等子资源。有趣的是,与 Kubernetes 中通常的声明式资源不同,这些代表了命令式操作的端点,例如查看日志、代理连接、在正在运行的容器中执行命令等等。
为了在你自己的 API 上支持此类命令式命令,你需要实现一个扩展 API 和一个扩展 API 服务器。以下是一些著名的例子:
- KubeVirt:一个 Kubernetes 的插件,扩展其 API 功能以运行传统的虚拟机。作为 KubeVirt 一部分创建的扩展 API 服务器处理虚拟机的
/restart
、/console
和/vnc
等子资源。 - Knative:一个 Kubernetes 插件,扩展其无服务器计算的能力,实现
/scale
子资源为其资源类型设置自动扩缩容。
顺便说一下,尽管 Kubernetes 中的子资源逻辑可以是命令式的,但你可以使用 Kubernetes 标准的 RBAC 模型来声明式地管理对它们的访问。
例如,你可以通过这种方式控制对 Pod 类型的 /log
和 /exec
子资源的访问
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
namespace: default
name: pod-and-pod-logs-reader
rules:
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]
你不必局限于使用 etcd
通常,Kubernetes API 服务器使用 etcd 作为其后端。然而,实现自己的 API 服务器并不意味着你只能使用 etcd。如果将服务器的状态存储在 etcd 中没有意义,你可以将信息存储在任何其他系统中,并动态生成响应。以下是一些案例来说明:
metrics-server 是 Kubernetes 的一个标准扩展,允许你查看节点和 Pod 的实时指标。它在自己的 metrics.k8s.io API 中定义了替代的 Pod 和 Node 类型。对这些资源的请求会直接从 Kubelet 转换为指标。因此,当你运行
kubectl top node
或kubectl top pod
时,metrics-server 会实时从 cAdvisor 获取指标,然后将这些指标返回给你。由于信息是实时生成的,并且仅在请求时相关,因此无需将其存储在 etcd 中。这种方法节省了资源。如果需要,你可以使用 etcd 以外的后端。你甚至可以为其实现一个与 Kubernetes 兼容的 API。例如,如果你使用 Postgres,你可以在 Kubernetes API 中为其创建实体的透明表示。例如,Postgres 中的数据库、用户和授权将显示为常规的 Kubernetes 资源,这要归功于你的扩展 API 服务器。你可以使用
kubectl
或任何其他与 Kubernetes 兼容的工具来管理它们。与控制器不同,控制器使用自定义资源和协调方法实现业务逻辑,而扩展 API 服务器则无需为每种类型都创建单独的控制器。这意味着你不必在 Kubernetes API 和后端之间同步状态。
一次性资源
Kubernetes 有一个特殊的 API,用于向用户提供有关其权限的信息。这是通过 SelfSubjectAccessReview API 实现的。这些资源的一个不寻常的细节是,你不能使用 get 或 list 动词查看它们。你只能创建它们(使用 create 动词),并接收一个包含你当前有权访问的内容信息的输出。
如果你直接尝试运行
kubectl get selfsubjectaccessreviews
,你会得到类似这样的错误:Error from server (MethodNotAllowed): the server does not allow this method on the requested resource
原因在于 Kubernetes API 服务器不支持与此类资源进行任何其他交互(你只能创建它们)。
SelfSubjectAccessReview API 支持诸如以下的命令:
kubectl auth can-i create deployments --namespace dev
当你运行上述命令时,
kubectl
使用 Kubernetes API 创建一个 SelfSubjectAccessReview。这使得 Kubernetes 能够获取你用户的可能权限列表。然后,Kubernetes 会实时生成对你请求的个性化响应。这种逻辑与将此资源简单地存储在 etcd 中的情况不同。类似地,在 KubeVirt 的 CDI(容器化数据导入器) 扩展中,该扩展允许使用
virtctl
工具从本地机器上传文件到 PVC,在上传过程开始前需要一个特殊的令牌。这个令牌是通过 Kubernetes API 创建一个 UploadTokenRequest 资源来生成的。Kubernetes 将所有 UploadTokenRequest 资源创建请求路由(代理)到 CDI 扩展 API 服务器,该服务器会生成并返回令牌作为响应。
对转换、验证和输出格式的完全控制
你自己的 API 服务器可以拥有原生 Kubernetes API 服务器的所有功能。你在 API 服务器中创建的资源可以在服务器端立即得到验证,而无需额外的 webhook。虽然 CRD 也支持使用通用表达式语言 (CEL) 进行声明式验证和ValidatingAdmissionPolicies 来实现服务器端验证,无需 webhook,但自定义 API 服务器可以在需要时实现更复杂和定制化的验证逻辑。
Kubernetes 允许你为每种资源类型提供多个 API 版本,通常是
v1alpha1
、v1beta1
和v1
。只有一个版本可以被指定为存储版本。所有对其他版本的请求都必须自动转换到被指定为存储版本的版本。对于 CRD,这个机制是通过转换 webhook 实现的。而在扩展 API 服务器中,你可以实现自己的转换机制,选择混合使用不同的存储版本(一个对象可能序列化为v1
,另一个为v2
),或者依赖于外部的后端 API。直接实现 Kubernetes API 让你能够随心所欲地格式化表格输出,而不必遵循 CRD 中的
additionalPrinterColumns
逻辑。相反,你可以编写自己的格式化程序来格式化表格输出和其中的自定义字段。例如,使用additionalPrinterColumns
时,你只能按照 JSONPath 逻辑显示字段值。而在你自己的 API 服务器中,你可以动态生成和插入值,随心所欲地格式化表格输出。
动态资源注册
- 由扩展 API 服务器提供的资源不需要预先注册为 CRD。一旦你的扩展 API 服务器通过 APIService 注册,Kubernetes 就会开始轮询它以发现它可以提供的 API 和资源。在收到发现响应后,Kubernetes API 服务器会自动为该 API 组注册所有可用的类型。尽管这不被认为是常见做法,但你可以实现动态注册 Kubernetes 集群中所需资源类型的逻辑。
何时不应使用 API 聚合层
在某些反模式下,不建议使用 API 聚合层。让我们来看看它们。
不稳定的后端
如果你的 API 服务器由于后端不可用或其他问题而停止响应,可能会阻塞某些 Kubernetes 功能。例如,在删除命名空间时,Kubernetes 会等待你的 API 服务器的响应,以查看是否还有剩余资源。如果响应没有返回,命名空间删除将被阻塞。
此外,你可能遇到过这种情况:当 metrics-server 不可用时,每次 API 请求后(即使与指标无关),stderr 中都会出现一条额外信息,说明 metrics.k8s.io
不可用。这是另一个例子,说明当处理请求的 API 服务器不可用时,使用 API 聚合层可能导致问题。
慢速请求
如果你无法保证对用户请求的即时响应,最好考虑使用 CustomResourceDefinition 和控制器。否则,你可能会使集群的稳定性降低。许多项目仅为有限的一组资源实现扩展 API 服务器,特别是用于命令式逻辑和子资源。这一建议在官方 Kubernetes 文档中也有提及。
为什么我们在 Cozystack 中需要它
提醒一下,我们正在开发开源 PaaS 平台 Cozystack,它也可以用作构建你自己的私有云的框架。因此,能够轻松扩展平台对我们至关重要。
Cozystack 是基于 FluxCD 构建的。任何应用程序都被打包到其自己的 Helm Chart 中,准备好部署到租户命名空间中。在平台上部署任何应用程序都是通过创建一个 HelmRelease 资源来完成的,指定 Chart 名称和应用程序的参数。所有其余的逻辑都由 FluxCD 处理。这种模式使我们能够轻松地用新的应用程序扩展平台,并提供了创建新应用程序的能力,只需将它们打包成相应的 Helm Chart。

Cozystack 平台界面
因此,在我们的平台中,一切都配置为 HelmRelease 资源。然而,我们遇到了两个问题:RBAC 模型的限制和对公共 API 的需求。让我们深入探讨这些问题。
RBAC 模型的限制
Kubernetes 中广泛部署的 RBAC 系统不允许你根据标签或 spec 中的特定字段来限制对同一类型资源列表的访问。在创建角色时,你只能通过在 resourceNames
中指定特定的资源名称来限制对同类资源的访问。对于像 get 或 update 这样的动词,这会起作用。然而,使用 list 动词按 resourceNames
过滤并不奏效。因此,你可以限制列出特定类型的某些资源,但不能按名称限制。
- Kubernetes 有一个特殊的 API,用于向用户提供有关其权限的信息。这是通过 SelfSubjectAccessReview API 实现的。这些资源的一个不寻常的细节是,你不能使用 get 或 list 动词查看它们。你只能创建它们(使用 create 动词),并接收一个包含你当前有权访问的内容信息的输出。
因此,我们决定根据它们使用的 Helm Chart 的名称引入新的资源类型,并在我们的扩展 API 服务器中在运行时动态生成可用类型的列表。这样,我们就可以重用 Kubernetes 标准的 RBAC 模型来管理对特定资源类型的访问。
对公共 API 的需求
由于我们的平台提供了部署各种托管服务的能力,我们希望组织对平台 API 的公共访问。然而,我们不能允许用户直接与 HelmRelease 这样的资源交互,因为这会让他们为要部署的 Helm Chart 指定任意的名称和参数,从而可能危及我们的系统。
我们希望让用户能够通过在 Kubernetes 中创建相应类型的资源来简单地部署特定服务。此资源的类型应与其部署所用的 Chart 同名。以下是一些例子:
kind: Kubernetes
→chart: kubernetes
kind: Postgres
→chart: postgres
kind: Redis
→chart: redis
kind: VirtualMachine
→chart: virtual-machine
此外,我们不希望每次添加一个新的 Chart 时,都必须向代码生成器添加一个新类型并重新编译我们的扩展 API 服务器才能使其开始提供服务。模式更新应该动态完成,或者由管理员通过 ConfigMap 提供。
双向转换
目前,我们已经有了继续使用 HelmRelease 资源的集成和仪表盘。在这个阶段,我们不想失去支持这个 API 的能力。考虑到我们只是将一种资源转换成另一种,支持得以维持并且双向有效。如果你创建一个 HelmRelease,你将在 Kubernetes 中得到一个自定义资源;如果你在 Kubernetes 中创建一个自定义资源,它也将作为一个 HelmRelease 可用。
我们没有任何额外的控制器来同步这些资源之间的状态。所有对我们扩展 API 服务器中资源的请求都透明地代理到 HelmRelease,反之亦然。这消除了中间状态,也无需编写控制器和同步逻辑。
实现
要实现聚合 API,你可能会考虑从以下项目入手:
- apiserver-builder:目前处于 alpha 阶段,已有两年未更新。它的工作方式类似 kubebuilder,提供了一个用于创建扩展 API 服务器的框架,允许你按顺序创建项目结构并为你的资源生成代码。
- sample-apiserver:一个已实现的 API 服务器的现成示例,基于官方 Kubernetes 库,你可以将其用作项目的基础。
出于实际考虑,我们选择了第二个项目。以下是我们需要做的事情:
禁用 etcd 支持
在我们的情况下,我们不需要它,因为所有资源都直接存储在 Kubernetes API 中。
你可以通过向 RecommendedOptions.Etcd
传递 nil 来禁用 etcd 选项。
生成一个通用的资源类型
我们称之为 Application,它看起来像这样:
这是一个用于任何应用程序类型的通用类型,其处理逻辑对所有 Chart 都是相同的。
配置配置加载
由于我们希望通过配置文件来配置我们的扩展 API 服务器,我们在 Go 中定义了配置结构:
我们还修改了资源注册逻辑,以便我们创建的资源以不同的 Kind
值注册到 scheme 中。
最终,我们得到了一个配置,你可以在其中传递所有可能的类型并指定它们应该映射到什么。
实现我们自己的注册表
为了不将状态存储在 etcd 中,而是直接将其转换为 Kubernetes HelmRelease 资源(反之亦然),我们编写了从 Application 到 HelmRelease 以及从 HelmRelease 到 Application 的转换函数。
我们实现了按 HelmRelease 名称中的 Chart 名称、sourceRef
和前缀来过滤资源的逻辑。
然后,我们使用这个逻辑实现了 Get()
、Delete()
、List()
和 Create()
方法。
你可以在这里看到完整的示例:
在每个方法的末尾,我们设置了正确的 Kind
并返回一个 unstructured.Unstructured{}
对象,以便 Kubernetes 正确序列化该对象。否则,它会总是将它们序列化为 kind: Application
,这不是我们想要的。
我们实现了什么?
在 Cozystack 中,我们 ConfigMap 中的所有类型现在都可以在 Kubernetes 中直接使用。
kubectl api-resources | grep cozystack
buckets apps.cozystack.io/v1alpha1 true Bucket
clickhouses apps.cozystack.io/v1alpha1 true ClickHouse
etcds apps.cozystack.io/v1alpha1 true Etcd
ferretdb apps.cozystack.io/v1alpha1 true FerretDB
httpcaches apps.cozystack.io/v1alpha1 true HTTPCache
ingresses apps.cozystack.io/v1alpha1 true Ingress
kafkas apps.cozystack.io/v1alpha1 true Kafka
kuberneteses apps.cozystack.io/v1alpha1 true Kubernetes
monitorings apps.cozystack.io/v1alpha1 true Monitoring
mysqls apps.cozystack.io/v1alpha1 true MySQL
natses apps.cozystack.io/v1alpha1 true NATS
postgreses apps.cozystack.io/v1alpha1 true Postgres
rabbitmqs apps.cozystack.io/v1alpha1 true RabbitMQ
redises apps.cozystack.io/v1alpha1 true Redis
seaweedfses apps.cozystack.io/v1alpha1 true SeaweedFS
tcpbalancers apps.cozystack.io/v1alpha1 true TCPBalancer
tenants apps.cozystack.io/v1alpha1 true Tenant
virtualmachines apps.cozystack.io/v1alpha1 true VirtualMachine
vmdisks apps.cozystack.io/v1alpha1 true VMDisk
vminstances apps.cozystack.io/v1alpha1 true VMInstance
vpns apps.cozystack.io/v1alpha1 true VPN
我们可以像处理常规 Kubernetes 资源一样使用它们。
列出 S3 存储桶
kubectl get buckets.apps.cozystack.io -n tenant-kvaps
输出示例
NAME READY AGE VERSION
foo True 22h 0.1.0
testaasd True 27h 0.1.0
列出 Kubernetes 集群
kubectl get kuberneteses.apps.cozystack.io -n tenant-kvaps
输出示例
NAME READY AGE VERSION
abc False 19h 0.14.0
asdte True 22h 0.13.0
列出虚拟机磁盘
kubectl get vmdisks.apps.cozystack.io -n tenant-kvaps
输出示例
NAME READY AGE VERSION
docker True 21d 0.1.0
test True 18d 0.1.0
win2k25-iso True 21d 0.1.0
win2k25-system True 21d 0.1.0
列出虚拟机实例
kubectl get vminstances.apps.cozystack.io -n tenant-kvaps
输出示例
NAME READY AGE VERSION
docker True 21d 0.1.0
test True 18d 0.1.0
win2k25 True 20d 0.1.0
我们可以创建、修改和删除它们中的每一个,任何与它们的交互都将被转换为 HelmRelease 资源,同时还会应用资源结构和名称中的前缀。
查看所有相关的 Helm Release
kubectl get helmreleases -n tenant-kvaps -l cozystack.io/ui
输出示例
NAME AGE READY
bucket-foo 22h True
bucket-testaasd 27h True
kubernetes-abc 19h False
kubernetes-asdte 22h True
redis-test 18d True
redis-yttt 12d True
vm-disk-docker 21d True
vm-disk-test 18d True
vm-disk-win2k25-iso 21d True
vm-disk-win2k25-system 21d True
vm-instance-docker 21d True
vm-instance-test 18d True
vm-instance-win2k25 20d True
后续步骤
我们不打算就此止步于我们的 API。未来,我们计划添加新功能:
- 根据直接从 Helm Chart 生成的 OpenAPI 规范添加验证。
- 开发一个控制器,收集已部署版本的发布说明,并向用户显示特定服务的访问信息。
- 改造我们的仪表盘,使其直接与新的 API 协同工作。
结论
API 聚合层使我们能够快速有效地解决问题,它提供了一种灵活的机制,通过动态注册资源并即时转换它们来扩展 Kubernetes API。最终,这使得我们的平台更加灵活和可扩展,而无需为每个新资源编写代码。
你可以在开源 PaaS 平台 Cozystack 的 v0.18 版本及以后版本中亲自测试这个 API。