我们如何在 Cozystack 中为 API 聚合层构建一个动态 Kubernetes API 服务器
大家好!我是 Andrei Kvapil,您可能通过 @kvaps 在 Kubernetes 和云原生工具相关的社区中认识我。在本文中,我想分享我们如何在开源 PaaS 平台 Cozystack 中实现自己的扩展 API 服务器。
Kubernetes 强大的可扩展性特性令我惊叹。您可能已经熟悉控制器 (controller) 的概念,以及诸如 kubebuilder 和 operator-sdk 这样的框架可以帮助您实现它。简而言之,它们允许您通过定义自定义资源 (CRD) 并编写额外的控制器来处理这些资源类型的协调和管理业务逻辑,从而扩展您的 Kubernetes 集群。这种方法有详细的文档记录,网上有大量关于如何开发自己的 Operator 的信息。
然而,这并不是扩展 Kubernetes API 的唯一方法。对于更复杂的场景,例如实现命令式逻辑、管理子资源和动态生成响应,Kubernetes API 聚合层 (aggregation layer) 提供了一种有效的替代方案。通过聚合层,您可以开发一个自定义扩展 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 聚合层
API 聚合层有助于解决一些普通 CRD 机制可能不足的问题。让我们逐一分析。
命令式逻辑和子资源
除了常规资源之外,Kubernetes 还有一种叫做子资源 (subresources) 的东西。
在 Kubernetes 中,子资源是您可以通过 Kubernetes API 对主资源(如 Pod、Deployment、Service)执行的附加操作或操作。它们提供了接口来管理资源的特定方面,而无需影响整个对象。
一个简单的例子是 status
,它传统上作为单独的子资源暴露出来,您可以独立于父对象访问它。status
字段并非旨在被修改。
但除了 /status
之外,Kubernetes 中的 Pod 还有 /exec
、/portforward
和 /log
等子资源。有趣的是,与 Kubernetes 中通常的声明式资源不同,这些代表了命令式操作的端点,例如查看日志、代理连接、在运行中的容器中执行命令等等。
为了在您的 API 上支持此类命令式操作,您需要实现一个扩展 API 和一个扩展 API 服务器。这里有一些著名的例子:
- KubeVirt:一个 Kubernetes 附加组件,它扩展了 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 (Containerized Data Importer) 扩展中,它允许使用
virtctl
工具将文件从本地机器上传到 PVC,在上传过程开始前需要一个特殊令牌。这个令牌是通过 Kubernetes API 创建一个 UploadTokenRequest 资源生成的。Kubernetes 将所有 UploadTokenRequest 资源创建请求路由(代理)到 CDI 扩展 API 服务器,由它生成并返回令牌作为响应。
完全控制转换、验证和输出格式
您自己的 API 服务器可以拥有原生 Kubernetes API 服务器的所有能力。您在 API 服务器中创建的资源可以在服务器端立即进行验证,无需额外的 Webhook。虽然 CRD 也支持服务器端验证,使用 Common Expression Language (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 系统不允许您根据标签或规范中的特定字段限制对同一类型资源列表的访问。创建角色时,您只能通过在 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 服务器才能开始提供服务。Schema 更新应该动态完成,或由管理员通过 ConfigMap 提供。
双向转换
目前,我们已经有继续使用 HelmRelease 资源的集成和仪表盘。在这个阶段,我们不想失去支持这个 API 的能力。考虑到我们只是将一个资源翻译成另一个资源,支持得到了维护,并且是双向的。如果您创建一个 HelmRelease,您将在 Kubernetes 中得到一个自定义资源,如果您在 Kubernetes 中创建一个自定义资源,它也将作为一个 HelmRelease 可用。
我们没有额外的控制器来同步这些资源之间的状态。对我们扩展 API 服务器中所有资源的请求都会透明地代理到 HelmRelease,反之亦然。这消除了中间状态以及编写控制器和同步逻辑的需求。
实现
为了实现聚合 API,您可以考虑从以下项目开始:
- apiserver-builder:目前处于 Alpha 阶段,已有两年未更新。它类似于 kubebuilder,提供了一个创建扩展 API 服务器的框架,允许您按顺序创建项目结构,并为您的资源生成代码。
- sample-apiserver:一个现成的已实现的 API 服务器示例,基于官方 Kubernetes 库,您可以将其作为项目的基础。
出于实际原因,我们选择了第二个项目。这是我们需要做的事情:
禁用 etcd 支持
在我们的案例中,我们不需要它,因为所有资源都直接存储在 Kubernetes API 中。
通过将 nil 传递给 RecommendedOptions.Etcd
来禁用 etcd 选项
生成一个通用资源类型
我们将其命名为 Application,它看起来像这样:
这是一个用于任何应用类型的通用类型,它的处理逻辑对于所有 Chart 都是相同的。
配置加载
由于我们想通过配置文件配置我们的扩展 API 服务器,我们在 Go 中定义了配置结构
我们还修改了资源注册逻辑,以便我们创建的资源在 scheme 中以不同的 Kind
值注册
结果,我们得到了一个可以传递所有可能类型的配置,并指定它们应该映射到什么
实现我们自己的注册中心
为了不将状态存储在 etcd 中,而是将其直接转换为 Kubernetes HelmRelease 资源(反之亦然),我们编写了从 Application 到 HelmRelease 以及从 HelmRelease 到 Application 的转换函数
我们实现了过滤资源的逻辑,按 Chart 名称、sourceRef
和 HelmRelease 名称中的前缀
然后,使用这个逻辑,我们实现了 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 版本
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 中自行测试此 API,从 v0.18 版本开始。