kubebuilder實戰之七:webhook

程式設計師欣宸發表於2021-09-02

歡迎訪問我的GitHub

https://github.com/zq2599/blog_demos

內容:所有原創文章分類彙總及配套原始碼,涉及Java、Docker、Kubernetes、DevOPS等;

本篇概覽

  • 本文是《kubebuilder實戰》系列的第七篇,之前的文章我們們完成了一個Operator的設計、開發、部署、驗證過程,為了讓整個過程保持簡潔並且篇幅不膨脹,實戰中刻意跳過了一個重要的知識點:webhook,如今是時候學習它了,這是個很重要的功能;
  • 本篇由以下部分構成:
  1. 介紹webhook;
  2. 結合前面的elasticweb專案,設計一個使用webhook的場景;
  3. 準備工作
  4. 生成webhook
  5. 開發(配置)
  6. 開發(編碼)
  7. 部署
  8. 驗證Defaulter(新增預設值)
  9. 驗證Validator(合法性校驗)

關於webhook

  • 熟悉java開發的讀者大多知道過濾器(Servlet Filter),如下圖,外部請求會先到達過濾器,做一些統一的操作,例如轉碼、校驗,然後才由真正的業務邏輯處理請求:

在這裡插入圖片描述

在這裡插入圖片描述

  • 再來看看webhook具體做了哪些事情,如下圖,kubernetes官方部落格明確指出webhook可以做兩件事:修改(mutating)和驗證(validating)
    在這裡插入圖片描述

  • kubebuilder為我們提供了生成webhook的基礎檔案和程式碼的工具,與製作API的工具類似,極大地簡化了工作量,我們們只需聚焦業務實現即可;

  • 基於kubebuilder製作的webhook和controller,如果是同一個資源,那麼它們在同一個程式中

設計實戰場景

  • 為了讓實戰有意義,我們們為前面的elasticweb專案上增加需求,讓webhook發揮實際作用;
  1. 如果使用者忘記輸入總QPS,系統webhook負責設定預設值1300,操作如下圖:

在這裡插入圖片描述

  1. 為了保護系統,給單個pod的QPS設定上限1000,如果外部輸入的singlePodQPS值超過1000,就建立資源物件失敗,如下圖所示:

在這裡插入圖片描述

原始碼下載

名稱 連結 備註
專案主頁 https://github.com/zq2599/blog_demos 該專案在GitHub上的主頁
git倉庫地址(https) https://github.com/zq2599/blog_demos.git 該專案原始碼的倉庫地址,https協議
git倉庫地址(ssh) git@github.com:zq2599/blog_demos.git 該專案原始碼的倉庫地址,ssh協議
  • 這個git專案中有多個資料夾,kubebuilder相關的應用在kubebuilder資料夾下,如下圖紅框所示:

在這裡插入圖片描述

  • kubebuilder資料夾下有多個子資料夾,本篇對應的原始碼在elasticweb目錄下,如下圖紅框所示:

在這裡插入圖片描述

準備工作

  • 和controller類似,webhook既能在kubernetes環境中執行,也能在kubernetes環境之外執行;
  • 如果webhook在kubernetes環境之外執行,是有些麻煩的,需要將證照放在所在環境,預設地址是:
/tmp/k8s-webhook-server/serving-certs/tls.{crt,key}
  • 為了省事兒,也為了更接近生產環境的用法,接下來的實戰的做法是將webhook部署在kubernetes環境中
  • 為了讓webhook在kubernetes環境中執行,我們們要做一點準備工作安裝cert manager,執行以下操作:
kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.2.0/cert-manager.yaml
  • 上述操作完成後會新建很多資源,如namespace、rbac、pod等,以pod為例如下:
[root@hedy ~]# kubectl get pods --all-namespaces
NAMESPACE        NAME                                       READY   STATUS    RESTARTS   AGE
cert-manager     cert-manager-6588898cb4-nvnz8              1/1     Running   1          5d14h
cert-manager     cert-manager-cainjector-7bcbdbd99f-q645r   1/1     Running   1          5d14h
cert-manager     cert-manager-webhook-5fd9f9dd86-98tm9      1/1     Running   1          5d14h
...
  • 操作完成後,準備工作結束,可以開始實戰了;

生成webhook

  • 進入elasticweb工程下,執行以下命令建立webhook:
kubebuilder create webhook \
--group elasticweb \
--version v1 \
--kind ElasticWeb \
--defaulting \
--programmatic-validation
  • 上述命令執行完畢後,先去看看main.go檔案,如下圖紅框1所示,自動增加了一段程式碼,作用是讓webhook生效:

在這裡插入圖片描述

  • 上圖紅框2中的elasticweb_webhook.go就是新增檔案,內容如下:
package v1

import (
	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/webhook"
)

// log is for logging in this package.
var elasticweblog = logf.Log.WithName("elasticweb-resource")

func (r *ElasticWeb) SetupWebhookWithManager(mgr ctrl.Manager) error {
	return ctrl.NewWebhookManagedBy(mgr).
		For(r).
		Complete()
}

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!

// +kubebuilder:webhook:path=/mutate-elasticweb-com-bolingcavalry-v1-elasticweb,mutating=true,failurePolicy=fail,groups=elasticweb.com.bolingcavalry,resources=elasticwebs,verbs=create;update,versions=v1,name=melasticweb.kb.io

var _ webhook.Defaulter = &ElasticWeb{}

// Default implements webhook.Defaulter so a webhook will be registered for the type
func (r *ElasticWeb) Default() {
	elasticweblog.Info("default", "name", r.Name)

	// TODO(user): fill in your defaulting logic.
}

// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
// +kubebuilder:webhook:verbs=create;update,path=/validate-elasticweb-com-bolingcavalry-v1-elasticweb,mutating=false,failurePolicy=fail,groups=elasticweb.com.bolingcavalry,resources=elasticwebs,versions=v1,name=velasticweb.kb.io

