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

生成webhook

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

  • 上圖紅框2中的elasticweb_webhook.go就是新增檔案,內容如下:
  1. package v1
  2. import (
  3. "k8s.io/apimachinery/pkg/runtime"
  4. ctrl "sigs.k8s.io/controller-runtime"
  5. logf "sigs.k8s.io/controller-runtime/pkg/log"
  6. "sigs.k8s.io/controller-runtime/pkg/webhook"
  7. )
  8. // log is for logging in this package.
  9. var elasticweblog = logf.Log.WithName("elasticweb-resource")
  10. func (r *ElasticWeb) SetupWebhookWithManager(mgr ctrl.Manager) error {
  11. return ctrl.NewWebhookManagedBy(mgr).
  12. For(r).
  13. Complete()
  14. }
  15. // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
  16. // +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
  17. var _ webhook.Defaulter = &ElasticWeb{}
  18. // Default implements webhook.Defaulter so a webhook will be registered for the type
  19. func (r *ElasticWeb) Default() {
  20. elasticweblog.Info("default", "name", r.Name)
  21. // TODO(user): fill in your defaulting logic.
  22. }
  23. // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
  24. // +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
  25. var _ webhook.Validator = &ElasticWeb{}
  26. // ValidateCreate implements webhook.Validator so a webhook will be registered for the type
  27. func (r *ElasticWeb) ValidateCreate() error {
  28. elasticweblog.Info("validate create", "name", r.Name)
  29. // TODO(user): fill in your validation logic upon object creation.
  30. return nil
  31. }
  32. // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
  33. func (r *ElasticWeb) ValidateUpdate(old runtime.Object) error {
  34. elasticweblog.Info("validate update", "name", r.Name)
  35. // TODO(user): fill in your validation logic upon object update.
  36. return nil
  37. }
  38. // ValidateDelete implements webhook.Validator so a webhook will be registered for the type
  39. func (r *ElasticWeb) ValidateDelete() error {
  40. elasticweblog.Info("validate delete", "name", r.Name)
  41. // TODO(user): fill in your validation logic upon object deletion.
  42. return nil
  43. }
  • 上述程式碼有兩處需要注意,第一處和填寫預設值有關,如下圖:

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

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

開發(配置)

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

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

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

開發(編碼)

  • 開啟檔案elasticweb_webhook.go

  • 新增依賴:

  1. apierrors "k8s.io/apimachinery/pkg/api/errors"
  • 找到Default方法,改成如下內容,可見程式碼很簡單,判斷TotalQPS是否存在,若不存在就寫入預設值,另外還加了兩行日誌:
  1. func (r *ElasticWeb) Default() {
  2. elasticweblog.Info("default", "name", r.Name)
  3. // TODO(user): fill in your defaulting logic.
  4. // 如果建立的時候沒有輸入總QPS,就設定個預設值
  5. if r.Spec.TotalQPS == nil {
  6. r.Spec.TotalQPS = new(int32)
  7. *r.Spec.TotalQPS = 1300
  8. elasticweblog.Info("a. TotalQPS is nil, set default value now", "TotalQPS", *r.Spec.TotalQPS)
  9. } else {
  10. elasticweblog.Info("b. TotalQPS exists", "TotalQPS", *r.Spec.TotalQPS)
  11. }
  12. }
  • 接下來開發校驗功能,咱們把校驗功能封裝成一個validateElasticWeb方法,然後在新增和修改的時候各呼叫一次,如下,可見最終是呼叫apierrors.NewInvalid生成錯誤例項的,而此方法接受的是多個錯誤,因此要為其準備切片做入參,當然了,如果是多個引數校驗失敗,可以都放入切片中:
  1. func (r *ElasticWeb) validateElasticWeb() error {
  2. var allErrs field.ErrorList
  3. if *r.Spec.SinglePodQPS > 1000 {
  4. elasticweblog.Info("c. Invalid SinglePodQPS")
  5. err := field.Invalid(field.NewPath("spec").Child("singlePodQPS"),
  6. *r.Spec.SinglePodQPS,
  7. "d. must be less than 1000")
  8. allErrs = append(allErrs, err)
  9. return apierrors.NewInvalid(
  10. schema.GroupKind{Group: "elasticweb.com.bolingcavalry", Kind: "ElasticWeb"},
  11. r.Name,
  12. allErrs)
  13. } else {
  14. elasticweblog.Info("e. SinglePodQPS is valid")
  15. return nil
  16. }
  17. }
  • 再找到新增和修改資源物件時被呼叫的方法,在裡面呼叫validateElasticWeb:
  1. // ValidateCreate implements webhook.Validator so a webhook will be registered for the type
  2. func (r *ElasticWeb) ValidateCreate() error {
  3. elasticweblog.Info("validate create", "name", r.Name)
  4. // TODO(user): fill in your validation logic upon object creation.
  5. return r.validateElasticWeb()
  6. }
  7. // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
  8. func (r *ElasticWeb) ValidateUpdate(old runtime.Object) error {
  9. elasticweblog.Info("validate update", "name", r.Name)
  10. // TODO(user): fill in your validation logic upon object update.
  11. return r.validateElasticWeb()
  12. }
  • 編碼完成,可見非常簡單,接下來,咱們把以前實戰遺留的東西清理一下,再開始新的部署和驗證;

清理工作

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

部署

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

