深入解析聲明式 API 之 API 對(duì)象的奧秘

你可能一直就很好奇:當(dāng)我把一個(gè) YAML 文件提交給 Kubernetes 之后,它究竟是如何創(chuàng)建出一個(gè) API 對(duì)象的呢?

這得從聲明式 API 的設(shè)計(jì)談起了。在 Kubernetes 項(xiàng)目中,一個(gè) API 對(duì)象在 Etcd 里的完整資源路徑,是由:Group(API 組)、Version(API 版本)和 Resource(API 資源類型)三個(gè)部分組成的。

Kubernetes 里 API 對(duì)象的組織方式,其實(shí)是層層遞進(jìn)的。比如,現(xiàn)在我要聲明要?jiǎng)?chuàng)建一個(gè) CronJob 對(duì)象,那么我的 YAML 文件的開(kāi)始部分會(huì)這么寫(xiě):

apiVersion: batch/v2alpha1

????kind: CronJob...

在這個(gè) YAML 文件中,“CronJob”就是這個(gè) API 對(duì)象的資源類型(Resource),“batch”就是它的組(Group),v2alpha1 就是它的版本(Version)。

當(dāng)我們提交了這個(gè) YAML 文件之后,Kubernetes 就會(huì)把這個(gè) YAML 文件里描述的內(nèi)容,轉(zhuǎn)換成 Kubernetes 里的一個(gè) CronJob 對(duì)象。

那么,Kubernetes 是如何對(duì) Resource、Group 和 Version 進(jìn)行解析,從而在 Kubernetes 項(xiàng)目里找到 CronJob 對(duì)象的定義呢?

首先,Kubernetes 會(huì)匹配 API 對(duì)象的組。

需要明確的是,對(duì)于 Kubernetes 里的核心 API 對(duì)象,比如:Pod、Node 等,是不需要 Group 的(即:它們的 Group 是“”)。所以,對(duì)于這些 API 對(duì)象來(lái)說(shuō),Kubernetes 會(huì)直接在 /api 這個(gè)層級(jí)進(jìn)行下一步的匹配過(guò)程。

而對(duì)于 CronJob 等非核心 API 對(duì)象來(lái)說(shuō),Kubernetes 就必須在 /apis 這個(gè)層級(jí)里查找它對(duì)應(yīng)的 Group,進(jìn)而根據(jù)“batch”這個(gè) Group 的名字,找到 /apis/batch。不難發(fā)現(xiàn),這些 API Group 的分類是以對(duì)象功能為依據(jù)的,比如 Job 和 CronJob 就都屬于“batch” (離線業(yè)務(wù))這個(gè) Group。

然后,Kubernetes 會(huì)進(jìn)一步匹配到 API 對(duì)象的版本號(hào)。

對(duì)于 CronJob 這個(gè) API 對(duì)象來(lái)說(shuō),Kubernetes 在 batch 這個(gè) Group 下,匹配到的版本號(hào)就是 v2alpha1。

在 Kubernetes 中,同一種 API 對(duì)象可以有多個(gè)版本,這正是 Kubernetes 進(jìn)行 API 版本化管理的重要手段。這樣,比如在 CronJob 的開(kāi)發(fā)過(guò)程中,對(duì)于會(huì)影響到用戶的變更就可以通過(guò)升級(jí)新版本來(lái)處理,從而保證了向后兼容。

最后,Kubernetes 會(huì)匹配 API 對(duì)象的資源類型。

在前面匹配到正確的版本之后,Kubernetes 就知道,我要?jiǎng)?chuàng)建的原來(lái)是一個(gè) /apis/batch/v2alpha1 下的 CronJob 對(duì)象。這時(shí)候,APIServer 就可以繼續(xù)創(chuàng)建這個(gè) CronJob 對(duì)象了。

首先,當(dāng)我們發(fā)起了創(chuàng)建 CronJob 的 POST 請(qǐng)求之后,我們編寫(xiě)的 YAML 的信息就被提交給了 APIServer。

而 APIServer 的第一個(gè)功能,就是過(guò)濾這個(gè)請(qǐng)求,并完成一些前置性的工作,比如授權(quán)、超時(shí)處理、審計(jì)等。

然后,請(qǐng)求會(huì)進(jìn)入 MUX 和 Routes 流程。如果你編寫(xiě)過(guò) Web Server 的話就會(huì)知道,MUX 和 Routes 是 APIServer 完成 URL 和 Handler 綁定的場(chǎng)所。而 APIServer 的 Handler 要做的事情,就是按照我剛剛介紹的匹配過(guò)程,找到對(duì)應(yīng)的 CronJob 類型定義。

