Cozystack 中 API 聚合層的動態 Kubernetes API 伺服器建置方式

嗨,大家好!我是 Andrei Kvapil,但在專注於 Kubernetes 和雲原生工具的社群中,您可能更認識我的網名 @kvaps。在本文中,我想分享我們如何在開源 PaaS 平台 Cozystack 中實作我們自己的擴充 api-server。

Kubernetes 強大的擴充性功能真的讓我驚艷。您可能已經熟悉控制器 (controller) 概念,以及像是 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 中建立其實體的透明表示。例如,由於您的擴充 API 伺服器,Postgres 中的資料庫、使用者和授權將顯示為常規 Kubernetes 資源。您可以使用 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 (Containerized Data Importer) 擴充功能中,它允許使用 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-server 服務的資源不需要預先註冊為 CRD。一旦您的擴充 API 伺服器使用 APIService 註冊,Kubernetes 就會開始輪詢它,以發現它可以服務的 API 和資源。收到探索回應後,Kubernetes API 伺服器會自動註冊此 API 群組的所有可用類型。雖然這不被認為是常見的做法,但您可以實作邏輯來動態註冊 Kubernetes 叢集中所需的資源類型。

何時不使用 API 聚合層

有些反模式不建議使用 API 聚合層。讓我們來看看它們。

不穩定的後端

如果您的 API 伺服器由於後端不可用或其他問題而停止回應,可能會封鎖某些 Kubernetes 功能。例如,在刪除命名空間時,Kubernetes 將等待您的 API 伺服器的回應,以查看是否還有任何剩餘資源。如果沒有收到回應,命名空間刪除將會被封鎖。

此外,您可能還遇到過 這樣的情況:當 metrics-server 不可用時,在每個 API 請求(甚至與指標無關)之後,stderr 中都會出現一條額外的訊息,指出 metrics.k8s.io 不可用。這是另一個範例,說明當處理請求的 api-server 不可用時,使用 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-server 中於運行時動態產生可用種類的清單。這樣,我們可以重複使用 Kubernetes 標準 RBAC 模型來管理對特定資源類型的存取。

對公用 API 的需求

由於我們的平台提供部署各種託管服務的功能,我們希望組織對平台 API 的公開存取。但是,我們不能允許使用者直接與 HelmRelease 等資源互動,因為這會讓他們為要部署的 Helm chart 指定任意名稱和參數,從而可能危及我們的系統。

我們希望讓使用者能夠透過簡單地在 Kubernetes 中建立具有相應種類的資源來部署特定服務。此資源的類型應與部署它的 chart 名稱相同。以下是一些範例

  • kind: Kuberneteschart: kubernetes
  • kind: Postgreschart: postgres
  • kind: Redischart: redis
  • kind: VirtualMachinechart: virtual-machine

此外,我們不希望每次新增一個新的 chart 以便開始服務時,都必須在 codegen 中新增一個新的類型並重新編譯我們的擴充 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-server,因此我們在 Go 中形成了配置結構

我們還修改了資源註冊邏輯,以便我們建立的資源以不同的 Kind 值在架構中註冊

因此,我們獲得了一個配置,您可以在其中傳遞所有可能的類型並指定它們應對應到什麼

實作我們自己的註冊表

為了不將狀態儲存在 etcd 中,而是直接將其轉換為 Kubernetes HelmRelease 資源(反之亦然),我們編寫了從 Application 到 HelmRelease 以及從 HelmRelease 到 Application 的轉換函數

我們實作了邏輯,以依 chart 名稱、sourceRef 和 HelmRelease 名稱中的前綴來篩選資源

然後,使用此邏輯,我們實作了方法 Get()Delete()List()Create()

您可以在此處查看完整範例

在每個方法的結尾,我們都設定了正確的 Kind 並傳回一個 unstructured.Unstructured{} 物件,以便 Kubernetes 正確地序列化物件。否則,它始終會使用種類: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 開始。