operator 之旅(一)

環境準備

依賴版本

MAC M1

kubernetes: 1.18.3

go: 1.17.6

kubebuilder:3.1.0

知識必備

Kubernetes的Group、Version、Resource、Kind淺解

Kubernetes是以資源爲中心的系統:Group、Version、Resource、Kind 等資源相關的數據結構顯得格外重要。

Group

Group即資源組,在kubernetes對資源進行分組時,對應的數據結構就是Group,源碼路徑:staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go
,如下,可見Group有自己的名稱和版本:

type APIGroup struct {
	TypeMeta `json:",inline"`
	Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
	Versions []GroupVersionForDiscovery `json:"versions" protobuf:"bytes,2,rep,name=versions"`
	PreferredVersion GroupVersionForDiscovery `json:"preferredVersion,omitempty" protobuf:"bytes,3,opt,name=preferredVersion"`
	ServerAddressByClientCIDRs []ServerAddressByClientCIDR `json:"serverAddressByClientCIDRs,omitempty" protobuf:"bytes,4,rep,name=serverAddressByClientCIDRs"`
}

在kubernetes中有兩種資源組:有組名資源組和無組名資源組(也叫核心資源組Core Groups),它們都很常見:
例子:deployment有組名,pod沒有組名,把它倆的OpenAPI放在一起對比就一目瞭然了

Version

Version即版本,這個好理解,kubernetes的版本分爲三種:

Alpha:內部測試版本,如v1alpha1

Beta:經歷了官方和社區測試的相對穩定版,如v1beta1

Stable:正式發佈版,如v1、v2

如下圖紅框,資源組batch之下有v1和v2alpha1兩個版本,每個版本下都有多個資源:

數據結構源碼還是在staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go文件中,如下:

type APIVersions struct {
	TypeMeta `json:",inline"`
	Versions []string `json:"versions" protobuf:"bytes,1,rep,name=versions"`
	ServerAddressByClientCIDRs []ServerAddressByClientCIDR `json:"serverAddressByClientCIDRs" protobuf:"bytes,2,rep,name=serverAddressByClientCIDRs"`
}

Resource

Resource資源在kubernetes中的重要性是不言而喻的,常見的pod、service、deployment這些都是資源。

在kubernetes環境被實例化的資源即資源對象(ResourceObject)

資源被分爲持久性(Persistent Entity)和非持久性(Ephemeral Entity),持久性如deployment,創建後會在etcd保存,非持久性如pod

kubernetes爲資源準備了8種操作:create、delete、deletecollection、get、list、patch、update、watch,每一種資源都支持其中的一部分,這在每個資源的API文檔中可以看到

增:

create:Resource Object 創建
刪:

delete:單個 Resource Object 刪除

deletecollection:多個 Resource Objects 刪除

改:
patch:Resource Object 局部字段更新

update:Resource Object 整體更新

查:

get:單個 Resource Object 獲取

list:多個 Resource Objects 獲取

watch:Resource Objects 監控

資源支持以命名空間(namespace)進行隔離

資源對象描述文件在日常操作中頻繁用到,一共由五部分組成:apiVersion、kind、metadata、spec、status,下圖是官方的deployment描述文件,用於創建3個nginx pod,對着紅框和文字就瞭解每個部分的作用了


上圖並沒有status,該部分是用來反應當前資源對象狀態的,體現在資源的數據結構中,如下所示:

type Deployment struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
	Spec DeploymentSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"`
	Status DeploymentStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
}

數據結構源碼還是在staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go文件中,如下:

type APIResource struct {
	Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
	SingularName string `json:"singularName" protobuf:"bytes,6,opt,name=singularName"`
	Namespaced bool `json:"namespaced" protobuf:"varint,2,opt,name=namespaced"`
	Group string `json:"group,omitempty" protobuf:"bytes,8,opt,name=group"`
	Version string `json:"version,omitempty" protobuf:"bytes,9,opt,name=version"`
	Kind string `json:"kind" protobuf:"bytes,3,opt,name=kind"`
	Verbs Verbs `json:"verbs" protobuf:"bytes,4,opt,name=verbs"`
	ShortNames []string `json:"shortNames,omitempty" protobuf:"bytes,5,rep,name=shortNames"`
	Categories []string `json:"categories,omitempty" protobuf:"bytes,7,rep,name=categories"`
	StorageVersionHash string `json:"storageVersionHash,omitempty" protobuf:"bytes,10,opt,name=storageVersionHash"`
}

Kind

Kind 與 Resource 屬於同一級概念,Kind 用於描述 Resource 的種類

Scheme

每種資源的都需要有對應的Scheme,Scheme結構體包含gvkToType和typeToGVK的字段映射關係,APIServer 根據Scheme來進行資源的序列化和反序列化。

GV & GVK & GVR

GV: Api Group & Version

API Group 是相關 API 功能的集合

每個 Group 擁有一或多個 Versions

GVK: Group Version Kind

每個 GV 都包含 N 個 api 類型,稱之爲 Kinds,不同 Version 同一個 Kinds 可能不同

GVR: Group Version Resource

