Kubernetes Webhooks 编程:动态修改或验证 API 请求

好的,各位观众老爷们,欢迎来到今天的 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 集群更加强大! 💪

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注