接著,APIServer 最重要的職責(zé)就來(lái)了:根據(jù)這個(gè) CronJob 類型定義,使用用戶提交的 YAML 文件里的字段,創(chuàng)建一個(gè) CronJob 對(duì)象。而在這個(gè)過(guò)程中,APIServer 會(huì)進(jìn)行一個(gè) Convert 工作,即:把用戶提交的 YAML 文件,轉(zhuǎn)換成一個(gè)叫作 Super Version 的對(duì)象,它正是該 API 資源類型所有版本的字段全集。這樣用戶提交的不同版本的 YAML 文件,就都可以用這個(gè) Super Version 對(duì)象來(lái)進(jìn)行處理了。

接下來(lái),APIServer 會(huì)先后進(jìn)行 Admission() 和 Validation() 操作。比如,我在上一篇文章中提到的 Admission Controller 和 Initializer,就都屬于 Admission 的內(nèi)容。而 Validation,則負(fù)責(zé)驗(yàn)證這個(gè)對(duì)象里的各個(gè)字段是否合法。這個(gè)被驗(yàn)證過(guò)的 API 對(duì)象,都保存在了 APIServer 里一個(gè)叫作 Registry 的數(shù)據(jù)結(jié)構(gòu)中。也就是說(shuō),只要一個(gè) API 對(duì)象的定義能在 Registry 里查到,它就是一個(gè)有效的 Kubernetes API 對(duì)象。

最后,APIServer 會(huì)把驗(yàn)證過(guò)的 API 對(duì)象轉(zhuǎn)換成用戶最初提交的版本,進(jìn)行序列化操作,并調(diào)用 Etcd 的 API 把它保存起來(lái)。

由此可見(jiàn),聲明式 API 對(duì)于 Kubernetes 來(lái)說(shuō)非常重要。所以,APIServer 這樣一個(gè)在其他項(xiàng)目里“平淡無(wú)奇”的組件,卻成了 Kubernetes 項(xiàng)目的重中之重。

CRD 的全稱是 Custom Resource Definition。顧名思義,它指的就是,允許用戶在 Kubernetes 中添加一個(gè)跟 Pod、Node 類似的、新的 API 資源類型,即:自定義 API 資源。

舉個(gè)例子,我現(xiàn)在要為 Kubernetes 添加一個(gè)名叫 Network 的 API 資源類型。它的作用是,一旦用戶創(chuàng)建一個(gè) Network 對(duì)象,那么 Kubernetes 就應(yīng)該使用這個(gè)對(duì)象定義的網(wǎng)絡(luò)參數(shù),調(diào)用真實(shí)的網(wǎng)絡(luò)插件,比如 Neutron 項(xiàng)目,為用戶創(chuàng)建一個(gè)真正的“網(wǎng)絡(luò)”。這樣,將來(lái)用戶創(chuàng)建的 Pod,就可以聲明使用這個(gè)“網(wǎng)絡(luò)”了。

這個(gè) Network 對(duì)象的 YAML 文件,名叫 example-network.yaml,它的內(nèi)容如下所示:

apiVersion: samplecrd.k8s.io/v1

kind: Network

metadata:

? name: example-network

spec:

? cidr: "192.168.0.0/16"

? gateway: "192.168.0.1"

可以看到,我想要描述“網(wǎng)絡(luò)”的 API 資源類型是 Network;API 組是samplecrd.k8s.io;API 版本是 v1。

那么,Kubernetes 又該如何知道這個(gè) API(samplecrd.k8s.io/v1/network)的存在呢?其實(shí),上面的這個(gè) YAML 文件,就是一個(gè)具體的“自定義 API 資源”實(shí)例,也叫 CR(Custom Resource)。而為了能夠讓 Kubernetes 認(rèn)識(shí)這個(gè) CR,你就需要讓 Kubernetes 明白這個(gè) CR 的宏觀定義是什么,也就是 CRD(Custom Resource Definition)。

所以,接下來(lái),我就先編寫(xiě)一個(gè) CRD 的 YAML 文件,它的名字叫作 network.yaml,內(nèi)容如下所示:

apiVersion: apiextensions.k8s.io/v1beta1

kind: CustomResourceDefinition

metadata:

? name: networks.samplecrd.k8s.io

spec:

? group: samplecrd.k8s.io

? version: v1

? names:

? ? kind: Network

? ? plural: networks

? scope: Namespaced

可以看到,在這個(gè) CRD 中,我指定了“group: samplecrd.k8s.io”“version: v1”這樣的 API 信息,也指定了這個(gè) CR 的資源類型叫作 Network,復(fù)數(shù)(plural)是 networks。然后,我還聲明了它的 scope 是 Namespaced,即:我們定義的這個(gè) Network 是一個(gè)屬于 Namespace 的對(duì)象,類似于 Pod。這就是一個(gè) Network API 資源類型的 API 部分的宏觀定義了。