Resource 是 Kind 的對象標識,一般來 Kind 和 Resource 是 1:1 的,但是有時候存在 1:n 的關係,不過對於 Operator 來說都是 1:1 的關係

舉個例子,我們在 k8s 中的 yaml 文件都有下面這麼兩行:

apiVersion: apps/v1 # 這個是 GV,G 是 apps,V 是 v1
kind: Deployment    # 這個就是 Kind
sepc:               # 加上下放的 spec 就是 Resource了
  ...

根據 GVK K8s 就能找到你到底要創建什麼類型的資源,根據你定義的 Spec 創建好資源之後就成爲了 Resource,也就是 GVR。GVK/GVR 就是 K8s 資源的座標,是我們創建/刪除/修改/讀取資源的基礎。

查看當前環境的所有資源,及其相關屬性
# ctl api-resources -o wide
查看特定group下面的資源,例如:apps
# ctl api-resources --api-group apps -o wide
查看指定資源的詳情,例如:configmap
# ctl explain configmap
查看所有Group和Version的命令
# ctl api-versions

官方文檔速查

在實際學習和開發中,對指定資源做增刪改查操作時,官方文檔是我們最可靠的依賴,地址:https://kubernetes.io/docs/reference/kubernetes-api/

打開deployment的文檔,如下圖:

另外還有API文檔也是必不可少的,最新的是1.19版本,地址:https://v1-22.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/

下圖是deployment的api接口文檔,可見示例、path、請求響應參數都有詳細的說明:

APIResources數據結構

APIResource是個常用的數據結構了,可以用來描述資源,例如kubernetes/pkg/controller/resourcequota/resource_quota_controller_test.go中有對其的使用:

func TestDiscoverySync(t *testing.T) {
	serverResources := []*metav1.APIResourceList{
		{
			GroupVersion: "v1",
			APIResources: []metav1.APIResource{
				{Name: "pods", Namespaced: true, Kind: "Pod", Verbs: metav1.Verbs{"create", "delete", "list", "watch"}},
			},
		},
	}
	unsyncableServerResources := []*metav1.APIResourceList{
		{
			GroupVersion: "v1",
			APIResources: []metav1.APIResource{
				{Name: "pods", Namespaced: true, Kind: "Pod", Verbs: metav1.Verbs{"create", "delete", "list", "watch"}},
				{Name: "secrets", Namespaced: true, Kind: "Secret", Verbs: metav1.Verbs{"create", "delete", "list", "watch"}},
			},
		},
	}

如何對Kubernetes進行擴展

Kubernetes 是一個可移植的、可擴展的開源平臺,用於管理容器化的工作負載和服務,可促進聲明式配置和自動化。 Kubernetes 擁有一個龐大且快速增長的生態系統。Kubernetes 的服務、支持和工具廣泛可用。
雖然現在 Kubernetes 已經是容器編排的事實標準,其本身的功能也非常豐富並且靈活,但是也不能滿足所有人的需求,在遇到 Kubernetes 提供的能力無法滿足我們需求的時候,我們就可以利用其強大的擴展能力進行定製。
在實際工作中,對kubernetes的資源執行各種個性化配置和控制是很常見的需求,例如自定義鏡像的pod如何控制副本數、主從關係,以及各種自定義資源的控制等。

Kubernetes 有哪些擴展點


如上圖所示,從客戶端到底層容器運行時,絕大部分地方 Kubernetes 都爲我們預留了擴展點,我們從上往下一個一個的來看

kubectl

kubectl 是我們平時和 Kubernetes 交互使用的最多的客戶端工具,常見的運維操作都會通過 kubectl 來完成,kubectl 爲我們提供了插件機制來方便擴展。

kubectl 插件其實就是以kubectl-爲前綴的任意可執行文件 ,執行 kubectl 插件的時候可以通過 kubectl 插件名 參數 的方式運行插件。

就像 Ubuntu 使用 apt 管理軟件,mac 可以使用 brew 一樣,kubectl 也有類似的插件管理工具 krew ,同時我們可以從 https://krew.sigs.Kubernetes.io/plugins/ 查找是否已經存在我們需要的插件

APIServer
聚合層

從 Kubernetes v1.7 版本之後 APIServer 引入了聚合層的功能,這個功能可以讓每個開發者都能夠實現聚合 API 服務暴露它們需要的接口,這個過程不需要重新編譯 Kubernetes 的任何代碼。

如果我們將下面這個資源提交給 Kubernetes 之後,用戶在訪問 API 服務器的 /apis/metrics.Kubernetes.io/v1beta1 路徑時,會被轉發到集羣中的 metrics-server.kube-system.svc 服務上

apiVersion: apiregistration.Kubernetes.io/v1
kind: APIService
metadata:
  name: v1beta1.metrics.Kubernetes.io
spec:
  service:
    name: metrics-server
    namespace: kube-system
  group: metrics.Kubernetes.io
  version: v1beta1
  insecureSkipTLSVerify: true
  groupPriorityMinimum: 100
  versionPriority: 100
准入控制

除此之外無論是從 kubectl 還是 client-go 等其他客戶端發起的請求都會發送到 APIServer 經過 認證 -> 鑑權 -> 准入控制 的步驟,這其中的每一步我們都可以對其進行擴展,而這其中用的最多的就是准入控制的擴展。

准入控制當中又會先經過,變更准入控制 MutatingAdmissionWebhook,然後再經過驗證准入控制 ValidatingAdmissionWebhook,任何一個准入控制器返回了錯誤這個請求都會失敗,例如這兩個准入控制器我們可以做很多事情,例如注入 sidecar,驗證資源,調整 pod 的配額等等。

Kubernetes 資源

我們常用的 Deployment、Pod、Node 等都是 Kubernetes 官方提供的內置資源,但是有的時候內置的資源無法滿足我們的需求的時候,就可以使用 CR(CustomResource) 也就是自定義資源。自定義資源常常會和 Controller 一起配合使用,不過需要注意的是使用自定義資源的時候需要思考一下如果只是一些配置可能 ConfigMap 會更加適合,不要濫用這個特性。

Controller 控制器

Kubernetes 中資源的狀態的維護都是 Controller 來實現的,Controller 會不斷的嘗試將一個資源調整爲我們描述的狀態,這其實也就是我們常說的聲明式 api,聲明式 api 背後具體的活都是 Controller 乾的。Controller 一般會配合着 CRD(Custom Resource Definition) 一起使用

Schedule 調度器

調度器是一種特殊的控制器,負責監視 Pod 變化並將 Pod 分派給節點,調度器可以被直接替換掉或者是使用多個調度器,除此之外官方默認的調度器也支持 WebHook。

CNI 網絡插件

CNI 網絡插件,全稱 Container Network Interface(容器網絡接口)包含一組用於開發插件去配置 Linux 容器中網卡的接口和框架。一般我們不會去對網絡插件做定製開發,而是採用開源的組件,例如 Flannel、Cilium,如果使用雲服務的 Kubernetes 還會遇到一些定製的網絡插件, 例如阿里雲有 Terway

CSI 存儲插件

CSI 存儲插件,全稱 Container Storage Interface,可以通過 CSI 接口支持不同的存儲類型

CRI 容器運行時

CRI 容器運行時,全稱 Container Runtime Interface,是一組用於管理容器運行時和鏡像的 gRPC 接口,利用這個接口可以支持 docker、containerd 等不同的容器運行時

kustomize

看到 Kustomize 我的第一反應是這個東西和 helm 有什麼區別,Kustomize 沒有模板語法,只需要一個二進制命令就可以生成對應的 yaml 文件非常的輕量,而 helm 支持 GoTemplate,組件上也要多一些,並且 helm 通過 chart 包來進行發佈相對來說還是要重量級一些。個人覺得 Kustomize 更適合做 gitops 而 helm 更合適做應用包的分發。

那麼kustomize是什麼,有什麼用,怎麼用?

簡介

kustomize 是一個通過 kustomization 文件定製 kubernetes 對象的工具,它可以通過一些資源生成一些新的資源,也可以定製不同的資源的集合。

一個比較典型的場景是有一個應用,在不同的環境例如生產環境和測試環境,它的 yaml 配置絕大部分都是相同的,只有個別的字段不同,這時候就可以利用 kustomize 來解決,kustomize 也比較適合用於 gitops 工作流。


如上圖所示,有一個 ldap 的應用,/base目錄保存的是基本的配置,/overlays裏放置的不同環境的配置,例如 /dev、/staging,/prod這些就是不同環境的配置,/base等文件夾下都有一個 kustomization .yml 文件,用於配置。

執行 kustomize build dir的方式就可以生成我們最後用於部署的 yaml 文件,也就是進行到了我們上圖的第四步,然後通過 kubectl apply -f命令進行部署。

佈局

├── base
│   ├── deployment.yaml
│   ├── kustomization.yaml
│   └── service.yaml
└── overlays
    ├── dev
    │   ├── kustomization.yaml
    │   └── patch.yaml
    ├── prod
    │   ├── kustomization.yaml
    │   └── patch.yaml
    └── staging
        ├── kustomization.yaml
        └── patch.yaml

一個常見的項目 kustomize 項目佈局如上所示,可以看到每個環境文件夾裏面都有一個 kustomization.yaml 文件,這個文件裏面就類似配置文件,裏面指定源文件以及對應的一些轉換文件,例如 patch 等

kustomization.yml

一個常見的 kustomization.yml 如下所示,一般包含 apiVsersion 和 kind 兩個固定字段

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- manager.yaml

configMapGenerator:
- files:
  - controller_manager_config.yaml
  name: manager-config

kustomize 提供了比較豐富的字段選擇,除此之外還可以自定義插件,下面會大概列舉一下每個字段的含義,當我們需要用到的時候知道有這麼個能力,然後再去 Kustomize 官方文檔 (https://kubectl.docs.kubernetes.io/zh/guides/) 查找對應的 API 文檔就行了

resources: 表示 k8s 資源的位置,這個可以是一個文件,也可以指向一個文件夾,讀取的時候會按照順序讀取,路徑可以是相對路徑也可以是絕對路徑,如果是相對路徑那麼就是相對於 kustomization.yml的路徑

crds: 和 resources 類似,只是 crds 是我們自定義的資源

namespace: 爲所有資源添加 namespace

images: 修改鏡像的名稱、tag 或 image digest ,而無需使用 patches

replicas: 修改資源副本數

namePrefix: 爲所有資源和引用的名稱添加前綴

nameSuffix: 爲所有資源和引用的名稱添加後綴

patches: 在資源上添加或覆蓋字段,Kustomization 使用 patches 字段來提供該功能。

patchesJson6902: 列表中的每個條目都應可以解析爲 kubernetes 對象和將應用於該對象的 JSON patch。

patchesStrategicMerge: 使用 strategic merge patch 標準 Patch resources.

vars: 類似指定變量

commonAnnotations: 爲所有資源加上 annotations 如果對應的 key 已經存在值,這個值將會被覆蓋

commonAnnotations:
  app.lailin.xyz/inject: agent

resources:
- deploy.yaml

commonLabels: 爲所有資源的加上 label 和 label selector 注意:這個操作會比較危險

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

commonLabels:
  app: bingo

configMapGenerator 可以生成 config map,列表中的每一條都會生成一個 configmap

secretGenerator 用於生成 secret 資源

generatorOptions 用於控制 configMapGenerator 和 secretGenerator 的行爲

CRD

CRD:自定義資源定義,Kubernetes 中的資源類型。

CR:Custom Resource,對使用 CRD 創建出來的自定義資源的統稱

CRD 是用來擴展 Kubernetes 最常用的方式,在 Service Mesh 和 Operator 中也被大量使用。因此如果想在 Kubernetes 上做擴展和開發的話,是十分有必要了解 CRD 的。

官方文檔:https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/

創建 CRD(CustomResourceDefinition)

創建新的 CustomResourceDefinition(CRD)時,Kubernetes API Server 會爲您指定的每個版本創建新的 RESTful 資源路徑。CRD 可以是命名空間的,也可以是集羣範圍的,可以在 CRD scope 字段中所指定。與現有的內置對象一樣,刪除命名空間會刪除該命名空間中的所有自定義對象。CustomResourceDefinition 本身是非命名空間的,可供所有命名空間使用。

參考下面的 CRD,將其配置保存在 resourcedefinition.yaml 文件中:

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  # 名稱必須符合下面的格式:<plural>.<group>
  name: crontabs.stable.example.com
spec:
  # REST API使用的組名稱:/apis/<group>/<version>
  group: stable.example.com
  # REST API使用的版本號:/apis/<group>/<version>
  versions:
    - name: v1
      # 可以通過 served 來開關每個 version
      served: true
      # 有且僅有一個 version 開啓存儲
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                cronSpec:
                  type: string
                image:
                  type: string
                replicas:
                  type: integer
  # Namespaced或Cluster
  scope: Namespaced
  names:
    # URL中使用的複數名稱: /apis/<group>/<version>/<plural>
    plural: crontabs
    # CLI中使用的單數名稱
    singular: crontab
    # CamelCased格式的單數類型。在清單文件中使用
    kind: CronTab
    # CLI中使用的資源簡稱
    shortNames:
    - ct

然後在以下位置創建一個新的命名空間 RESTful API 端點:

/apis/stable.example.com/v1/namespaces/*/crontabs/...

然後,此端點 URL 可用於創建和管理自定義對象。上面的 CRD 中定義的類型就是 CronTab。

可能需要幾秒鐘才能創建端點。可以監控 CustomResourceDefinition 中 Established 的狀態何時爲 true,或者查看 API 資源的發現信息中是否顯示了資源。

創建自定義對象

創建 CustomResourceDefinition 對象後,您可以創建自定義對象。自定義對象可包含自定義字段。這些字段可以包含任意 JSON。在以下示例中, cronSpec 和 image 自定義字段在自定義對象中設置 CronTab。CronTab 類型來自您在上面創建的 CustomResourceDefinition 對象的規範。

如果您將以下 YAML 保存到 my-crontab.yaml:

apiVersion: "stable.example.com/v1"
kind: CronTab
metadata:
  name: my-new-cron-object
spec:
  cronSpec: "* * * * */5"
  image: my-awesome-cron-image

並創建它:

kubectl create -f my-crontab.yaml

然後,您可以使用 kubectl 管理 CronTab 對象。例如:

kubectl get crontab

應該打印這樣的列表:

NAME                 AGE
my-new-cron-object   6s

...
https://jimmysong.io/kubernetes-handbook/concepts/crd.html

https://liqiang.io/post/kubernetes-all-about-crd-part01-crd-introduction-fb14d399

Operator簡介

Kubernetes 是一個高度可擴展的系統,雖然它的擴展點這麼多,但是一般來說我們接觸的比較多的還是 自定義資源,控制器,准入控制,有些還會對 kubectl 和 調度器做一些擴展,其他的大部分使用成熟的開源組件就可以了。

官方對Operator的介紹:https://kubernetes.io/zh/docs/concepts/extend-kubernetes/operator/ ,Operator模式的執行流程如下圖所示:

Operator 遵循 Kubernetes 的理念,它利用自定義資源管理應用及其組件, Operator 模式會封裝你編寫的任務自動化代碼。

Operator 常見使用範圍包括:

按需部署應用

獲取/還原應用狀態的備份

處理應用代碼的升級以及相關改動。例如,數據庫 schema 或額外的配置設置

發佈一個 service,要求不支持 Kubernetes API 的應用也能發現它

模擬整個或部分集羣中的故障以測試其穩定性

在沒有內部成員選舉程序的情況下,爲分佈式應用選擇首領角色

從 Operator 理念的提出到現在已經有了很多工具可以幫助快速低成本的開發,其中最常用的就是 CoreOS 開源的 operator-sdk 和 k8s sig 小組維護的 kubebuilder。

除了自己開發之外還可以在 https://operatorhub.io/ 上找到別人開發的現成的 Operator 進行使用

kubebuilder

kubebuilder 初體驗

#  mkdir kubedev   
# cd kubedev      
# go mod init kubedev
go: creating new go.mod: module kubedev
#  kubebuilder init --domain zise.feizhu
Error: failed to initialize project: unable to run pre-scaffold tasks of "base.go.kubebuilder.io/v3": go version 'go1.17.6' is incompatible because 'requires 1.13 <= version < 1.17'. You can skip this check using the --skip-go-version-check flag 

這種情況下可以添加 --skip-go-version-check 忽略這個錯誤,但是還是建議使用官方推薦的版本

# kubebuilder init --domain zise.feizhu --skip-go-version-check
Writing scaffold for you to edit...
Get controller runtime:
$ go get sigs.k8s.io/[email protected]
Update dependencies:
$ go mod tidy
Next: define a resource with:
$ kubebuilder create api

在當前目錄下新增以下內容,可見這是個標準的go module工程:

.
├── Dockerfile
├── Makefile                                                    # 這裏定義了很多腳本命令,例如運行測試,開始執行等
├── PROJECT                                                     # 這裏是 kubebuilder 的一些元數據信息
├── config                                                      # config目錄下獲得啓動配置
│   ├── default                                           # 一些默認配置
│   │   ├── kustomization.yaml
│   │   ├── manager_auth_proxy_patch.yaml
│   │   └── manager_config_patch.yaml
│   ├── manager                                           # 部署 crd 所需的 yaml
│   │   ├── controller_manager_config.yaml
│   │   ├── kustomization.yaml
│   │   └── manager.yaml
│   ├── prometheus                                        # 監控指標數據採集配置
│   │   ├── kustomization.yaml
│   │   └── monitor.yaml
│   └── rbac                                              # 部署所需的 rbac 授權 yaml
│       ├── auth_proxy_client_clusterrole.yaml
│       ├── auth_proxy_role.yaml
│       ├── auth_proxy_role_binding.yaml
│       ├── auth_proxy_service.yaml
│       ├── kustomization.yaml
│       ├── leader_election_role.yaml
│       ├── leader_election_role_binding.yaml
│       ├── role_binding.yaml
│       └── service_account.yaml
├── go.mod                                                      # 一個新的Go模塊,與我們的項目相匹配,具有基本的依賴項
├── go.sum
├── hack                                                        # 腳本
│   └── boilerplate.go.txt
└── main.go

6 directories, 24 files

創建API(CRD和Controller)

# kubebuilder create api --group apps --version v1 --kind Application
Create Resource [y/n]               # 是否創建資源
y
Create Controller [y/n]             # 是否創建控制器
y

執行之後我們可以發現項目結構發生了一些變化

├── api
│   └── v1
│       ├── application_types.go                            # crd定義
│       ├── groupversion_info.go                            # 主要存放schema和我們的gvk的定義
│       └── zz_generated.deepcopy.go                        # 序列化和反序列化我們的crd資源。這個一般不需要關注
├── bin
│   └── controller-gen
├── config
│   ├── crd                                                      # 自動生成的 crd 文件,不用修改這裏,只需要修改了 v1 中的 go 文件之後執行 make generate 即可
│   │   ├── kustomization.yaml
│   │   ├── kustomizeconfig.yaml
│   │   └── patches
│   │       ├── cainjection_in_applications.yaml
│   │       └── webhook_in_applications.yaml
│   ├── rbac
│   │   ├── application_editor_role.yaml
│   │   ├── application_viewer_role.yaml
│   │   ├── auth_proxy_client_clusterrole.yaml
│   │   ├── auth_proxy_role.yaml
│   │   ├── auth_proxy_role_binding.yaml
│   │   ├── auth_proxy_service.yaml
│   │   ├── kustomization.yaml
│   │   ├── leader_election_role.yaml
│   │   ├── leader_election_role_binding.yaml
│   │   ├── role_binding.yaml
│   │   └── service_account.yaml
│   └── samples                                            # 這裏是 crd 示例文件,可以用來部署到集羣當中
│       └── apps_v1_application.yaml
├── controllers
│   ├── application_controller.go                          # 自定義控制器主要實現cr的reconcile方法,通過拿到cr資源信息,我們真正業務邏輯存放的地方
│   └── suite_test.go                                      # 測試組件
    
13 directories, 37 files

構建和部署CRD

kubebuilder提供的Makefile將構建和部署工作大幅度簡化,執行以下命令會將最新構建的CRD部署在kubernetes上

#  make install  
......
customresourcedefinition.apiextensions.k8s.io/applications.apps.zise.feizhu created

編譯和運行controller

以體驗基本流程爲主,不深入研究源碼,所以對代碼僅做少量修改,用於驗證是否能生效
main.go

if err = (&controllers.ApplicationReconciler{
		Client: mgr.GetClient(),
		Log:    ctrl.Log.WithName("controllers").WithName("Application"),                   // add this line
		Scheme: mgr.GetScheme(),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "unable to create controller", "controller", "Application")
		os.Exit(1)
	}

./controllers/application_controller.go


type ApplicationReconciler struct {
	client.Client
	Log    logr.Logger
	Scheme *runtime.Scheme
}
func (r *ApplicationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = log.FromContext(ctx)
	_ = r.Log.WithValues("application",req.NamespacedName)
	// your logic here
	r.Log.Info("1. %v",req)  				// 打印入參
	r.Log.Info("2. %s",debug.Stack())		// 打印堆棧

	return ctrl.Result{}, nil
}
...

執行以下命令,會編譯並啓動剛纔修改的controller:

# make run
/Users/zisefeizhu/linkun/goproject/kubedev/bin/controller-gen "crd:trivialVersions=true,preserveUnknownFields=false" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
main.go:35:2: package kubedev/controllers imports github.com/go-logr/logr from implicitly required module; to add missing requirements, run:
        go get github.com/go-logr/[email protected]
Error: not all generators ran successfully
run `controller-gen crd:trivialVersions=true,preserveUnknownFields=false rbac:roleName=manager-role webhook paths=./... output:crd:artifacts:config=config/crd/bases -w` to see all available markers, or `controller-gen crd:trivialVersions=true,preserveUnknownFields=false rbac:roleName=manager-role webhook paths=./... output:crd:artifacts:config=config/crd/bases -h` for usage
make: *** [manifests] Error 1
#  go get github.com/go-logr/[email protected]
# make run

創建Application資源的實例

現在kubernetes已經部署了Application類型的CRD,而且對應的controller也已正在運行中,可以嘗試創建Application類型的實例了(相當於有了pod的定義後,纔可以創建pod);

kubebuilder已經自動創建了一個類型的部署文件 ./config/samples/apps_v1_application.yaml ,內容如下,很簡單,接下來就用這個文件來創建Application實例:

apiVersion: apps.zise.feizhu/v1
kind: Application
metadata:
  name: application-sample
spec:
  # Add fields here
  foo: bar
執行 apply, 去controller所在控制檯,可以看到新增和修改的操作都有日誌輸出,新增的日誌都在裏面,代碼調用棧一目瞭然
make run            
/Users/zisefeizhu/linkun/goproject/kubedev/bin/controller-gen "crd:trivialVersions=true,preserveUnknownFields=false" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
/Users/zisefeizhu/linkun/goproject/kubedev/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
go vet ./...
go run ./main.go
I0210 15:26:27.993087   31662 request.go:655] Throttling request took 1.031351708s, request: GET:https://192.168.101.45:6443/apis/storage.k8s.io/v1beta1?timeout=32s
2022-02-10T15:26:29.262+0800    INFO    controller-runtime.metrics      metrics server is starting to listen    {"addr": ":8080"}
2022-02-10T15:26:29.263+0800    INFO    setup   starting manager
2022-02-10T15:26:29.263+0800    INFO    controller-runtime.manager      starting metrics server {"path": "/metrics"}
2022-02-10T15:26:29.263+0800    INFO    controller-runtime.manager.controller.application       Starting EventSource    {"reconciler group": "apps.zise.feizhu", "reconciler kind": "Application", "source": "kind source: /, Kind="}
2022-02-10T15:26:29.368+0800    INFO    controller-runtime.manager.controller.application       Starting Controller     {"reconciler group": "apps.zise.feizhu", "reconciler kind": "Application"}
2022-02-10T15:26:29.368+0800    INFO    controller-runtime.manager.controller.application       Starting workers        {"reconciler group": "apps.zise.feizhu", "reconciler kind": "Application", "worker count": 1}
2022-02-10T15:26:36.742+0800    INFO    controllers.Application 1. default/application-sample
2022-02-10T15:26:36.743+0800    INFO    controllers.Application 2. goroutine 420 [running]:
runtime/debug.Stack()
/Users/zisefeizhu/go/go1.17.6/src/runtime/debug/stack.go:24 +0x88
kubedev/controllers.(*ApplicationReconciler).Reconcile(0x140007443c0, {0x10399f3d8, 0x140006c5680}, {{{0x140003ac869, 0x7}, {0x1400071cc30, 0x12}}})
/Users/zisefeizhu/linkun/goproject/kubedev/controllers/application_controller.go:58 +0x184
sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller).reconcileHandler(0x1400055e140, {0x10399f330, 0x14000632600}, {0x10383dfc0, 0x14000443f20})
/Users/zisefeizhu/go/pkg/mod/sigs.k8s.io/[email protected]/pkg/internal/controller/controller.go:298 +0x2ac
sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller).processNextWorkItem(0x1400055e140, {0x10399f330, 0x14000632600})
/Users/zisefeizhu/go/pkg/mod/sigs.k8s.io/[email protected]/pkg/internal/controller/controller.go:253 +0x1d8
sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller).Start.func1.2({0x10399f330, 0x14000632600})
/Users/zisefeizhu/go/pkg/mod/sigs.k8s.io/[email protected]/pkg/internal/controller/controller.go:216 +0x44
k8s.io/apimachinery/pkg/util/wait.JitterUntilWithContext.func1()
/Users/zisefeizhu/go/pkg/mod/k8s.io/[email protected]/pkg/util/wait/wait.go:185 +0x38
k8s.io/apimachinery/pkg/util/wait.BackoffUntil.func1(0x14000025748)
/Users/zisefeizhu/go/pkg/mod/k8s.io/[email protected]/pkg/util/wait/wait.go:155 +0x68
k8s.io/apimachinery/pkg/util/wait.BackoffUntil(0x1400052ff48, {0x10397a2e0, 0x140005585a0}, 0x1, 0x14000026300)
/Users/zisefeizhu/go/pkg/mod/k8s.io/[email protected]/pkg/util/wait/wait.go:156 +0x94
k8s.io/apimachinery/pkg/util/wait.JitterUntil(0x14000025748, 0x3b9aca00, 0x0, 0x1, 0x14000026300)
/Users/zisefeizhu/go/pkg/mod/k8s.io/[email protected]/pkg/util/wait/wait.go:133 +0x88
k8s.io/apimachinery/pkg/util/wait.JitterUntilWithContext({0x10399f330, 0x14000632600}, 0x14000720140, 0x3b9aca00, 0x0, 0x1)
/Users/zisefeizhu/go/pkg/mod/k8s.io/[email protected]/pkg/util/wait/wait.go:185 +0x88
k8s.io/apimachinery/pkg/util/wait.UntilWithContext({0x10399f330, 0x14000632600}, 0x14000720140, 0x3b9aca00)
/Users/zisefeizhu/go/pkg/mod/k8s.io/[email protected]/pkg/util/wait/wait.go:99 +0x50
created by sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller).Start.func1
/Users/zisefeizhu/go/pkg/mod/sigs.k8s.io/[email protected]/pkg/internal/controller/controller.go:213 +0x308

