我们如何在 Cozystack 中为 API 聚合层构建动态 Kubernetes API 服务器

大家好!我是 Andrei Kvapil,但您可能在致力于 Kubernetes 和云原生工具的社区中认识我为 @kvaps。在本文中,我想分享我们如何在开源 PaaS 平台 Cozystack 中实现我们自己的扩展 api-server。

Kubernetes 强大的可扩展性功能真的让我惊叹。您可能已经熟悉 控制器 概念和诸如 kubebuilderoperator-sdk 之类的框架,它们可以帮助您实现它。简而言之,它们允许您通过定义自定义资源(CRD)并编写额外的控制器来处理业务逻辑,以协调和管理这些类型的资源来扩展 Kubernetes 集群。这种方法有详细的文档记录,网上有大量关于如何开发自己的 Operator 的信息。

但是,这并不是 扩展 Kubernetes API 的唯一方法。对于更复杂的场景,例如实现命令式逻辑、管理子资源和动态生成响应,Kubernetes API 的聚合层提供了一个有效的替代方案。通过聚合层,您可以开发自定义的扩展 API 服务器,并将其无缝集成到更广泛的 Kubernetes API 框架中。

在本文中,我将探讨 API 聚合层、它适合解决的挑战类型、可能不太适用的情况,以及我们如何利用此模型在 Cozystack 中实现我们自己的扩展 API 服务器。

什么是 API 聚合层?

首先,让我们理清定义,以避免后续出现任何混淆。API 聚合层是 Kubernetes 中的一项功能,而扩展 api-server 是聚合层的 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-server 收到对组 v1alpha1.apps.cozystack.io 中资源的请求,它会将所有这些请求重定向到我们的扩展 api-server,该服务器可以根据我们构建到其中的业务逻辑来处理它们。

何时使用 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-server 处理虚拟机的 /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 nodekubectl top pod 时,metrics-server 会从 cAdvisor 中实时获取指标。然后,它将这些指标返回给您。由于信息是实时生成的,并且仅在请求时相关,因此无需将其存储在 etcd 中。这种方法节省了资源。

  • 如果需要,您可以使用 etcd 以外的后端。您甚至可以为其实现与 Kubernetes 兼容的 API。例如,如果您使用 Postgres,您可以在 Kubernetes API 中透明地表示其实体。例如。Postgres 中的数据库、用户和授权将作为常规的 Kubernetes 资源出现,这要归功于您的扩展 API 服务器。您可以使用 kubectl 或任何其他与 Kubernetes 兼容的工具来管理它们。与使用自定义资源和协调方法实现业务逻辑的控制器不同,扩展 API 服务器消除了对每种类型使用单独控制器的需要。这意味着您不必在 Kubernetes API 和后端之间同步状态。

一次性资源

  • Kubernetes 有一个特殊的 API,用于向用户提供有关其权限的信息。这是使用 SelfSubjectAccessReview API 实现的。这些资源的一个不寻常的细节是,您无法使用 getlist 动词来查看它们。您只能创建它们(使用 create 动词)并接收有关您当时可以访问的内容的信息的输出。

    如果您尝试直接运行 kubectl get selfsubjectaccessreviews,您只会收到如下错误

    Error from server (MethodNotAllowed): the server does not allow this method on the requested resource
    

    原因是 Kubernetes API 服务器不支持与此类型资源的任何其他交互(您只能 CREATE 它们)。

    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 服务器可以拥有 Vanilla Kubernetes API 服务器的所有功能。您在 API 服务器中创建的资源可以在服务器端立即进行验证,而无需额外的 Webhook。虽然 CRD 也支持使用 通用表达式语言 (CEL) 进行声明式验证和使用 ValidatingAdmissionPolicies 而无需 Webhook 的服务器端验证,但如果需要,自定义 API 服务器允许更复杂和定制的验证逻辑。

    Kubernetes 允许您为每个资源类型提供多个 API 版本,通常是 v1alpha1v1beta1v1。只能指定一个版本作为存储版本。对其他所有版本的请求都必须自动转换为指定为存储版本的版本。对于 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 服务器的响应,以查看是否还有任何剩余资源。如果响应没有到来,命名空间删除将被阻止。

