云原生小技巧 : 如何在本地調(diào)試 Kubernetes Webhook?
如果你是一名 Kubernetes Operator 的開(kāi)發(fā)者,你曾經(jīng)是否面臨過(guò)這樣一個(gè)棘手的問(wèn)題:如何在本地環(huán)境中高效地調(diào)試 Webhook,尤其是在涉及有效證書(shū)回調(diào)的情況下。這篇文章旨在提供一種清晰的指南,幫助你克服這一挑戰(zhàn),優(yōu)化本地開(kāi)發(fā)和測(cè)試流程。
為什么本地調(diào)試 Webhook 如此重要?
當(dāng)我們初步涉足 Kubernetes Webhook 時(shí),面對(duì)的首個(gè)挑戰(zhàn)通常是 Validation Webhook。對(duì)于這種驗(yàn)證型 Webhook 來(lái)說(shuō),我們可以通過(guò)編寫(xiě)自動(dòng)化測(cè)試來(lái)驗(yàn)證其功能。
這不僅確保了我的 Webhook 按預(yù)期工作,還允許我在日常開(kāi)發(fā)中臨時(shí)禁用它,從而加快了整個(gè)開(kāi)發(fā)過(guò)程。這種方法讓我能夠巧妙地避免復(fù)雜的調(diào)試問(wèn)題,而不對(duì)整體功能造成任何影響。
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
if err = (&webappv1.Guestbook{}).SetupWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "Guestbook")
os.Exit(1)
}
}
然而,對(duì)于 Mutating Webhook 來(lái)說(shuō),情況就變得有點(diǎn)復(fù)雜了。這類(lèi) Webhook 通常負(fù)責(zé)埋點(diǎn)的行為甚至更深層次的集群操作,比如注入 sidecar,這時(shí)候單靠自動(dòng)化測(cè)試顯然是不夠的。我們需要一個(gè)更加高效的本地測(cè)試和調(diào)試方法。
在我們團(tuán)隊(duì)中,有同事采用 Kind 來(lái)部署和測(cè)試服務(wù),這種方法非常值得稱贊。它完全符合 K8s 的操作模式,為我們提供了一個(gè)接近生產(chǎn)環(huán)境的本地測(cè)試平臺(tái)。但是,大家可能也注意到了,這種方式存在一個(gè)效率瓶頸:每次進(jìn)行代碼更改后,都需要重新構(gòu)建 Docker 鏡像并部署到集群中,這一過(guò)程既耗時(shí)又影響開(kāi)發(fā)流程的連貫性。
作為開(kāi)發(fā)工程師,我們渴望的是一個(gè)極速的內(nèi)部開(kāi)發(fā)循環(huán),一個(gè)不再需要頻繁的 docker build、docker push 或繁瑣的部署流程,即使這些已經(jīng)完全自動(dòng)化。
我們希望能夠使用本地熟悉的開(kāi)發(fā)工具,如 VS Code 或者 IntelliJ IDEA 進(jìn)行本地調(diào)試,而不是先部署到集群環(huán)境,再通過(guò)日志來(lái)分析錯(cuò)誤這種遠(yuǎn)程調(diào)試模式。
從 Service 到 URL 的魔法變換
在不禁用 Webhook 的情況下,我們?cè)诒镜貑?dòng) controller 后會(huì)有如下錯(cuò)誤。這個(gè)比較好處理,我們只要使用自簽證書(shū),注入到 WebhookServer 即可,在前面的文章中我介紹過(guò)很多次,這里不再贅述。
2023-11-26T12:55:17+08:00 INFO Stopping and waiting for webhooks
...
2023-11-26T12:55:17+08:00 INFO Wait completed, proceeding to shutdown the manager
2023-11-26T12:55:17+08:00 ERROR setup problem running manager
{"error": "open /var/folders/hn/v2s5bx...00000gn/T/k8s-webhook-server/serving-certs/tls.crt:
no such file or directory"}
...
我們來(lái)運(yùn)行一個(gè)示例,想必下面這個(gè)錯(cuò)誤大家都非常熟悉吧,這個(gè)是因?yàn)?Webhook 注冊(cè)的地址'不對(duì)',它是集群內(nèi)的地址。
? kubectl apply -f ./config/samples
Error from server (InternalError): error when creating "config/samples/webapp_v1_guestbook.yaml":
Internal error occurred: failed calling webhook "vguestbook.kb.io": failed to call webhook:
Post "https://testing-webhooks-webhook-service.testing-webhooks-system.svc:443/validate-webapp-foobar-ai-v1-guestbook?timeout=10s":
no endpoints available for service "testing-webhooks-webhook-service"
我們?cè)賮?lái)看下 ValidatingWebhookConfiguration 的配置。
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: testing-webhooks-validating-webhook-configuration
webhooks:
- admissionReviewVersions:
- v1
clientConfig:
service:
name: testing-webhooks-webhook-service
namespace: testing-webhooks-system
path: /validate-webapp-foobar-ai-v1-guestbook
port: 443
failurePolicy: Fail
matchPolicy: Equivalent
name: vguestbook.kb.io
rules:
- ...
...
在這個(gè)配置中,webhooks 字段定義了一個(gè)或多個(gè)要注冊(cè)的 Webhook。每個(gè) Webhook 通過(guò) clientConfig 配置與 Kubernetes API 服務(wù)器的連接方式。
正如大家所看到的 https://testing-webhooks-webhook-service.testing-webhooks-system.svc:443/validate-webapp-foobar-ai-v1-guestbook,這個(gè)默認(rèn)地址其實(shí)就是 K8s 集群內(nèi)部的地址。這恰是 K8s 中處理 Webhook 的常規(guī)方法,其中 service 字段指向集群內(nèi)運(yùn)行的特定服務(wù)。
然而,在本地開(kāi)發(fā)環(huán)境中,我們只在本地運(yùn)行了我們的 Operator,直接使用內(nèi)部服務(wù)是不大可能的,因?yàn)樗?Webhook 服務(wù)必須部署在 K8s 集群中。
使用 kubectl explain 探索 Webhook 配置
當(dāng)我們?cè)?K8s 中配置 Webhook 時(shí),了解其配置細(xì)節(jié)是非常重要的。kubectl explain 是一個(gè)非常實(shí)用的小工具,它可以幫助我們深入理解 K8s 資源的各個(gè)屬性。
以 ValidatingWebhookConfiguration.webhooks.clientConfig 為例:
? kubectl explain ValidatingWebhookConfiguration.webhooks.clientConfig
GROUP: admissionregistration.k8s.io
KIND: ValidatingWebhookConfiguration
VERSION: v1
FIELD: clientConfig <WebhookClientConfig>:
DESCRIPTION:
FIELDS:
caBundle <string>
`caBundle` is a PEM encoded CA bundle which will be used to validate the
webhook's server certificate. If unspecified, system trust roots on the
apiserver are used.
service <ServiceReference>
`service` is a reference to the service for this webhook. Either `service`
or `url` must be specified.
If the webhook is running within the cluster, then you should use `service`.
url <string>
`url` gives the location of the webhook, in standard URL form
(`scheme://host:port/path`). Exactly one of `url` or `service` must be
specified.
通過(guò)以上提供的詳細(xì)信息,不難發(fā)現(xiàn) clientConfig 它除了通過(guò)定義 Service 讓 API 服務(wù)器連接到 WebhookServer 外,還有另外一種方式,那就是直接通過(guò) URL 連接。
為了解決這個(gè)問(wèn)題,我們可以將 Webhook 的配置從服務(wù)轉(zhuǎn)變?yōu)橹苯邮褂?URL。
使用 URL 連接 Webhook
通過(guò)將 clientConfig 中的 service 字段替換為 url 字段,我們可以指定 Webhook 服務(wù)的外部 URL。這樣一來(lái),開(kāi)發(fā)者可以在本地運(yùn)行 Webhook 服務(wù),并通過(guò)公開(kāi)的 URL 使其可被 Kubernetes API 服務(wù)器訪問(wèn)。
例如:
webhooks:
- admissionReviewVersions:
- v1
clientConfig:
url: https://testing-webhooks.loca.lt/validate-webapp-foobar-ai-v1-guestbook
這種方法使得在本地開(kāi)發(fā)環(huán)境中調(diào)試 Webhook 變得更加靈活和便捷。開(kāi)發(fā)者可以使用本地服務(wù)器或通過(guò)隧道(如 ngrok[1] 或 localtunnel[2])暴露的服務(wù),從而實(shí)現(xiàn)在本地環(huán)境中的有效調(diào)試。
事實(shí)上,我最初的首選是 ngrok,因?yàn)檫@玩意確實(shí)好用,它還有個(gè) localhost:4040 非常的實(shí)用,但遺憾的是,它的 tls 能力是付費(fèi)的。幸好,有很多平替工具可以選擇,比如 localtunnel,用起來(lái)也非常的方便。
步驟 1: 在我們的 main.go 需要接收一個(gè)證書(shū)路徑
...
var certDir string
flag.StringVar(&certDir, "webhook-cert-dir", "/tmp/k8s-webhook-server/serving-certs", "Admission webhook cert/key dir.")
...
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
Metrics: metricsserver.Options{BindAddress: metricsAddr},
HealthProbeBindAddress: probeAddr,
WebhookServer: webhook.NewServer(webhook.Options{
CertDir: certDir,
Port: 9443,
}),
LeaderElection: enableLeaderElection,
LeaderElectionID: "dcc993a0.foobar.ai",
})
...
步驟 2:調(diào)整 Makefile,并啟動(dòng)我們的程序
修改 Makefile 文件,讓其可以接收證書(shū)目錄:
.PHONY: run
run: manifests generate fmt vet ## Run a controller from your host.
go run ./cmd/main.go --webhook-cert-dir ./config/certs
啟動(dòng)程序:
? make run
...
go run ./cmd/main.go --webhook-cert-dir ./config/certs
2023-11-26T11:18:42+08:00 INFO controller-runtime.builder Registering a mutating webhook {"GVK": "webapp.foobar.ai/v1, Kind=Guestbook", "path": "/mutate-webapp-foobar-ai-v1-guestbook"}
2023-11-26T11:18:42+08:00 INFO controller-runtime.webhook Registering webhook {"path": "/mutate-webapp-foobar-ai-v1-guestbook"}
2023-11-26T11:18:42+08:00 INFO controller-runtime.builder Registering a validating webhook {"GVK": "webapp.foobar.ai/v1, Kind=Guestbook", "path": "/validate-webapp-foobar-ai-v1-guestbook"}
2023-11-26T11:18:42+08:00 INFO controller-runtime.webhook Registering webhook {"path": "/validate-webapp-foobar-ai-v1-guestbook"}
...
2023-11-26T11:18:42+08:00 INFO controller-runtime.webhook Starting webhook server
...
2023-11-26T11:18:42+08:00 INFO controller-runtime.webhook Serving webhook server {"host": "", "port": 9443}
...
步驟 3:將本地主機(jī)服務(wù)器通過(guò)隧道公開(kāi)
# 安裝
npm install -g localtunnel
# 使用 lt 命令啟動(dòng)隧道
lt --port 9443 \
--local-https \
--local-ca $(pwd)/certs/ca.crt \
--local-cert $(pwd)/certs/tls.crt \
--local-key $(pwd)/certs/tls.key \
--subdomain testing-webhooks
your url is: https://testing-webhooks.loca.lt
步驟 4:修改 ValidatingWebhookConfiguration 配置
我們將默認(rèn)的 service 服務(wù)。
webhooks:
- admissionReviewVersions:
- v1
clientConfig:
service:
name: testing-webhooks-webhook-service
namespace: testing-webhooks-system
path: /validate-webapp-foobar-ai-v1-guestbook
port: 443
...
替換換成 url 直連模式
webhooks:
- admissionReviewVersions:
- v1
clientConfig:
url: https://testing-webhooks.loca.lt/validate-webapp-foobar-ai-v1-guestbook
...
以上僅以 ValidatingWebhookConfiguration 為例,如果你的 controller 同時(shí)使用了 MutatingWebhookConfiguration,別忘了,處理方式是一樣的。
步驟 5:看看實(shí)際效果如何
最后,我們?cè)賵?zhí)行同樣一個(gè)用例,就可以被當(dāng)前的 ValidatingWebhook 攔截到了。
? kubectl apply -f ./config/samples/webapp_v1_guestbook.yaml
The Guestbook "guestbook-sample" is invalid: metadata.name: Invalid value: "guestbook-sample":
Guestbook name must be no more than 5 characters for test purposes
?? 小貼士:對(duì)于那些出于安全考慮不愿將本地服務(wù)暴露在公網(wǎng)上的小伙伴們,這里有一個(gè)安全的替代方案。你可以使用如 docker.for.mac.host.internal 這樣的特定域名,它允許在不同環(huán)境下安全地連接到你的主機(jī)。
為了實(shí)現(xiàn)這一點(diǎn),你需要根據(jù)你所在的環(huán)境,將 docker.for.mac.host.internal 替換為能夠訪問(wèn)到你本地主機(jī)的相應(yīng)域名。此外,別忘了補(bǔ)充 caBundle 字段,確保使用了正確的 CA 證書(shū)的 base64 編碼字符串。
示例配置如下所示:
webhooks:
- admissionReviewVersions:
- v1
clientConfig:
caBundle: [你的CA證書(shū)的base64編碼字符串]
url: https://docker.for.mac.host.internal:9443/validate-webapp-foobar-ai-v1-guestbook
當(dāng)然,在實(shí)際開(kāi)發(fā)中,手動(dòng)更改 Webhook 配置顯然不是理想選擇。推薦大家根據(jù)項(xiàng)目需求,結(jié)合這里提供的策略,開(kāi)發(fā)自動(dòng)化的配置處理流程。這不僅提升效率,還確保了配置的準(zhǔn)確性和項(xiàng)目的靈活性。
Debugging operator on Kubernetes
在以前的舊文中,我分享過(guò) 《Kubernetes 101: Debugging Microservices on Kubernetes》 這篇文章,主要介紹微服務(wù)在 K8s 環(huán)境下,在本地如何進(jìn)行有效的調(diào)試。事實(shí)上,我們當(dāng)前 webhook 的調(diào)試場(chǎng)景,完全可以利用 Nocalhost[3] 這款工具達(dá)到同樣的目的。
圖片
在 Nocalhost 的開(kāi)發(fā)模式下,我們可以直接在 K8s 集群中構(gòu)建、測(cè)試和調(diào)試應(yīng)用程序的,這意味著我們可以默認(rèn)使用 clientConfig.service 模式,直接通過(guò)內(nèi)部服務(wù)來(lái)連接,非常的方便。如果你還不太熟悉 Nocalhost,那可得抓緊時(shí)間補(bǔ)課了 ??
至于證書(shū),我們自然是選擇 cert-manager 來(lái)管理 admission webhooks 證書(shū)了,畢竟我們已經(jīng)在集群里運(yùn)行了,沒(méi)有比它更方便的了。
我在 github 上寫(xiě)了個(gè) chart,感興趣的可以了解下:https://github.com/lqshow/testing-webhooks
寫(xiě)在最后
在本文中,我們深入了解了 Kubernetes Webhook 的本地調(diào)試流程,從基礎(chǔ)理解到實(shí)際操作。通過(guò)將傳統(tǒng)的基于服務(wù)的配置方式轉(zhuǎn)變?yōu)橹苯邮褂?URL,我們不僅克服了本地調(diào)試的局限性,還引入了前所未有的靈活性和效率,大幅優(yōu)化了整個(gè)開(kāi)發(fā)周期。
此外,我們還探討了如何在更廣泛的 Kubernetes 生態(tài)中應(yīng)用 Nocalhost 和 cert-manager。這些工具的整合不僅簡(jiǎn)化了開(kāi)發(fā)和調(diào)試過(guò)程,還強(qiáng)化了安全性和可擴(kuò)展性。通過(guò)實(shí)際案例,我們展示了如何在云原生環(huán)境中靈活地應(yīng)對(duì)挑戰(zhàn),從而提升整體開(kāi)發(fā)效率。