刪除實例並停止controller

不再需要Application實例的時候,執行以下命令即可刪除:
# ctl delete -f config/samples/
不再需要controller的時候,去它的控制檯使用Ctrl+c中斷即可

不過實際生產環境中controller一般都會運行在kubernetes環境內,像上面這種運行在kubernetes之外的方式就不合適了,來試試將其做成docker鏡像然後在kubernetes環境運行

將controller製作成docker鏡像

執行以下命令構建docker鏡像並推送到aliyun,鏡像名爲

# docker login --username=zisefeizhu registry.cn-shenzhen.aliyuncs.com
Password: 
Login Succeeded
# docker build -t registry.cn-shenzhen.aliyuncs.com/zisefeizhu-xm/application:v001 .
# docker push registry.cn-shenzhen.aliyuncs.com/zisefeizhu-xm/application:v001

在kubernetes環境部署controller

# make deploy IMG=registry.cn-shenzhen.aliyuncs.com/zisefeizhu-xm/application:v001
/Users/zisefeizhu/linkun/goproject/kubedev/bin/controller-gen "crd:trivialVersions=true,preserveUnknownFields=false" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
cd config/manager && /Users/zisefeizhu/linkun/goproject/kubedev/bin/kustomize edit set image controller=registry.cn-shenzhen.aliyuncs.com/zisefeizhu-xm/application:v001
/Users/zisefeizhu/linkun/goproject/kubedev/bin/kustomize build config/default | kubectl apply -f -
namespace/kubedev-system created
customresourcedefinition.apiextensions.k8s.io/applications.apps.zise.feizhu configured
serviceaccount/kubedev-controller-manager created
role.rbac.authorization.k8s.io/kubedev-leader-election-role created
clusterrole.rbac.authorization.k8s.io/kubedev-manager-role created
clusterrole.rbac.authorization.k8s.io/kubedev-metrics-reader created
clusterrole.rbac.authorization.k8s.io/kubedev-proxy-role created
rolebinding.rbac.authorization.k8s.io/kubedev-leader-election-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/kubedev-manager-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/kubedev-proxy-rolebinding created
configmap/kubedev-manager-config created
service/kubedev-controller-manager-metrics-service created
deployment.apps/kubedev-controller-manager created

