我們如何在 Cozystack 中為 API 聚合層構建動態的 Kubernetes API Server

大家好!我是 Andrei Kvapil,你可能在 Kubernetes 和雲原生工具社群中認識我,我的 ID 是 @kvaps。在本文中,我想分享我們如何在開源 PaaS 平臺 Cozystack 中實現自己的擴充套件 API 伺服器。

Kubernetes 強大的可擴充套件性功能確實讓我驚歎。你可能已經熟悉 controller 的概念以及像 kubebuilderoperator-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 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 伺服器不支援與此類資源進行任何其他互動(你只能建立它們)。

    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 版本,通常是 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 伺服器的響應,以檢視是否還有剩餘資源。如果響應沒有返回,名稱空間刪除將被阻塞。

此外,你可能遇到過這種情況:當 metrics-server 不可用時,每次 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 中。

你可以透過向 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。