接下來(lái),我還需要讓 Kubernetes“認(rèn)識(shí)”這種 YAML 文件里描述的“網(wǎng)絡(luò)”部分,比如“cidr”(網(wǎng)段),“gateway”(網(wǎng)關(guān))這些字段的含義。

首先,我要在 GOPATH 下,創(chuàng)建一個(gè)結(jié)構(gòu)如下的項(xiàng)目:

$ tree $GOPATH/src/github.com/<your-name>/k8s-controller-custom-resource

.

├── controller.go

├── crd

│? └── network.yaml

├── example

│? └── example-network.yaml

├── main.go

└── pkg

? ? └── apis

? ? ? ? └── samplecrd

? ? ? ? ? ? ├── register.go

? ? ? ? ? ? └── v1

? ? ? ? ? ? ? ? ├── doc.go

? ? ? ? ? ? ? ? ├── register.go

? ? ? ? ? ? ? ? └── types.go

其中,pkg/apis/samplecrd 就是 API 組的名字,v1 是版本,而 v1 下面的 types.go 文件里,則定義了 Network 對(duì)象的完整描述。

然后,我在 pkg/apis/samplecrd 目錄下創(chuàng)建了一個(gè) register.go 文件,用來(lái)放置后面要用到的全局變量。

package samplecrd

const (

GroupName = "samplecrd.k8s.io"

Version? = "v1"

)

接著,我需要在 pkg/apis/samplecrd 目錄下添加一個(gè) doc.go 文件(Golang 的文檔源文件)。

// +k8s:deepcopy-gen=package

// +groupName=samplecrd.k8s.io

package v1

在這個(gè)文件中,你會(huì)看到 +<tag_name>[=value]格式的注釋,這就是 Kubernetes 進(jìn)行代碼生成要用的 Annotation 風(fēng)格的注釋。其中,+k8s:deepcopy-gen=package 意思是,請(qǐng)為整個(gè) v1 包里的所有類型定義自動(dòng)生成 DeepCopy 方法;而+groupName=samplecrd.k8s.io,則定義了這個(gè)包對(duì)應(yīng)的 API 組的名字。

接下來(lái),我需要添加 types.go 文件。顧名思義,它的作用就是定義一個(gè) Network 類型到底有哪些字段(比如,spec 字段里的內(nèi)容)。

package v1

...

// +genclient

// +genclient:noStatus

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// Network describes a Network resource

type Network struct {

// TypeMeta is the metadata for the resource, like kind and apiversion

metav1.TypeMeta `json:",inline"`

// ObjectMeta contains the metadata for the particular object, including

// things like...

//? - name

//? - namespace

//? - self link

//? - labels

//? - ... etc ...

metav1.ObjectMeta `json:"metadata,omitempty"`

Spec networkspec `json:"spec"`

}

// networkspec is the spec for a Network resource

type networkspec struct {

Cidr? ? string `json:"cidr"`

Gateway string `json:"gateway"`

}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// NetworkList is a list of Network resources

type NetworkList struct {

metav1.TypeMeta `json:",inline"`

metav1.ListMeta `json:"metadata"`

Items []Network `json:"items"`

}

在上面這部分代碼里,你可以看到 Network 類型定義方法跟標(biāo)準(zhǔn)的 Kubernetes 對(duì)象一樣,都包括了 TypeMeta(API 元數(shù)據(jù))和 ObjectMeta(對(duì)象元數(shù)據(jù))字段。而其中的 Spec 字段,就是需要我們自己定義的部分。所以,在 networkspec 里,我定義了 Cidr 和 Gateway 兩個(gè)字段。其中,每個(gè)字段最后面的部分比如json:"cidr",指的就是這個(gè)字段被轉(zhuǎn)換成 JSON 格式之后的名字,也就是 YAML 文件里的字段名字。

此外,除了定義 Network 類型,你還需要定義一個(gè) NetworkList 類型,用來(lái)描述一組 Network 對(duì)象應(yīng)該包括哪些字段。之所以需要這樣一個(gè)類型,是因?yàn)樵?Kubernetes 中,獲取所有 X 對(duì)象的 List() 方法,返回值都是List 類型,而不是 X 類型的數(shù)組。這是不一樣的。

需要注意的是,+genclient 只需要寫(xiě)在 Network 類型上,而不用寫(xiě)在 NetworkList 上。因?yàn)?NetworkList 只是一個(gè)返回值類型,Network 才是“主類型”。而由于我在 Global Tags 里已經(jīng)定義了為所有類型生成 DeepCopy 方法,所以這里就不需要再顯式地加上 +k8s:deepcopy-gen=true 了。