# kubectl get pods -n kubedev-system -w
NAME                                          READY   STATUS              RESTARTS   AGE
kubedev-controller-manager-776899d98c-9xdfg   2/2     Running   0          9s

將kube-rbac-proxy 改爲阿里雲鏡像地址

卸載和清理

把前面創建的資源和CRD全部清理掉,可以執行以下命令

# make uninstall

簡單實現 Controller

定義 CR

api/v1/application_types.go

	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	// Product 該應用所屬的產品
	Product string `json:"product,omitempty"`
}

修改之後執行一下 make manifests generate 可以發現已經生成了相關的字段,並且代碼中的字段註釋也就是 yaml 文件中的註釋

# make manifests generate
/Users/zisefeizhu/linkun/goproject/kubedev/bin/controller-gen "crd:trivialVersions=true,preserveUnknownFields=false" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
/Users/zisefeizhu/linkun/goproject/kubedev/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."

config/crd/bases/apps.lailin.xyz_applications.yaml

			properties:
              product:
                description: Product 該應用所屬的產品
                type: string

實現 controller

kubebuilder 已經實現了 Operator 所需的大部分邏輯,所以只需要在 Reconcile 中實現業務邏輯就行了

./controllers/application_controller.go

func (r *ApplicationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = log.FromContext(ctx)
	_ = r.Log.WithValues("application", req.NamespacedName)
	// your logic here
	r.Log.Info("app changed", "ns", req.Namespace)
	//r.Log.Info(fmt.Sprintf("1. %v", req))           // 打印入參
	//r.Log.Info(fmt.Sprintf("2. %s", debug.Stack())) // 打印堆棧

	return ctrl.Result{}, nil
}

