1. 程式人生 > >如何自定義Kubernetes資源

如何自定義Kubernetes資源

目前最流行的微服務架構非`Springboot+Kubernetes+Istio`莫屬, 然而隨著越來越多的微服務被拆分出來, 不但Deploy過程boilerplate的配置越來越多, 且繁瑣易錯, 維護成本也逐漸增高, 那麼是時候採用k8s提供的擴充套件自定義資源的方法, 將重複的template抽到後面, 從而簡化Deploy配置的數量與複雜度. Tips: `一個基礎的k8s微服務應該由幾部分組成, 首先Deployment負責App的部署, Service負責埠的暴露, ServiceAccount負責賦予Pod相應的Identity, ServiceRole+RoleBinding負責api的訪問許可權控制, VirtualService負責路由, 如果我們將許可權控制從RBAC` 如果我們通過自定義資源的方式, 將每個微服務App的共有配置封裝起來, 暴露出可變部分供各個應用配置, 如image, public/private api, mesh內訪問許可權等, 並設定mandatory/required的欄位與validation pattern, 這樣每一個App只需要一個配置檔案, 就可以完成統一部署. 下面我們可以首先看一下k8s提供的擴充套件的兩種方法. - 通過Aggregated Apiserver - 通過Custom Resource Defination ![](https://img2020.cnblogs.com/blog/325852/202012/325852-20201215182210994-1446718453.png) 這兩種最大的區別就是, 前者需要自己實現一個使用者自定義的Apiserver, 而後者是被kube-apiserver內的extension apiserver module所處理. 二者都需要通過自定義Controller來處理資源的配置. ## 建立CRD 雖然看似AA的開放程度更高, 但實際上通過CRD來定義自定義資源更成熟且方便. 通過[官方文件](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/), 我們可以看到CRD的定義規則, 如下面我們定義的一個資源`Beer`: ``` apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: beer.weyoung.io spec: group: weyoung.io versions: - name: v1alpha1 storage: true served: true schema: openAPIV3Schema: type: object properties: spec: type: object properties: BeerName: description: This is a custom field of beer pattern: ^[a-z]+$ type: string required: - beerName required: - spec type: object names: kind: Beer listKind: BeerList singular: beer plural: beers scope: Namespaced ``` 這裡我們添加了一個自定義欄位`beerName`, 指定了pattern, 並且設定了spec與其下的beerName死`required`欄位. 其餘關鍵字可以查閱[官方文件](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/). 在apply該配置後, 通過`kubectl get crd`即可獲得已經存在的資源. ``` > kubectl get crd NAME CREATED AT beers.weyoung.io 2020-12-12T17:28:34Z ``` 有了CRD之後, 我們就可以apply自定義的資源了: ``` apiVersion: weyoung.io/v1alpha1 kind: Beer metadata: name: test-beer spec: beerName: abc ``` 這裡大家可以試驗一下required與pattern的作用是否生效. 這裡有個比較關鍵的點, CRD中未宣告的資源依舊可以被成功新增到`Beer`資源裡, 併成功apply&configured, 但是不具備validation的效果, 這也可能是早期版本的crd把`schema`關鍵字換做`validation`的原因吧. ## 建立Controller 當我們實現了自己的CRD, 並且apply了根據CRD定義的自定義資源`Beer`後, 這僅僅是存在etcd的靜態資源, 還需要controller來根據`Beer`的變化建立相應資源, 讓`Beer`動起來. 首先可以先了解一下Controller的工作原理. ![](https://img2020.cnblogs.com/blog/325852/202012/325852-20201215182237861-1912923392.png) 從圖中可以看出, Informer模組內的Reflactor會監聽k8sapi, 根據自己註冊的資源型別, 輪詢的將所有資源的最新狀態存入佇列, Indexer會對資源進行index, 生成key與namespace/name的對映關係. 通過Informer的監聽方法, 可以從佇列依次讀取, 放進WorkQueue中供controller消費, 而controller可以通過Lister API由namespace/name獲取到對應的自定義資源, 並做增刪改查操作. 看似有很多模組在其中, 不過k8s已經幫我們實現了大部分(Informer, Lister等), 即它的`client-go` library. 我們只需要用對應的api來實現自己的controller部分就可以了. 具體的邏輯, k8s官方提供了一個[sample-controller](https://github.com/kubernetes/sample-controller)來供大家學習, 本人通過對這個sample的踩坑, 總結了一些需要注意的地方會在之後highlight出來. 總體來講, 自定義自己的controller需要三個步驟: - 定義資源Type與註冊 - 生成Informer相應程式碼 - 編寫與呼叫Controller 當然在這之前, 需要先新增依賴. - apimachinery 負責client與k8sapi之間通訊編解碼等 - client-go 呼叫k8s cluster相關api - code-generator 根據定義的type生成相關程式碼, 包括informer, client 這裡需要注意的是依賴版本應該最新, 並且一致, 因為在嘗試多次之後, 發現新老版本之間的變化非常大, 添加了很多新的module與功能, 而且如果不一致, 也會導致程式碼生成, 與k8sapi之間通訊等各種的異常. 當我們通過`go init custom-k8s-controller`初始化新的module後, 可以新增最新依賴: ``` go get apimachinery@master go get client-go@master go get code-generator@master ``` ### 定義Type與註冊 首先需要在工程目錄建立一個存放自己api module的地方, 如`api`, 並在之下建立`beercontroller`, 放置我們自定義資源相關的程式碼, 版本號可以自己定義, v1/v1alphav1. - `types.go` ``` package v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object //Beer is a custom kubenetes resourec type Beer struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec BeerSpec `json:"spec"` } //BeerSpec is the spec of Beer type BeerSpec struct { BeerName string `json:"BeerName"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object //BeerList is a list of Beer type BeerList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata"` Items []Beer `json:"items"` } ``` 這裡定義了`Beer`的成員`BeerName`, 且有兩行給程式碼生成器的註釋也很關鍵, 不能缺失. - `register.go` ``` ... var ( // SchemeBuilder initializes a scheme builder SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) // AddToScheme is a global function that registers this API group & version to a scheme AddToScheme = SchemeBuilder.AddToScheme ) func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes( SchemeGroupVersion, &Beer{}, &BeerList{}, ) // register the type in the scheme metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil } ... ``` 這裡省略了一些, 完整程式碼可參考官方[sample](https://github.com/.kubernetes/sample-controller/blob/master/pkg/apis/samplecontroller/v1alpha1/register.go). 這裡主要的邏輯是將我們定義的資源註冊上去, 當然由於還沒有執行generator, AddKnownTypes會因為我們的`Beer`沒有Deepcopy而報錯. ### 生成程式碼 之前講過需要實現一個完整的自定義k8s資源需要很多東西, 但實際上除了controller之外的都可以生成出來, 這裡就使用到了`code-generator`裡面的`generate-groups.sh`. 由於要使用該指令碼, 這裡我們可以新增`vendor`來管理我們的之前新增的依賴. 在工程的根目錄輸出`git mod vendor`來建立vendor目錄, 這時所有之前新增過的依賴 (go.mod)都會出現在這個資料夾下供我們專案使用. 關於如何呼叫generator的邏輯可以參考官方[sample](https://github.com/kubernetes/code-generator/blob/master/hack/update-codegen.sh), 這裡我們需要給`vendor`整體(-R)賦予可執行許可權, 否則在generate的過程會提示許可權問題. 關於`generate-groups.sh`的使用需要注意的是它其中的2,3,4引數, 分別為輸出目錄, 輸入目錄(我們自定義資源的module所在的目錄, 即之前建立的api資料夾), 自定義資源名:版本. 具體引數設定也可以參考: ``` ../vendor/k8s.io/code-generator/generate-groups.sh \ "deepcopy,client,informer,lister" \ custom-k8s-controller/generated \ custom-k8s-controller/api \ beercontroller:v1alpha1 \ --go-header-file $(pwd)/boilerplate.go.txt \ --output-base $(pwd)/../../ ``` 在執行指令碼過後, 它會在`types.go`旁生成deepcopy程式碼, 並會在我們指定的generated資料夾下生成informer, lister等程式碼. ### 編寫與呼叫Controller 現在萬事俱備只欠實現最後的自定義資源處理邏輯, 就是之前提到的controller. 首先我們需要定義controller的成員: ``` type Controller struct { // kubeclientset is a standard kubernetes clientset kubeclientset kubernetes.Interface // beerclientset is a clientset for our own API group beerclientset clientset.Interface deploymentsLister appslisters.DeploymentLister deploymentsSynced cache.InformerSynced beersLister listers.beerLister beersSynced cache.InformerSynced // workqueue is a rate limited work queue. This is used to queue work to be // processed instead of performing it as soon as a change happens. This // means we can ensure we only process a fixed amount of resources at a // time, and makes it easy to ensure we are never processing the same item // simultaneously in two different workers. workqueue workqueue.RateLimitingInterface // recorder is an event recorder for recording Event resources to the // Kubernetes API. recorder record.EventRecorder } ``` 其中kubeclientset可以呼叫k8s的CRUD api, 例如建立更新deployment; beerclientset可以呼叫自定義資源的CRUD; deploymentsLister與beersLister可以通過namespace/name獲得對應資源; workqueue用來同步與限流多個自定義資源處理worker; recorder用來publish在資源處理過程中的事件, 該事件可以在kubectl describe裡面看到. 接著我們可以建立建構函式將clientset, informer傳入構建自己的controller物件, 並通過informer的`AddEventHandler`新增監聽, 將回調的beer資源通過indexer(cache)的MetaNamespaceKeyFunc方法轉換為key, 加入workqueue佇列. 當然有佇列必然有死迴圈去讀取這個佇列, 這也是我們啟動整個controller的入口, 我們需要從workqueue中pop交由下游處理, 在處理成功後呼叫forget方法清除queue, 否則會重新進行處理. 處理資源是我們controller的核心邏輯, 這裡我們可以再次通過indexer(cache)通過key反轉回namespace/name, 然後通過listers查詢對應的資源, 比如beer或者deployment, 通過對比自定義資源與背後k8s資源的區別, 對k8s資源進行CRUD. ``` func (c *Controller) syncHandler(key string) error { // Convert the namespace/name string into a distinct namespace and name namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { utilruntime.HandleError(fmt.Errorf("invalid resource key: %s", key)) return nil } // Get the beer resource with this namespace/name beer, err := c.beersLister.beers(namespace).Get(name) if err != nil { // The beer resource may no longer exist, in which case we stop // processing. klog.Infof("beer %s is deleted ", name) if errors.IsNotFound(err) { utilruntime.HandleError(fmt.Errorf("beer '%s' in work queue no longer exists", key)) return nil } return err } deploymentName := beer.Spec.beerName if deploymentName == "" { // We choose to absorb the error here as the worker would requeue the // resource otherwise. Instead, the next time the resource is updated // the resource will be queued again. utilruntime.HandleError(fmt.Errorf("%s: deployment name must be specified", key)) return nil } deployment, err := c.deploymentsLister.Deployments(beer.Namespace).Get(beer.Name) // If the resource doesn't exist, we'll create it if errors.IsNotFound(err) { deployment, err = c.kubeclientset.AppsV1().Deployments(beer.Namespace).Create(context.TODO(), newDeployment(beer), metav1.CreateOptions{}) } // If an error occurs during Get/Create, we'll requeue the item so we can // attempt processing again later. This could have been caused by a // temporary network failure, or any other transient reason. if err != nil { return err } // *********************** // Handle custom CRUD here // *********************** c.recorder.Event(beer, corev1.EventTypeNormal, SuccessSynced, MessageResourceSynced) return nil } ``` 整個`syncHanlder`函式分為四個部分: - 通過index做key->namespace/name - 通過lister查詢beer資源 - 如果不存在, 說明這是一次刪除操作, 直接返回 - 通過lister獲取deployment - 如果不存在, 通過beer中相關欄位建立對應的deployment, 或者其他k8s資源 - 否則, 說明資源已經存在, 則對比beer與實際資源的區別, 判斷是否需要對相應的資源, 如deployment進行更新. 這裡舉例子 當然在整個過程中也可以通過`recorder`來廣播event, 這樣在apply自定義資源後, 可以獲取一定資訊, 方便檢視或者debug問題原因. controller完整的邏輯存在大量boilerplate的程式碼, 具體也可以參考官方的[sample](https://github.com/kubernetes/sample-controller/blob/master/controller.go)進行學習. 最後還有一點需要注意的是, 在我們建立並啟動自己的controller時, 即本次`custom-k8s-controller`的入口main package內, 除了呼叫構造以及啟動函式, 一定要記得傳入的Informer需要呼叫`Start`進行啟動, 否則無法獲得資源變化的回撥. 當然這裡也涉及到需要傳入一個stopSignal可以讓監聽中斷, 這一個可以直接cop[y官方程式碼](https://github.com/kubernetes/sample-controller/tree/master/pkg/signals). ## 總結 k8s已經提供了一個非常詳盡的[demo](https://github.com/kubernetes/sample-controller)來實現自定義資源並通過controller進行配置, 但是對於沒有go開發經驗, 有不少由於版本, 依賴, 許可權造成的小問題, 希望文章中highlight的點能提供一定幫助~ 最後通過不懈努力, 我們可以通過一個自定義的Beer資源來deploy pod了. 由於這條路的打通, 我們可以繼續新增CRD的schema, 並在controller內進行解析, 簡化deploy過程, 統一基於k8s的微服務基礎架構. 比如對於任意一個通過Beer資源deploy的app, 我們都通過istio sidecar來做ingress/outgress, 對於image的完整倉庫地址, 我們也在後面通過不同部署(apply)環境來組合不同的地址, 還有大量預設值的公共配置, 都不需要再重複的寫在多個yaml中了. ## Reference - https://github.com/kubernetes/sample-controller - https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/ - https://itnext.io/comparing-kubernetes-api-extension-mechanisms-of-custom-resource-definition-and-aggregated-api-64f4