此外,您可能还遇到过 一种情况,即当指标服务器不可用时,每个 API 请求(甚至与指标无关)之后都会在 stderr 中出现一条额外的消息,指出 metrics.k8s.io 不可用。这是另一个示例,说明当处理请求的 API 服务器不可用时,使用 API 聚合层可能会导致问题。

慢速请求

如果您无法保证用户请求的即时响应,最好考虑使用 CustomResourceDefinition 和控制器。否则,您可能会使集群的稳定性降低。许多项目仅为有限的一组资源(特别是对于命令式逻辑和子资源)实现扩展 API 服务器。官方 Kubernetes 文档中也 提到了 此建议。

为什么我们在 Cozystack 中需要它

提醒一下,我们正在开发开源 PaaS 平台 Cozystack,它也可以用作构建您自己的私有云的框架。因此,轻松扩展平台的能力对我们至关重要。

Cozystack 构建在 FluxCD 之上。任何应用程序都打包到自己的 Helm chart 中,准备在租户命名空间中部署。在平台上部署任何应用程序都是通过创建 HelmRelease 资源,指定应用程序的 chart 名称和参数来完成的。其余逻辑由 FluxCD 处理。此模式允许我们轻松地使用新应用程序扩展平台,并提供创建只需打包到适当的 Helm chart 中的新应用程序的能力。

Interface of the Cozystack platform

Cozystack 平台的界面

因此,在我们的平台中,一切都配置为 HelmRelease 资源。但是,我们遇到了两个问题:RBAC 模型的限制和对公共 API 的需求。让我们深入研究一下这些问题

RBAC 模型的限制

Kubernetes 中广泛部署的 RBAC 系统不允许您根据标签或 spec 中的特定字段来限制对同一类资源列表的访问。创建角色时,您只能通过在 resourceNames 中指定特定的资源名称来限制对同类资源中资源的访问。对于 getupdate 等动词,这将起作用。但是,使用 list 动词按 resourceNames 过滤不起作用。因此,您可以限制按类型列出某些资源,但不能按名称列出。

  • Kubernetes 有一个特殊的 API,用于向用户提供有关其权限的信息。这是使用 SelfSubjectAccessReview API 实现的。这些资源的一个不寻常的细节是,您无法使用 getlist 动词来查看它们。您只能创建它们(使用 create 动词)并接收有关您当时可以访问的内容的信息的输出。

因此,我们决定基于它们使用的 Helm chart 的名称引入新的资源类型,并在我们的扩展 API 服务器中运行时动态生成可用类型的列表。这样,我们可以重用 Kubernetes 标准 RBAC 模型来管理对特定资源类型的访问。

对公共 API 的需求

由于我们的平台提供部署各种托管服务的功能,我们希望组织对平台 API 的公共访问。但是,我们不能允许用户直接与 HelmRelease 等资源交互,因为这将允许他们为要部署的 Helm chart 指定任意名称和参数,这可能会危及我们的系统。

我们希望让用户能够简单地通过在 Kubernetes 中创建具有相应类型的资源来部署特定服务。此资源的类型应与部署它的 chart 的名称相同。以下是一些示例

  • kind: Kuberneteschart: kubernetes
  • kind: Postgreschart: postgres
  • kind: Redischart: redis
  • kind: VirtualMachinechart: 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 中。

您可以通过将 nil 传递给 RecommendedOptions.Etcd 来禁用 etcd 选项

生成通用资源类型

我们将其称为 Application,它看起来像这样

这是用于任何应用程序类型的通用类型,其处理逻辑对于所有 chart 都是相同的。

配置配置加载

由于我们想通过配置文件配置我们的扩展 API 服务器,我们在 Go 中形成了配置结构

我们还修改了资源注册逻辑,以便我们在方案中创建的资源以不同的 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 并动态注册资源并动态转换它们,使我们能够快速有效地解决问题。最终,这使我们的平台更加灵活和可扩展,而无需为每个新资源编写代码。

您可以从 v0.18 版本 开始,在开源 PaaS 平台 Cozystack 中自行测试该 API。