Aggregated APIServer 構建雲原生應用最佳實踐

作者

張鵬,騰訊雲容器產品工程師,擁有多年雲原生項目開發落地經驗。目前主要負責騰訊雲 TKE 雲原生 AI 產品的開發工作。

謝遠東,騰訊高級工程師,Kubeflow Member、Fluid(CNCF Sandbox) 核心開發者,負責騰訊雲 TKE 在 AI 場景的研發和支援工作。

概述

隨著 Kubernetes 的日趨成熟,越來越多的公司、企業開始使用 K8s 來構建自己的雲原生平台,基於 kubernetes 良好的擴展性以及成熟穩定的架構,你可以快速部署並管理自己的雲原生應用。

目前我們也在基於 kubernetes 打造一個雲原生 AI 平台(我們稱它為:SKAI),該平台具備極致彈性多雲兼容性高易用可觀測性可復現性的特點,旨在利用雲原生的思想和技術,為 AI 場景的數據處理、模型訓練、模型上線推理等需求構建彈性可擴展的系統架構,從而提升資源利用率。

為了使我們的平台更加的雲原生,我們沒有選擇常用的 web 框架來構建 API 服務,而是使用 kubernetes 擴展來構建整個平台,這樣使我們的平台能更好的和 kubernetes 融合,可以無縫適配任何基於 k8s 的多雲混合雲環境。

為什麼選擇 Aggregated APIServer?

選擇獨立 API 還是 Aggregated APIServer ?

儘管使用 gin、go-restful 等 go 語言 web 框架可以輕易地構建出一個穩定的 API 介面服務,但以 kubernetes 原生的方式構建 API 介面服務還是有很多優勢,例如:

  • 能利用 kubernetes 原生的認證、授權、准入等機制,有更高的開發效率;
  • 能更好的和 K8s 系統融合,藉助 K8s 生態更快的推廣自己的產品,方便用戶上手;
  • 藉助於 K8s 成熟的 API 工具及規範,構建出的 API 介面更加規範整齊;

但是在很多場景下,我們還是不能確定到底使用聚合 API(Aggregated APIServer)還是獨立 API 來構建我們的服務,官方為我們提供了兩種選擇的對比;如果你不能確定使用聚合 API 還是獨立 API,下面的表格或許對你有幫助:

考慮 API 聚合的情況 優選獨立 API 的情況
你在開發新的 API 你已經有一個提供 API 服務的程式並且工作良好
你希望可以是使用 kubectl 來讀寫你的新資源類別 不要求 kubectl 支援
你希望在 Kubernetes UI (如儀錶板)中和其他內置類別一起查看你的新資源類別 不需要 Kubernetes UI 支援
你希望復用 Kubernetes API 支援特性 你不需要這類特性
你有意願取接受 Kubernetes 對 REST 資源路徑所作的格式限制,例如 API 組和名字空間。(參閱 API 概述 你需要使用一些特殊的 REST 路徑以便與已經定義的 REST API 保持兼容
你的 API 是聲明式的 你的 API 不符合聲明式模型
你的資源可以自然地界定為集群作用域或集群中某個名字空間作用域 集群作用域或名字空間作用域這種二分法很不合適;你需要對資源路徑的細節進行控制

首先我們希望我們的 SKAI 平台能更好的和 k8s 結合,並且它是一個聲明式的 API,儘可能的復用 Kubernets API 的特性,顯然聚合 API 對我們來說更加適合。

選擇 CRDs 還是 Aggregated APIServer?

除了聚合 API,官方還提供了另一種方式以實現對標準 kubernetes API 介面的擴展:CRD(Custom Resource Definition ),能達到與聚合 API 基本一樣的功能,而且更加易用,開發成本更小,但相較而言聚合 API 則更為靈活。針對這兩種擴展方式如何選擇,官方也提供了相應的參考。

通常,如果存在以下情況,CRD 可能更合適:

  • 訂製資源的欄位不多;
  • 你在組織內部使用該資源或者在一個小規模的開源項目中使用該資源,而不是在商業產品中使用;
    聚合 API 可提供更多的高級 API 特性,也可對其他特性進行訂製;例如,對存儲層進行訂製、對 protobuf 協議支援、對 logs、patch 等操作支援。

兩種方式的核心區別是定義 api-resource 的方式不同。在 Aggregated APIServer 方式中,api-resource 是通過程式碼向 API 註冊資源類型,而 Custom Resource 是直接通過 yaml 文件向 API 註冊資源類型。

簡單來說就是 CRD 是讓 kube-apiserver 認識更多的對象類別(Kind),Aggregated APIServer 是構建自己的 APIServer 服務。雖然 CRD 更簡單,但是缺少更多的靈活性,更詳細的 CRDs 與 Aggregated API 的對比可參考官方文檔

對於我們而言,我們希望使用更多的高級 API 特性,例如 “logs” 或 “exec”,而不僅僅局限於 CRUD ,所以我們最終選擇了 Aggregated APIServer 。

APIServer 擴展的基本原理

kube-apiserver 作為整個 Kubernetes 集群操作 etcd 的唯一入口,負責 Kubernetes 各資源的認證&鑒權,校驗以及 CRUD 等操作,提供 RESTful APIs,供其它組件調用:

kube-apiserver 其實包含三種 APIServer:

  • AggregatorServer:負責處理 apiregistration.k8s.io 組下的 APIService 資源請求,同時將來自用戶的請求攔截轉發給 Aggregated APIServer(AA);
  • KubeAPIServer:負責對請求的一些通用處理,包括:認證、鑒權以及各個內建資源(pod, deployment,service)的 REST 服務等;
  • ApiExtensionsServer:負責 CustomResourceDefinition(CRD)apiResources 以及 apiVersions 的註冊,同時處理 CRD 以及相應 CustomResource(CR)的REST請求(如果對應 CR 不能被處理的話則會返回404),也是 apiserver Delegation 的最後一環;

三個 APIServer 通過 delegation 的關係關聯,在 kube-apiserver 初始化創建的過程中,首先創建的是 APIExtensionsServer,它的 delegationTarget 是一個空的 Delegate,即什麼都不做,繼而將 APIExtensionsServer 的 GenericAPIServer,作為 delegationTarget 傳給了 KubeAPIServer,創建出了 KubeAPIServer,再然後,將 kubeAPIServer 的 GenericAPIServer 作為 delegationTarget 傳給了 AggregatorServer,創建出了 AggregatorServer,所以他們之間 delegation 的關係為: Aggregator -> KubeAPIServer -> APIExtensions,如下圖所示:

如何快速構建 Aggregated APIServer?

雖然官方提供了一個 sample-apiserver,我們可以參考實現自己的 Aggregated APIServer。但完全手工編寫太過複雜,也不便於後期維護,我們最終選擇了官方推薦的工具 apiserver-builder,apiserver-builder 可以幫助我們快速創建項目骨架,並且使用 apiserver-builder 構建的項目目錄結構比較清晰,更利於後期維護。

安裝 apiserver-builder 工具

通過 Go Get 安裝

$ GO111MODULE=on go get sigs.k8s.io/apiserver-builder-alpha/cmd/apiserver-boot

通過安裝包安裝

  • 下載最新版本
  • 解壓到 /usr/local/apiserver-builder/
  • 如果此目錄不存在,則創建此目錄
  • 添加/usr/local/apiserver-builder/bin到您的路徑 export PATH=$PATH:/usr/local/apiserver-builder/bin
  • 運行apiserver-boot -h

初始化項目

完成 apiserver-boot 安裝後,可通過如下命令來初始化一個 Aggregated APIServer 項目:

$ mkdir skai-demo
$ cd skai-demo 
$ apiserver-boot init repo --domain skai.io

執行後會生成如下目錄:

.
├── BUILD.bazel
├── Dockerfile
├── Makefile
├── PROJECT
├── WORKSPACE
├── bin
├── cmd
│   ├── apiserver
│   │   └── main.go
│   └── manager
│       └── main.go -> ../../main.go
├── go.mod
├── hack
│   └── boilerplate.go.txt
├── main.go
└── pkg
    └── apis
        └── doc.go
  • hack 目錄存放自動腳本
  • cmd/apiserver 是 aggregated server的啟動入口
  • cmd/manager 是 controller 的啟動入口
  • pkg/apis 存放 CR 相關的結構體定義,會在下一步自動生成

生成自定義資源

$ apiserver-boot create group version resource --group animal --version v1alpha1 --kind Cat --non-namespaced=false
Create Resource [y/n]
y
Create Controller [y/n]
n

可根據自己的需求選擇是否生成 Controller,我們這裡暫時選擇不生成, 對於需要通過 namespace 隔離的 resource 需要增加 –non-namespaced=false 的參數,默認都是 true。

執行完成後程式碼結構如下:

└── pkg
    └── apis
        ├── animal
        │   ├── doc.go
        │   └── v1alpha1
        │       ├── cat_types.go
        │       ├── doc.go
        │       └── register.go
        └── doc.go

可以看到在 pkg/apis 下生成了 animal 的 group 並在 v1alpha1 版本下新增了 cat_types.go 文件,此文件包含了我們資源的基礎定義,我們在 spec 中增加欄位定義,並在已經實現的 Validate 方法中完成基礎欄位的校驗。

// Cat
// +k8s:openapi-gen=true
type Cat struct {
        metav1.TypeMeta   `json:",inline"`
        metav1.ObjectMeta `json:"metadata,omitempty"`
        Spec   CatSpec   `json:"spec,omitempty"`
        Status CatStatus `json:"status,omitempty"`
}
// CatSpec defines the desired state of Cat
type CatSpec struct {
    Name string `json:"name"`
}
func (in *Cat) Validate(ctx context.Context) field.ErrorList {
    allErrs := field.ErrorList{}
    if len(in.Spec.Name) == 0 {
        allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "name"), in.Spec.Name, "must be specify"))
    }
    return allErrs
}