邏輯修改好之後,先執行 make install 安裝 CRD,然後執行 make run運行 controller

make run
/Users/zisefeizhu/linkun/goproject/kubedev/bin/controller-gen "crd:trivialVersions=true,preserveUnknownFields=false" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
/Users/zisefeizhu/linkun/goproject/kubedev/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
go vet ./...
go run ./main.go
I0210 16:23:51.408872   35770 request.go:655] Throttling request took 1.029677459s, request: GET:https://192.168.101.45:6443/apis/storage.k8s.io/v1beta1?timeout=32s
2022-02-10T16:23:52.683+0800    INFO    controller-runtime.metrics      metrics server is starting to listen    {"addr": ":8080"}
2022-02-10T16:23:52.684+0800    INFO    setup   starting manager
2022-02-10T16:23:52.684+0800    INFO    controller-runtime.manager      starting metrics server {"path": "/metrics"}
2022-02-10T16:23:52.684+0800    INFO    controller-runtime.manager.controller.application       Starting EventSource    {"reconciler group": "apps.zise.feizhu", "reconciler kind": "Application", "source": "kind source: /, Kind="}
2022-02-10T16:23:52.786+0800    INFO    controller-runtime.manager.controller.application       Starting Controller     {"reconciler group": "apps.zise.feizhu", "reconciler kind": "Application"}
2022-02-10T16:23:52.786+0800    INFO    controller-runtime.manager.controller.application       Starting workers        {"reconciler group": "apps.zise.feizhu", "reconciler kind": "Application", "worker count": 1}

