如何自定義Kubernetes資源
阿新 • • 發佈:2020-12-15
目前最流行的微服務架構非`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