部署運行

完成以上步驟,你其實已經擁有一個完整的 Aggregated APIServer,接下來我們試著將它運行起來;apiserver-boot 本身提供了兩種運行模式:in-cluster、local; local 模式下只作為單獨的 API 服務部署在本地方便做調試,過於簡單這裡不做過多介紹,主要關注一下 in-cluster 模式;in-cluster 可以將你的 Aggregated APIServer 部署在任何 K8s 集群中,例如:minikube,騰訊 TKE,EKS 等,我們這裡使用 EKS 集群作為演示。

創建EKS集群&配置好本地kubeconfig

執行部署命令 ;

$ apiserver-boot run in-cluster --image=xxx/skai.io/skai-demo:0.0.1 --name=skai-demo --namespace=default

在執行部署命令過程中,apiserver-boot 主要幫我們做了如下幾件事情:

  • 自動生成 APIServer Dockerfile 文件;
  • 通過 APIServer Dockerfile 構建服務鏡像,並將鏡像推送到指定倉庫;
  • 在config目錄下生成 CA 及其他 APIServer 部署需要的證書文件;
  • 在config目錄下生成 APIServer 部署需要的 Deployment、Service、APIService、ServiceAccount 等 yaml 文件;
  • 將上一步生成的 yaml 文件部署到集群中;

功能驗證

確認 Resource 註冊成功

$ kubectl api-versions |grep animal
animal.skai.io/v1alpha1

確認 Aggregated APIServer 能正常工作

$ kubectl get apiservice v1alpha1.animal.skai.io 
NAME                      SERVICE             AVAILABLE   AGE
v1alpha1.animal.skai.io   default/skai-demo   True        19h

創建並查看新增的 Resource

創建

$ cat lucky.yaml
apiVersion: animal.skai.io/v1alpha1
kind: Cat
metadata:
  name: mycat
  namespace: default
spec:
  name: lucky
# 創建自定義 resource
$ kubectl apply -f lucky.yaml

查找

# 查找自定義 resource 列表
$ kubectl get cat
NAME    CREATED AT
mycat   2021-11-17T09:08:10Z
# 查找自定義資源詳情
$ kubectl get cat mycat -oyaml
apiVersion: animal.skai.io/v1alpha1
kind: Cat
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"animal.skai.io/v1alpha1","kind":"Cat","metadata":{"annotations":{},"name":"mycat"},"spec":{"name":"lucky"}}
  creationTimestamp: "2021-11-17T09:08:10Z"
  name: mycat
  resourceVersion: "17"
  uid: 98af0905-f01d-4042-bad3-71b96c0919f4
spec:
  name: lucky
status: {}

總結

本文從實戰角度出發介紹我們開發 SKAI 平台過程中選擇 Aggregated API 的原因,以及 kube-apisever 的擴展原理,最後介紹了 apiserver-builder 工具,並演示如何一步一步構建起自己的 Aggregated API,並將它部署到 EKS 集群中。希望該篇 Aggregated APIServer 最佳實踐可以幫助即將使用 K8s API 擴展來構建雲原生應用的開發者。

關於我們

更多關於雲原生的案例和知識,可關注同名【騰訊雲原生】公眾號~

福利:

①公眾號後台回復【手冊】,可獲得《騰訊雲原生路線圖手冊》&《騰訊雲原生最佳實踐》~

②公眾號後台回復【系列】,可獲得《15個系列100+篇超實用雲原生原創乾貨合集》,包含Kubernetes 降本增效、K8s 性能優化實踐、最佳實踐等系列。

【騰訊雲原生】雲說新品、雲研新術、雲遊新活、雲賞資訊,掃碼關注同名公眾號,及時獲取更多乾貨!!