當(dāng)然,這也就意味著你可以用 +k8s:deepcopy-gen=false 來(lái)阻止為某些類型生成 DeepCopy。你可能已經(jīng)注意到,在這兩個(gè)類型上面還有一句+k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object的注釋。它的意思是,請(qǐng)?jiān)谏?DeepCopy 的時(shí)候,實(shí)現(xiàn) Kubernetes 提供的 runtime.Object 接口。否則,在某些版本的 Kubernetes 里,你的這個(gè)類型定義會(huì)出現(xiàn)編譯錯(cuò)誤。這是一個(gè)固定的操作,記住即可。

最后,我需要再編寫(xiě)一個(gè) pkg/apis/samplecrd/v1/register.go 文件。在前面對(duì) APIServer 工作原理的講解中,我已經(jīng)提到,“registry”的作用就是注冊(cè)一個(gè)類型(Type)給 APIServer。其中,Network 資源類型在服務(wù)器端注冊(cè)的工作,APIServer 會(huì)自動(dòng)幫我們完成。但與之對(duì)應(yīng)的,我們還需要讓客戶端也能“知道”Network 資源類型的定義。這就需要我們?cè)陧?xiàng)目里添加一個(gè) register.go 文件。

它最主要的功能,就是定義了如下所示的 addKnownTypes() 方法:

package v1

...

// addKnownTypes adds our types to the API scheme by registering

// Network and NetworkList

func addKnownTypes(scheme *runtime.Scheme) error {

scheme.AddKnownTypes(

? SchemeGroupVersion,

? &Network{},

? &NetworkList{},

)

// register the type in the scheme

metav1.AddToGroupVersion(scheme, SchemeGroupVersion)

return nil

}

有了這個(gè)方法,Kubernetes 就能夠在后面生成客戶端的時(shí)候,“知道”Network 以及 NetworkList 類型的定義了。像上面這種 register.go 文件里的內(nèi)容其實(shí)是非常固定的,你以后可以直接使用我提供的這部分代碼做模板,然后把其中的資源類型、GroupName 和 Version 替換成你自己的定義即可。這樣,Network 對(duì)象的定義工作就全部完成了??梢钥吹剑鋵?shí)定義了兩部分內(nèi)容:

第一部分是,自定義資源類型的 API 描述,包括:組(Group)、版本(Version)、資源類型(Resource)等。這相當(dāng)于告訴了計(jì)算機(jī):兔子是哺乳動(dòng)物。

第二部分是,自定義資源類型的對(duì)象描述,包括:Spec、Status 等。這相當(dāng)于告訴了計(jì)算機(jī):兔子有長(zhǎng)耳朵和三瓣嘴。

接下來(lái),我就要使用 Kubernetes 提供的代碼生成工具,為上面定義的 Network 資源類型自動(dòng)生成 clientset、informer 和 lister。其中,clientset 就是操作 Network 對(duì)象所需要使用的客戶端,而 informer 和 lister 這兩個(gè)包的主要功能,我會(huì)在下一篇文章中重點(diǎn)講解。這個(gè)代碼生成工具名叫k8s.io/code-generator,使用方法如下所示:

# 代碼生成的工作目錄,也就是我們的項(xiàng)目路徑

$ ROOT_PACKAGE="github.com/resouer/k8s-controller-custom-resource"

# API Group

$ CUSTOM_RESOURCE_NAME="samplecrd"

# API Version

$ CUSTOM_RESOURCE_VERSION="v1"

# 安裝k8s.io/code-generator

$ go get -u k8s.io/code-generator/...

$ cd $GOPATH/src/k8s.io/code-generator

# 執(zhí)行代碼自動(dòng)生成,其中pkg/client是生成目標(biāo)目錄,pkg/apis是類型定義目錄

$ ./generate-groups.sh all "$ROOT_PACKAGE/pkg/client" "$ROOT_PACKAGE/pkg/apis" "$CUSTOM_RESOURCE_NAME:$CUSTOM_RESOURCE_VERSION"

而有了這些內(nèi)容,現(xiàn)在你就可以在 Kubernetes 集群里創(chuàng)建一個(gè) Network 類型的 API 對(duì)象了。

首先,使用 network.yaml 文件,在 Kubernetes 中創(chuàng)建 Network 對(duì)象的 CRD(Custom Resource Definition):

$ kubectl apply -f crd/network.yaml

customresourcedefinition.apiextensions.k8s.io/networks.samplecrd.k8s.io created

然后,我們就可以創(chuàng)建一個(gè) Network 對(duì)象了,這里用到的是 example-network.yaml:

$ kubectl apply -f example/example-network.yaml?

network.samplecrd.k8s.io/example-network created

通過(guò)這個(gè)操作,你就在 Kubernetes 集群里創(chuàng)建了一個(gè) Network 對(duì)象。它的 API 資源路徑是samplecrd.k8s.io/v1/networks。

此文章為3月Day15學(xué)習(xí)筆記,內(nèi)容來(lái)源于極客時(shí)間《深入剖析 Kubernetes》,強(qiáng)烈推薦該課程。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容