var _ webhook.Validator = &ElasticWeb{}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *ElasticWeb) ValidateCreate() error {
	elasticweblog.Info("validate create", "name", r.Name)

	// TODO(user): fill in your validation logic upon object creation.
	return nil
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *ElasticWeb) ValidateUpdate(old runtime.Object) error {
	elasticweblog.Info("validate update", "name", r.Name)

	// TODO(user): fill in your validation logic upon object update.
	return nil
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (r *ElasticWeb) ValidateDelete() error {
	elasticweblog.Info("validate delete", "name", r.Name)

	// TODO(user): fill in your validation logic upon object deletion.
	return nil
}
  • 上述程式碼有兩處需要注意,第一處和填寫預設值有關,如下圖:

在這裡插入圖片描述

  • 第二處和校驗有關,如下圖:

在這裡插入圖片描述

  • 我們們要實現的業務需求就是通過修改上述elasticweb_webhook.go的內容來實現,不過程式碼稍後再寫,先把配置都改好;

開發(配置)

  • 開啟檔案config/default/kustomization.yaml,下圖四個紅框中的內容原本都被註釋了,現在請將註釋符號都刪掉,使其生效:

在這裡插入圖片描述

  • 還是檔案config/default/kustomization.yaml,節點vars下面的內容,原本全部被註釋了,現在請全部放開,放開後的效果如下圖:

在這裡插入圖片描述

  • 配置已經完成,可以編碼了;

開發(編碼)

  • 開啟檔案elasticweb_webhook.go

  • 新增依賴:

apierrors "k8s.io/apimachinery/pkg/api/errors"
  • 找到Default方法,改成如下內容,可見程式碼很簡單,判斷TotalQPS是否存在,若不存在就寫入預設值,另外還加了兩行日誌:
func (r *ElasticWeb) Default() {
	elasticweblog.Info("default", "name", r.Name)

	// TODO(user): fill in your defaulting logic.
	// 如果建立的時候沒有輸入總QPS,就設定個預設值
	if r.Spec.TotalQPS == nil {
		r.Spec.TotalQPS = new(int32)
		*r.Spec.TotalQPS = 1300
		elasticweblog.Info("a. TotalQPS is nil, set default value now", "TotalQPS", *r.Spec.TotalQPS)
	} else {
		elasticweblog.Info("b. TotalQPS exists", "TotalQPS", *r.Spec.TotalQPS)
	}
}
  • 接下來開發校驗功能,我們們把校驗功能封裝成一個validateElasticWeb方法,然後在新增和修改的時候各呼叫一次,如下,可見最終是呼叫apierrors.NewInvalid生成錯誤例項的,而此方法接受的是多個錯誤,因此要為其準備切片做入參,當然了,如果是多個引數校驗失敗,可以都放入切片中:
func (r *ElasticWeb) validateElasticWeb() error {
	var allErrs field.ErrorList

	if *r.Spec.SinglePodQPS > 1000 {
		elasticweblog.Info("c. Invalid SinglePodQPS")

		err := field.Invalid(field.NewPath("spec").Child("singlePodQPS"),
			*r.Spec.SinglePodQPS,
			"d. must be less than 1000")

		allErrs = append(allErrs, err)

		return apierrors.NewInvalid(
			schema.GroupKind{Group: "elasticweb.com.bolingcavalry", Kind: "ElasticWeb"},
			r.Name,
			allErrs)
	} else {
		elasticweblog.Info("e. SinglePodQPS is valid")
		return nil
	}
}
  • 再找到新增和修改資源物件時被呼叫的方法,在裡面呼叫validateElasticWeb:
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *ElasticWeb) ValidateCreate() error {
	elasticweblog.Info("validate create", "name", r.Name)

	// TODO(user): fill in your validation logic upon object creation.

	return r.validateElasticWeb()
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *ElasticWeb) ValidateUpdate(old runtime.Object) error {
	elasticweblog.Info("validate update", "name", r.Name)

	// TODO(user): fill in your validation logic upon object update.
	return r.validateElasticWeb()
}
  • 編碼完成,可見非常簡單,接下來,我們們把以前實戰遺留的東西清理一下,再開始新的部署和驗證;

清理工作

  • 如果您是隨著《kubebuilder實戰》系列一路操作下來,此時系統上應該積攢了之前遺留的內容,可以通過以下步驟完成清理:
  1. 刪除elasticweb資源物件:
kubectl delete -f config/samples/elasticweb_v1_elasticweb.yaml
  1. 刪除controller
kustomize build config/default | kubectl delete -f -
  1. 刪除CRD
make uninstall
  • 現在萬事俱備,可以部署webhook了;

部署

  1. 部署CRD
make install
  1. 構建映象並推送到倉庫(我終於受夠了hub.docker.com的龜速,改為阿里雲映象倉庫):
make docker-build docker-push IMG=registry.cn-hangzhou.aliyuncs.com/bolingcavalry/elasticweb:001
  1. 部署整合了webhook功能的controller:
make deploy IMG=registry.cn-hangzhou.aliyuncs.com/bolingcavalry/elasticweb:001
  1. 檢視pod,確認啟動成功:
zhaoqin@zhaoqindeMBP-2 ~ % kubectl get pods --all-namespaces
NAMESPACE           NAME                                             READY   STATUS    RESTARTS   AGE
cert-manager        cert-manager-6588898cb4-nvnz8                    1/1     Running   1          5d21h
cert-manager        cert-manager-cainjector-7bcbdbd99f-q645r         1/1     Running   1          5d21h
cert-manager        cert-manager-webhook-5fd9f9dd86-98tm9            1/1     Running   1          5d21h
elasticweb-system   elasticweb-controller-manager-7dcbfd4675-898gb   2/2     Running   0          20s