驗證Defaulter(新增預設值)

  • 修改檔案config/samples/elasticweb_v1_elasticweb.yaml,修改後的內容如下,可見totalQPS欄位已經被註釋掉了:
  1. apiVersion: v1
  2. kind: Namespace
  3. metadata:
  4. name: dev
  5. labels:
  6. name: dev
  7. ---
  8. apiVersion: elasticweb.com.bolingcavalry/v1
  9. kind: ElasticWeb
  10. metadata:
  11. namespace: dev
  12. name: elasticweb-sample
  13. spec:
  14. # Add fields here
  15. image: tomcat:8.0.18-jre8
  16. port: 30003
  17. singlePodQPS: 500
  18. # totalQPS: 600
  • 建立一個elasticweb資源物件:
  1. kubectl apply -f config/samples/elasticweb_v1_elasticweb.yaml
  • 此時單個pod的QPS是500,如果webhook的程式碼生效的話,總QPS就是1300,而對應的pod數應該是3個,接下來咱們看看是否符合預期;
  • 先看elasticweb、deployment、pod等資源物件是否正常,如下所示,全部符合預期:
  1. zhaoqin@zhaoqindeMBP-2 ~ % kubectl get elasticweb -n dev
  2. NAME AGE
  3. elasticweb-sample 89s
  4. zhaoqin@zhaoqindeMBP-2 ~ % kubectl get deployments -n dev
  5. NAME READY UP-TO-DATE AVAILABLE AGE
  6. elasticweb-sample 3/3 3 3 98s
  7. zhaoqin@zhaoqindeMBP-2 ~ % kubectl get service -n dev
  8. NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
  9. elasticweb-sample NodePort 10.105.125.125 <none> 8080:30003/TCP 106s
  10. zhaoqin@zhaoqindeMBP-2 ~ % kubectl get pod -n dev
  11. NAME READY STATUS RESTARTS AGE
  12. elasticweb-sample-56fc5848b7-5tkxw 1/1 Running 0 113s
  13. elasticweb-sample-56fc5848b7-blkzg 1/1 Running 0 113s
  14. elasticweb-sample-56fc5848b7-pd7jg 1/1 Running 0 113s
  • 用kubectl describe命令檢視elasticweb資源物件的詳情,如下所示,TotalQPS欄位被webhook設定為1300,RealQPS也計算正確:
  1. zhaoqin@zhaoqindeMBP-2 ~ % kubectl describe elasticweb elasticweb-sample -n dev
  2. Name: elasticweb-sample
  3. Namespace: dev
  4. Labels: <none>
  5. Annotations: <none>
  6. API Version: elasticweb.com.bolingcavalry/v1
  7. Kind: ElasticWeb
  8. Metadata:
  9. Creation Timestamp: 2021-02-27T16:07:34Z
  10. Generation: 2
  11. Managed Fields:
  12. API Version: elasticweb.com.bolingcavalry/v1
  13. Fields Type: FieldsV1
  14. fieldsV1:
  15. f:metadata:
  16. f:annotations:
  17. .:
  18. f:kubectl.kubernetes.io/last-applied-configuration:
  19. f:spec:
  20. .:
  21. f:image:
  22. f:port:
  23. f:singlePodQPS:
  24. Manager: kubectl-client-side-apply
  25. Operation: Update
  26. Time: 2021-02-27T16:07:34Z
  27. API Version: elasticweb.com.bolingcavalry/v1
  28. Fields Type: FieldsV1
  29. fieldsV1:
  30. f:status:
  31. f:realQPS:
  32. Manager: manager
  33. Operation: Update
  34. Time: 2021-02-27T16:07:34Z
  35. Resource Version: 687628
  36. UID: 703de111-d859-4cd2-b3c4-1d201fb7bd7d
  37. Spec:
  38. Image: tomcat:8.0.18-jre8
  39. Port: 30003
  40. Single Pod QPS: 500
  41. Total QPS: 1300
  42. Status:
  43. Real QPS: 1500
  44. 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,值如下:
  1. spec:
  2. singlePodQPS: 1100
  • 用patch命令使之生效:
  1. kubectl patch elasticweb elasticweb-sample \
  2. -n dev \
  3. --type merge \
  4. --patch "$(cat config/samples/update_single_pod_qps.yaml)"
  • 此時,控制檯會輸出錯誤資訊:
  1. 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資源物件,執行命令:
  1. kubectl delete -f config/samples/elasticweb_v1_elasticweb.yaml
  • 修改檔案,如下圖紅框所示,咱們將singlePodQPS的值改為超過1000,看看webhook是否能檢查到這個錯誤,並阻止資源物件的建立:

  • 執行以下命令開始建立elasticweb資源物件:
  1. kubectl apply -f config/samples/elasticweb_v1_elasticweb.yaml
  • 控制檯提示以下資訊,包含了咱們程式碼中寫入的錯誤描述,證明elasticweb資源物件建立失敗,證明webhook的Validator功能已經生效:
  1. namespace/dev created
  2. 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命令檢查一下,發現空空如也:
  1. zhaoqin@zhaoqindeMBP-2 elasticweb % kubectl get elasticweb -n dev
  2. No resources found in dev namespace.
  3. zhaoqin@zhaoqindeMBP-2 elasticweb % kubectl get deployments -n dev
  4. No resources found in dev namespace.
  5. zhaoqin@zhaoqindeMBP-2 elasticweb % kubectl get service -n dev
  6. No resources found in dev namespace.
  7. zhaoqin@zhaoqindeMBP-2 elasticweb % kubectl get pod -n dev
  8. 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