好的,各位观众老爷们,欢迎来到今天的 Kubernetes Webhook 编程脱口秀! 🎤 我是你们的老朋友,一只在云端跳舞的程序员,今天咱们不聊什么高大上的架构,就来点接地气的,聊聊 Kubernetes Webhooks 那些让人又爱又恨的小妖精。
开场白:Webhook,你是我的朱砂痣,也是我的蚊子血
话说 Kubernetes 这个小伙子,那是越来越受欢迎,啥都想管,啥都想控制。可是呢,他也有力不从及的时候,这时候就需要 Webhooks 来帮忙了。
Webhook,这东西,说白了就是个钩子,能在 Kubernetes API 请求的不同阶段(创建、更新、删除等等)“钩”住请求,然后跑去问问你写的程序:“哥们,这请求靠谱吗?要不要改改?”。
你写得好,Webhook 就是你的神兵利器,能让 Kubernetes 更加智能,更加安全。你写得不好,Webhook 就是你的噩梦,分分钟把集群搞崩,让你怀疑人生。 😫
所以,Webhook 这东西,就像《红楼梦》里的朱砂痣和蚊子血,用好了是惊艳,用不好是糟心。今天,我们就来好好扒一扒 Webhook 的底裤,看看它到底是怎么玩的。
第一幕:Webhook 的前世今生
咱们先来了解一下 Webhook 的两种主要类型:
Webhook 类型 | 功能 | 适用场景 |
---|---|---|
Mutating Admission Webhook | 修改 API 请求 | 自动注入 sidecar 容器、修改资源默认值、强制标签等等 |
Validating Admission Webhook | 验证 API 请求 | 阻止不符合规范的资源创建、校验标签、检查镜像安全漏洞等等 |
简单来说,Mutating Webhook 就是个“美容师”,能给 API 请求“化妆”,让它更符合你的要求;Validating Webhook 就是个“保安”,能检查 API 请求是否“衣冠不整”,不合格的就直接拒之门外。
第二幕:手把手教你“调戏” Webhook
好了,光说不练假把式,咱们来点真格的,手把手教你写一个简单的 Mutating Webhook,给 Pod 自动注入一个 sidecar 容器。
1. 准备工作
- 一个 Kubernetes 集群(废话!)
- 一个可以暴露 HTTPS 服务的服务器(可以是你的电脑,也可以是云服务器)
- 一个域名(可选,但建议使用)
- Golang 开发环境 (必须的,因为我们这里用golang做例子)
2. 编写 Webhook 服务
首先,我们需要编写一个 Webhook 服务,它负责接收 Kubernetes 发来的请求,并返回修改后的资源。这里我们使用 Golang 来编写:
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
admissionv1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
)
var (
universalDeserializer = serializer.NewCodecFactory(runtime.NewScheme()).UniversalDeserializer()
)
func main() {
http.HandleFunc("/mutate", mutateHandler)
// 启动 HTTPS 服务
log.Fatal(http.ListenAndServeTLS(":443", "/path/to/tls.crt", "/path/to/tls.key", nil))
}
func mutateHandler(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading body: %v", err)
http.Error(w, "Error reading body", http.StatusBadRequest)
return
}
// 验证 Content-Type
contentType := r.Header.Get("Content-Type")
if contentType != "application/json" {
log.Printf("Content-Type was %s, expected application/json", contentType)
http.Error(w, "Invalid Content-Type, expected `application/json`", http.StatusUnsupportedMediaType)
return
}
// 反序列化 AdmissionReview
var admissionReview admissionv1.AdmissionReview
if _, _, err := universalDeserializer.Decode(body, nil, &admissionReview); err != nil {
log.Printf("Error decoding AdmissionReview: %v", err)
http.Error(w, "Error decoding AdmissionReview", http.StatusBadRequest)
return
}
// 处理 AdmissionReview
responseAdmissionReview := processAdmissionReview(admissionReview)
// 序列化 AdmissionReview
responseBytes, err := json.Marshal(responseAdmissionReview)
if err != nil {
log.Printf("Error marshaling AdmissionReview: %v", err)
http.Error(w, "Error marshaling AdmissionReview", http.StatusInternalServerError)
return
}
// 返回 AdmissionReview
w.Header().Set("Content-Type", "application/json")
if _, err := w.Write(responseBytes); err != nil {
log.Printf("Error writing response: %v", err)
http.Error(w, "Error writing response", http.StatusInternalServerError)
}
}
func processAdmissionReview(admissionReview admissionv1.AdmissionReview) admissionv1.AdmissionReview {
// 创建 AdmissionResponse
admissionResponse := admissionv1.AdmissionResponse{}
admissionResponse.UID = admissionReview.Request.UID
admissionResponse.Allowed = true
// 反序列化 Pod
var pod corev1.Pod
if err := json.Unmarshal(admissionReview.Request.Object.Raw, &pod); err != nil {
log.Printf("Error unmarshaling Pod: %v", err)
admissionResponse.Allowed = false
admissionResponse.Result = &metav1.Status{
Status: metav1.StatusFailure,
Message: err.Error(),
}
return admissionReview
}
// 修改 Pod
if pod.ObjectMeta.Annotations == nil {
pod.ObjectMeta.Annotations = make(map[string]string)
}
pod.ObjectMeta.Annotations["mutated-by"] = "my-webhook"
// 添加 sidecar 容器
sidecarContainer := corev1.Container{
Name: "my-sidecar",
Image: "busybox:latest",
Command: []string{"sleep", "3600"},
}
pod.Spec.Containers = append(pod.Spec.Containers, sidecarContainer)
// 序列化 Pod
patchBytes, err := json.Marshal(pod)
if err != nil {
log.Printf("Error marshaling Pod: %v", err)
admissionResponse.Allowed = false
admissionResponse.Result = &metav1.Status{
Status: metav1.StatusFailure,
Message: err.Error(),
}
return admissionReview
}
// 创建 patch
patchType := admissionv1.PatchTypeJSONPatch
admissionResponse.PatchType = &patchType
admissionResponse.Patch = createPatch(admissionReview.Request.Object.Raw, patchBytes)
admissionReview.Response = &admissionResponse
return admissionReview
}
func createPatch(original, modified []byte) []byte {
// This is a very basic patch creation. For more robust patching, consider using a library like "github.com/evanphx/json-patch"
// This example just replaces the entire object. A better approach would be to generate a JSON patch
return []byte(fmt.Sprintf(`[{"op":"replace", "path":"/spec", "value": %s}]`, string(modified)))
}
代码解析
mutateHandler
函数是 Webhook 服务的入口,它负责接收 Kubernetes 发来的 AdmissionReview 请求,并调用processAdmissionReview
函数进行处理。processAdmissionReview
函数负责解析 AdmissionReview 请求,反序列化 Pod 对象,然后对 Pod 进行修改,添加 annotation 和 sidecar 容器。- 最后,将修改后的 Pod 对象序列化成 JSON,并生成 patch,返回给 Kubernetes。
- 这里使用了
json patch
来进行对象更新,因为直接返回整个对象会导致一些字段丢失。
3. 生成 TLS 证书
Webhook 服务必须使用 HTTPS 协议,所以我们需要生成 TLS 证书。可以使用 openssl
命令来生成:
openssl req -x509 -newkey rsa:2048 -keyout tls.key -out tls.crt -days 365 -nodes -subj '/CN=your-domain.com'
记得把 your-domain.com
替换成你的域名,或者你的服务器 IP 地址。
4. 部署 Webhook 服务
将 Webhook 服务部署到你的服务器上,并确保它可以通过 HTTPS 访问。记得把 TLS 证书和私钥放到正确的位置。
5. 创建 MutatingWebhookConfiguration
接下来,我们需要创建一个 MutatingWebhookConfiguration 资源,告诉 Kubernetes 我们的 Webhook 服务在哪里,以及哪些资源需要被拦截。
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: my-mutating-webhook
webhooks:
- name: my-webhook.example.com
clientConfig:
url: "https://your-domain.com/mutate" # 替换成你的 Webhook 服务地址
caBundle: $(cat /path/to/tls.crt | base64 | tr -d 'n') # 替换成你的 CA 证书
rules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["pods"]
admissionReviewVersions: ["v1", "v1beta1"]
sideEffects: None
namespaceSelector:
matchLabels:
mutate: enabled
配置解析
clientConfig.url
:Webhook 服务的地址,必须使用 HTTPS 协议。clientConfig.caBundle
:CA 证书,用于验证 Webhook 服务的证书。rules
:定义了哪些资源需要被拦截,这里我们拦截了所有 Pod 的创建请求。sideEffects
:声明 Webhook 的副作用,这里我们声明为None
,表示 Webhook 不会产生任何副作用。namespaceSelector
: 定义了哪些 namespace 会被这个webhook拦截。
将这个 YAML 文件应用到你的 Kubernetes 集群:
kubectl apply -f mutating-webhook.yaml
6. 测试 Webhook
现在,我们可以创建一个 Pod 来测试我们的 Webhook 是否生效:
apiVersion: v1
kind: Pod
metadata:
name: my-pod
namespace: default
spec:
containers:
- name: my-container
image: nginx:latest
首先给namespace打上label
kubectl label namespace default mutate=enabled
创建pod
kubectl apply -f pod.yaml
如果一切顺利,你会发现 Pod 被自动注入了一个 sidecar 容器,并且 Pod 的 annotations 中多了一个 mutated-by: my-webhook
的标签。 🎉
第三幕:Validating Webhook,铁面判官
Validating Webhook 的原理和 Mutating Webhook 类似,只不过它不会修改 API 请求,而是验证 API 请求是否符合规范。如果 API 请求不符合规范,Validating Webhook 会直接拒绝该请求。
咱们也来写一个简单的 Validating Webhook,阻止创建没有标签的 Pod。
1. 编写 Webhook 服务
package main
import (
"encoding/json"
"io/ioutil"
"log"
"net/http"
admissionv1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
)
var (
universalDeserializer = serializer.NewCodecFactory(runtime.NewScheme()).UniversalDeserializer()
)
func main() {
http.HandleFunc("/validate", validateHandler)
// 启动 HTTPS 服务
log.Fatal(http.ListenAndServeTLS(":443", "/path/to/tls.crt", "/path/to/tls.key", nil))
}
func validateHandler(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading body: %v", err)
http.Error(w, "Error reading body", http.StatusBadRequest)
return
}
// 验证 Content-Type
contentType := r.Header.Get("Content-Type")
if contentType != "application/json" {
log.Printf("Content-Type was %s, expected application/json", contentType)
http.Error(w, "Invalid Content-Type, expected `application/json`", http.StatusUnsupportedMediaType)
return
}
// 反序列化 AdmissionReview
var admissionReview admissionv1.AdmissionReview
if _, _, err := universalDeserializer.Decode(body, nil, &admissionReview); err != nil {
log.Printf("Error decoding AdmissionReview: %v", err)
http.Error(w, "Error decoding AdmissionReview", http.StatusBadRequest)
return
}
// 处理 AdmissionReview
responseAdmissionReview := processAdmissionReview(admissionReview)
// 序列化 AdmissionReview
responseBytes, err := json.Marshal(responseAdmissionReview)
if err != nil {
log.Printf("Error marshaling AdmissionReview: %v", err)
http.Error(w, "Error marshaling AdmissionReview", http.StatusInternalServerError)
return
}
// 返回 AdmissionReview
w.Header().Set("Content-Type", "application/json")
if _, err := w.Write(responseBytes); err != nil {
log.Printf("Error writing response: %v", err)
http.Error(w, "Error writing response", http.StatusInternalServerError)
}
}
func processAdmissionReview(admissionReview admissionv1.AdmissionReview) admissionv1.AdmissionReview {
// 创建 AdmissionResponse
admissionResponse := admissionv1.AdmissionResponse{}
admissionResponse.UID = admissionReview.Request.UID
admissionResponse.Allowed = true
// 反序列化 Pod
var pod corev1.Pod
if err := json.Unmarshal(admissionReview.Request.Object.Raw, &pod); err != nil {
log.Printf("Error unmarshaling Pod: %v", err)
admissionResponse.Allowed = false
admissionResponse.Result = &metav1.Status{
Status: metav1.StatusFailure,
Message: err.Error(),
}
admissionReview.Response = &admissionResponse
return admissionReview
}
// 验证 Pod
if len(pod.ObjectMeta.Labels) == 0 {
admissionResponse.Allowed = false
admissionResponse.Result = &metav1.Status{
Status: metav1.StatusFailure,
Message: "Pod must have at least one label",
}
}
admissionReview.Response = &admissionResponse
return admissionReview
}
代码解析
validateHandler
函数是 Webhook 服务的入口,它负责接收 Kubernetes 发来的 AdmissionReview 请求,并调用processAdmissionReview
函数进行处理。processAdmissionReview
函数负责解析 AdmissionReview 请求,反序列化 Pod 对象,然后验证 Pod 是否有标签。如果没有标签,就拒绝该请求。
2. 创建 ValidatingWebhookConfiguration
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: my-validating-webhook
webhooks:
- name: my-webhook.example.com
clientConfig:
url: "https://your-domain.com/validate" # 替换成你的 Webhook 服务地址
caBundle: $(cat /path/to/tls.crt | base64 | tr -d 'n') # 替换成你的 CA 证书
rules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["pods"]
admissionReviewVersions: ["v1", "v1beta1"]
sideEffects: None
namespaceSelector:
matchLabels:
validate: enabled
将这个 YAML 文件应用到你的 Kubernetes 集群:
kubectl apply -f validating-webhook.yaml
3. 测试 Webhook
现在,我们可以创建一个没有标签的 Pod 来测试我们的 Webhook 是否生效:
apiVersion: v1
kind: Pod
metadata:
name: my-pod
namespace: default
spec:
containers:
- name: my-container
image: nginx:latest
首先给namespace打上label
kubectl label namespace default validate=enabled
如果一切顺利,你会发现 Kubernetes 会拒绝创建这个 Pod,并返回一个错误信息:“Pod must have at least one label”。 😎
第四幕:Webhook 的注意事项
- 性能:Webhook 会增加 API 请求的延迟,所以要尽量优化 Webhook 的性能,避免影响集群的整体性能。
- 可靠性:Webhook 服务必须保证高可用,否则会导致 API 请求失败。
- 安全性:Webhook 服务必须使用 HTTPS 协议,并且要验证客户端证书,防止恶意攻击。
- 幂等性:Mutating Webhook 必须保证幂等性,即多次执行同一个 Webhook 请求,结果应该是一样的。
- 版本兼容性:Kubernetes API 会不断更新,所以 Webhook 服务要保持与 Kubernetes API 的版本兼容。
- 监控:要对 Webhook 服务进行监控,及时发现和解决问题。
第五幕:Webhook 的高级玩法
- 动态准入控制:根据不同的策略,动态地启用或禁用 Webhook。
- 多集群 Webhook:将 Webhook 服务部署到多个集群,实现跨集群的准入控制。
- Webhook 链:将多个 Webhook 串联起来,实现复杂的准入控制逻辑。
- 使用 Webhook 实现自定义资源验证: 我们可以通过validating webhook 来做CRD的验证,例如验证CRD的字段是不是符合规范。
结尾:Webhook,未来的无限可能
总而言之,Kubernetes Webhook 是一种非常强大的扩展机制,可以让你自定义 Kubernetes 的 API 行为,实现各种各样的自动化和安全功能。
当然,Webhook 也是一把双刃剑,用好了是神器,用不好是灾难。所以,在使用 Webhook 的时候,一定要谨慎,要做好充分的测试和监控。
希望今天的脱口秀能让你对 Kubernetes Webhook 有更深入的了解。如果你还有什么问题,欢迎在评论区留言,我会尽力解答。
谢谢大家! 👏
补充:
- 以上代码只是一个简单的示例,实际应用中需要根据具体的需求进行修改。
- 在生产环境中,建议使用专业的 Webhook 管理工具,例如 Gatekeeper 或 Kyverno。
- 记得定期更新你的 Kubernetes 集群和 Webhook 服务,以保持安全性和兼容性。
- 最后,祝大家玩转 Webhook,让你的 Kubernetes 集群更加强大! 💪