部署一個測試的 crd kubectl apply -f config/samples/apps_v1_application.yaml

apiVersion: apps.lailin.xyz/v1
kind: Application
metadata:
  name: application-sample
spec:
  # Add fields here
  product: test

然後可以看到之前寫的日誌邏輯已經觸發

022-02-10T16:25:27.946+0800    INFO    controllers.Application app changed     {"ns": "default"}

Kubebuilder 註釋

在生成的代碼當中我們可以看到很多 //+kubebuilder:xxx 開頭的註釋,對 Go 比較熟悉的同學應該知道這些註釋是給對應的代碼生成器服務的,在 Go 中有一個比較常用的套路就是利用 go gennerate生成對應的 go 代碼。

kubebuilder 使用 controller-gen(https://github.com/kubernetes-sigs/controller-tools/blob/master/cmd/controller-gen/main.go)生成代碼和對應的 yaml 文件,這其中主要包含 CRD 生成、驗證、處理還有 WebHook 的 RBAC 的生成功能,下面我簡單介紹一下,完整版可以看 kubebuilder 的官方文檔(https://master.book.kubebuilder.io/quick-start.html)

CRD 生成

//+kubebuilder:subresource:status 開啓 status 子資源,添加這個註釋之後就可以對 status進行更新操作了

//+groupName=nodes.lailin.xyz 指定 groupname

//+kubebuilder:printcolumn 爲 kubectl get xxx 添加一列,這個挺有用的

……

CRD 驗證,利用這個功能,只需要添加一些註釋,就給可以完成大部分需要校驗的功能

//+kubebuilder:default:= 給字段設置默認值

//+kubebuilder:validation:Pattern:=string 使用正則驗證字段

……

Webhook

//+kubebuilder:webhook 用於指定 webhook 如何生成,例如我們可以指定只監聽 Update 事件的 webhook

RBAC 用於生成 rbac 的權限

//+kubebuilder:rbac

參考文檔:

https://lailin.xyz/post/operator-01-overview.html
https://blog.csdn.net/boling_cavalry/article/details/113089414

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章