驗證Defaulter(新增預設值)

  • 修改檔案config/samples/elasticweb_v1_elasticweb.yaml,修改後的內容如下,可見totalQPS欄位已經被註釋掉了:
apiVersion: v1
kind: Namespace
metadata:
  name: dev
  labels:
    name: dev
---
apiVersion: elasticweb.com.bolingcavalry/v1
kind: ElasticWeb
metadata:
  namespace: dev
  name: elasticweb-sample
spec:
  # Add fields here
  image: tomcat:8.0.18-jre8
  port: 30003
  singlePodQPS: 500
  # totalQPS: 600

  • 建立一個elasticweb資源物件:
kubectl apply -f config/samples/elasticweb_v1_elasticweb.yaml
  • 此時單個pod的QPS是500,如果webhook的程式碼生效的話,總QPS就是1300,而對應的pod數應該是3個,接下來我們們看看是否符合預期;
  • 先看elasticweb、deployment、pod等資源物件是否正常,如下所示,全部符合預期:
zhaoqin@zhaoqindeMBP-2 ~ % kubectl get elasticweb -n dev                                                                 
NAME                AGE
elasticweb-sample   89s
zhaoqin@zhaoqindeMBP-2 ~ % kubectl get deployments -n dev
NAME                READY   UP-TO-DATE   AVAILABLE   AGE
elasticweb-sample   3/3     3            3           98s
zhaoqin@zhaoqindeMBP-2 ~ % kubectl get service -n dev    
NAME                TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
elasticweb-sample   NodePort   10.105.125.125   <none>        8080:30003/TCP   106s
zhaoqin@zhaoqindeMBP-2 ~ % kubectl get pod -n dev    
NAME                                 READY   STATUS    RESTARTS   AGE
elasticweb-sample-56fc5848b7-5tkxw   1/1     Running   0          113s
elasticweb-sample-56fc5848b7-blkzg   1/1     Running   0          113s
elasticweb-sample-56fc5848b7-pd7jg   1/1     Running   0          113s
  • kubectl describe命令檢視elasticweb資源物件的詳情,如下所示,TotalQPS欄位被webhook設定為1300,RealQPS也計算正確:
zhaoqin@zhaoqindeMBP-2 ~ % kubectl describe elasticweb elasticweb-sample -n dev
Name:         elasticweb-sample
Namespace:    dev
Labels:       <none>
Annotations:  <none>
API Version:  elasticweb.com.bolingcavalry/v1
Kind:         ElasticWeb
Metadata:
  Creation Timestamp:  2021-02-27T16:07:34Z
  Generation:          2
  Managed Fields:
    API Version:  elasticweb.com.bolingcavalry/v1
    Fields Type:  FieldsV1
    fieldsV1:
      f:metadata:
        f:annotations:
          .:
          f:kubectl.kubernetes.io/last-applied-configuration:
      f:spec:
        .:
        f:image:
        f:port:
        f:singlePodQPS:
    Manager:      kubectl-client-side-apply
    Operation:    Update
    Time:         2021-02-27T16:07:34Z
    API Version:  elasticweb.com.bolingcavalry/v1
    Fields Type:  FieldsV1
    fieldsV1:
      f:status:
        f:realQPS:
    Manager:         manager
    Operation:       Update
    Time:            2021-02-27T16:07:34Z
  Resource Version:  687628
  UID:               703de111-d859-4cd2-b3c4-1d201fb7bd7d
Spec:
  Image:           tomcat:8.0.18-jre8
  Port:            30003
  Single Pod QPS:  500
  Total QPS:       1300
Status:
  Real QPS:  1500
Events:      <none>
  • 再來看看controller的日誌,其中的webhook部分是否符合預期,如下圖紅框所示,發現TotalQPS欄位為空,就將設定為預設值,並且在檢測的時候SinglePodQPS的值也沒有超過1000:

在這裡插入圖片描述

  • 最後別忘了用瀏覽器驗證web服務是否正常,我這裡的完整地址是:http://192.168.50.75:30003/
  • 至此,我們們完成了webhook的Defaulter驗證,接下來驗證Validator

驗證Validator

  • 接下來該驗證webhook的引數校驗功能了,先驗證修改時的邏輯;
  • 編輯檔案config/samples/update_single_pod_qps.yaml,值如下:
spec:
  singlePodQPS: 1100
  • 用patch命令使之生效:
kubectl patch elasticweb elasticweb-sample \
-n dev \
--type merge \
--patch "$(cat config/samples/update_single_pod_qps.yaml)"
  • 此時,控制檯會輸出錯誤資訊:
Error from server (ElasticWeb.elasticweb.com.bolingcavalry "elasticweb-sample" is invalid: spec.singlePodQPS: Invalid value: 1100: d. must be less than 1000): admission webhook "velasticweb.kb.io" denied the request: ElasticWeb.elasticweb.com.bolingcavalry "elasticweb-sample" is invalid: spec.singlePodQPS: Invalid value: 1100: d. must be less than 1000
  • 再用kubectl describe命令檢視elasticweb資源物件的詳情,如下圖紅框,依然是500,可見webhook已經生效,阻止了錯誤的發生:

在這裡插入圖片描述

  • 再去看controller日誌,如下圖紅框所示,和程式碼對應上了:

在這裡插入圖片描述

  • 接下來再試試webhook在新增時候的校驗功能;
  • 清理前面建立的elastic資源物件,執行命令:
kubectl delete -f config/samples/elasticweb_v1_elasticweb.yaml
  • 修改檔案,如下圖紅框所示,我們們將singlePodQPS的值改為超過1000,看看webhook是否能檢查到這個錯誤,並阻止資源物件的建立:

在這裡插入圖片描述

  • 執行以下命令開始建立elasticweb資源物件:
kubectl apply -f config/samples/elasticweb_v1_elasticweb.yaml
  • 控制檯提示以下資訊,包含了我們們程式碼中寫入的錯誤描述,證明elasticweb資源物件建立失敗,證明webhook的Validator功能已經生效:
namespace/dev created
Error from server (ElasticWeb.elasticweb.com.bolingcavalry "elasticweb-sample" is invalid: spec.singlePodQPS: Invalid value: 1500: d. must be less than 1000): error when creating "config/samples/elasticweb_v1_elasticweb.yaml": admission webhook "velasticweb.kb.io" denied the request: ElasticWeb.elasticweb.com.bolingcavalry "elasticweb-sample" is invalid: spec.singlePodQPS: Invalid value: 1500: d. must be less than 1000
  • 不放心的話執行kubectl get命令檢查一下,發現空空如也:
zhaoqin@zhaoqindeMBP-2 elasticweb % kubectl get elasticweb -n dev       
No resources found in dev namespace.
zhaoqin@zhaoqindeMBP-2 elasticweb % kubectl get deployments -n dev
No resources found in dev namespace.
zhaoqin@zhaoqindeMBP-2 elasticweb % kubectl get service -n dev
No resources found in dev namespace.
zhaoqin@zhaoqindeMBP-2 elasticweb % kubectl get pod -n dev
No resources found in dev namespace.
  • 還要看下controller日誌,如下圖紅框所示,符合預期:

在這裡插入圖片描述

  • 至此,operator的webhook的開發、部署、驗證我們們就完成了,整個elasticweb也算是基本功能齊全,希望能為您的operator開發提供參考;

你不孤單,欣宸原創一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 資料庫+中介軟體系列
  6. DevOps系列

歡迎關注公眾號:程式設計師欣宸

微信搜尋「程式設計師欣宸」,我是欣宸,期待與您一同暢遊Java世界...
https://github.com/zq2599/blog_demos

相關文章