Kubernetes Admission Webhooks 开发与策略管理

好的,各位观众,各位朋友,欢迎来到今天的 Kubernetes Admission Webhooks 开发与策略管理脱口秀!我是你们的老朋友,人称“云原生段子手”的程序猿老码。今天咱们不讲那些枯燥的源码分析,也不搞那些深奥的理论推导,咱们就用最接地气的方式,聊聊这 Kubernetes 世界里既神秘又强大的 Admission Webhooks!

开场白:Kubernetes 的“门卫大爷”

话说 Kubernetes,这可是个了不起的平台,承载着咱们的各种应用,管理着无数的容器。但是,你有没有想过,谁来保证进入集群的“东西”都是安全的、合规的、符合咱们要求的呢?难道就让它们随便进出,想干嘛干嘛?那还得了!

这时候,就轮到咱们今天的主角——Admission Webhooks 登场了!你可以把它想象成 Kubernetes 集群的“门卫大爷”,他负责检查每一个进入集群的请求,看看是不是符合规矩。如果符合,就放行;如果不符合,就直接拒绝,或者修改一下,让它符合规矩再放行。

是不是感觉瞬间安全感爆棚?有了 Admission Webhooks,咱们就可以自定义各种策略,对进入集群的资源进行精细化的管理和控制,让 Kubernetes 集群更加安全、稳定、可靠。

第一幕:Admission Webhooks 是个啥?

好了,玩笑归玩笑,咱们还是要稍微严肃一点,了解一下 Admission Webhooks 的基本概念。

1. 什么是 Admission Controller?

要理解 Admission Webhooks,首先要了解 Admission Controller。Admission Controller 是 Kubernetes API Server 的一个准入控制组件,它在对象持久化之前拦截 API 请求,并根据一系列策略进行验证和修改。

简单来说,它就像一个过滤器,在请求到达 etcd 之前,对请求进行过滤和处理。

2. Admission Webhooks 的两种类型

Admission Webhooks 分为两种类型:

  • Mutating Admission Webhook (变更型准入 Webhook): 它可以修改请求对象,例如,自动添加标签、注入 sidecar 容器等。就像装修队的泥瓦匠,给你修修补补,让房子更漂亮。
  • Validating Admission Webhook (验证型准入 Webhook): 它只能验证请求对象,不能修改。如果验证失败,则拒绝请求。就像质检员,不合格的产品一律不准出厂。

3. Admission Webhooks 的工作流程

Admission Webhooks 的工作流程大致如下:

  1. 用户通过 kubectl 或 API 客户端发送请求到 Kubernetes API Server。
  2. API Server 接收到请求后,会先经过认证 (Authentication) 和鉴权 (Authorization)。
  3. 如果认证和鉴权都通过了,API Server 会调用 Admission Controller 进行准入控制。
  4. Admission Controller 会依次调用配置好的 Admission Webhooks。
  5. Mutating Admission Webhooks 先执行,对请求对象进行修改。
  6. Validating Admission Webhooks 后执行,对请求对象进行验证。
  7. 如果所有 Webhooks 都通过了,API Server 才会将请求对象持久化到 etcd 中。
  8. 如果任何一个 Webhook 拒绝了请求,API Server 会返回错误给用户。

可以用表格更清晰地展示这个过程:

步骤 描述
1 用户发送请求到 Kubernetes API Server
2 API Server 进行认证 (Authentication) 和鉴权 (Authorization)
3 API Server 调用 Admission Controller
4 Admission Controller 依次调用配置好的 Admission Webhooks
5 Mutating Admission Webhooks 执行,修改请求对象
6 Validating Admission Webhooks 执行,验证请求对象
7 所有 Webhooks 通过,API Server 将请求对象持久化到 etcd
8 任何一个 Webhook 拒绝请求,API Server 返回错误给用户

第二幕:手把手教你开发 Admission Webhooks

光说不练假把式,接下来咱们就来手把手开发一个简单的 Admission Webhook。

1. 准备工作

  • 一个可用的 Kubernetes 集群 (可以是 Minikube, Kind, 或云厂商的 Kubernetes 服务)
  • Go 语言环境
  • 一个代码编辑器 (VS Code, Goland 等)

2. 创建 Webhook 项目

首先,创建一个新的 Go 项目:

mkdir admission-webhook-example
cd admission-webhook-example
go mod init example.com/admission-webhook

3. 编写 Webhook Handler

创建一个 main.go 文件,编写 Webhook Handler 的代码:

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"
)

func main() {
    http.HandleFunc("/mutate", mutateHandler)
    http.HandleFunc("/validate", validateHandler)

    log.Println("Webhook server started on port 8443")
    err := http.ListenAndServeTLS(":8443", "/etc/webhook/certs/tls.crt", "/etc/webhook/certs/tls.key", nil)
    if err != nil {
        log.Fatalf("Failed to listen and serve webhook server: %v", err)
    }
}

func mutateHandler(w http.ResponseWriter, r *http.Request) {
    log.Println("Mutate Handler called")
    body, err := ioutil.ReadAll(r.Body)
    if err != nil {
        log.Printf("Failed to read request body: %v", err)
        http.Error(w, "Failed to read request body", http.StatusBadRequest)
        return
    }

    var admissionReview admissionv1.AdmissionReview
    err = json.Unmarshal(body, &admissionReview)
    if err != nil {
        log.Printf("Failed to unmarshal admission review: %v", err)
        http.Error(w, "Failed to unmarshal admission review", http.StatusBadRequest)
        return
    }

    pod := corev1.Pod{}
    err = json.Unmarshal(admissionReview.Request.Object.Raw, &pod)
    if err != nil {
        log.Printf("Failed to unmarshal pod: %v", err)
        http.Error(w, "Failed to unmarshal pod", http.StatusBadRequest)
        return
    }

    // Mutate the pod - add a label
    if pod.ObjectMeta.Labels == nil {
        pod.ObjectMeta.Labels = make(map[string]string)
    }
    pod.ObjectMeta.Labels["mutated-by"] = "admission-webhook"

    patch, err := createPatch(&pod, admissionReview.Request.Object.Raw)
    if err != nil {
        log.Printf("Failed to create patch: %v", err)
        http.Error(w, "Failed to create patch", http.StatusInternalServerError)
        return
    }

    response := admissionv1.AdmissionReview{
        Response: &admissionv1.AdmissionResponse{
            UID:     admissionReview.Request.UID,
            Allowed: true,
            PatchType: func() *admissionv1.PatchType {
                pt := admissionv1.PatchTypeJSONPatch
                return &pt
            }(),
            Patch: patch,
        },
    }

    responseBytes, err := json.Marshal(response)
    if err != nil {
        log.Printf("Failed to marshal admission response: %v", err)
        http.Error(w, "Failed to marshal admission response", http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    w.Write(responseBytes)
    log.Println("Mutate Handler completed")
}

func validateHandler(w http.ResponseWriter, r *http.Request) {
    log.Println("Validate Handler called")
    body, err := ioutil.ReadAll(r.Body)
    if err != nil {
        log.Printf("Failed to read request body: %v", err)
        http.Error(w, "Failed to read request body", http.StatusBadRequest)
        return
    }

    var admissionReview admissionv1.AdmissionReview
    err = json.Unmarshal(body, &admissionReview)
    if err != nil {
        log.Printf("Failed to unmarshal admission review: %v", err)
        http.Error(w, "Failed to unmarshal admission review", http.StatusBadRequest)
        return
    }

    pod := corev1.Pod{}
    err = json.Unmarshal(admissionReview.Request.Object.Raw, &pod)
    if err != nil {
        log.Printf("Failed to unmarshal pod: %v", err)
        http.Error(w, "Failed to unmarshal pod", http.StatusBadRequest)
        return
    }

    // Validate the pod - ensure it has a specific label
    if _, ok := pod.ObjectMeta.Labels["required-label"]; !ok {
        response := admissionv1.AdmissionReview{
            Response: &admissionv1.AdmissionResponse{
                UID:     admissionReview.Request.UID,
                Allowed: false,
                Result: &metav1.Status{
                    Message: "Pod does not have the required-label label",
                },
            },
        }

        responseBytes, err := json.Marshal(response)
        if err != nil {
            log.Printf("Failed to marshal admission response: %v", err)
            http.Error(w, "Failed to marshal admission response", http.StatusInternalServerError)
            return
        }

        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusOK)
        w.Write(responseBytes)
        log.Println("Validate Handler completed - Rejected")
        return
    }

    response := admissionv1.AdmissionReview{
        Response: &admissionv1.AdmissionResponse{
            UID:     admissionReview.Request.UID,
            Allowed: true,
        },
    }

    responseBytes, err := json.Marshal(response)
    if err != nil {
        log.Printf("Failed to marshal admission response: %v", err)
        http.Error(w, "Failed to marshal admission response", http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    w.Write(responseBytes)
    log.Println("Validate Handler completed - Allowed")
}

func createPatch(pod *corev1.Pod, original []byte) ([]byte, error) {
    podJson, err := json.Marshal(pod)
    if err != nil {
        return nil, fmt.Errorf("failed to marshal pod: %v", err)
    }

    patch, err := jsonPatch(original, podJson)
    if err != nil {
        return nil, fmt.Errorf("failed to create json patch: %v", err)
    }

    return patch, nil
}

// jsonPatch creates a RFC6902 json patch between two documents.
func jsonPatch(original, modified []byte) ([]byte, error) {
    ops, err := diff(original, modified)
    if err != nil {
        return nil, err
    }
    return json.Marshal(ops)
}

// Diff calculates the RFC6902 json patch between two JSON documents.
func diff(original, modified []byte) ([]map[string]interface{}, error) {
    var orig, mod interface{}
    if err := json.Unmarshal(original, &orig); err != nil {
        return nil, err
    }
    if err := json.Unmarshal(modified, &mod); err != nil {
        return nil, err
    }

    return recursiveDiff("/", orig, mod), nil
}

func recursiveDiff(path string, orig, mod interface{}) []map[string]interface{} {
    var ops []map[string]interface{}

    switch original := orig.(type) {
    case map[string]interface{}:
        modified, ok := mod.(map[string]interface{})
        if !ok {
            // If the original was an object, but the modified isn't, replace the entire object
            ops = append(ops, map[string]interface{}{
                "op":   "replace",
                "path": path,
                "value": mod,
            })
            return ops
        }

        // Iterate through the original object to find removals and modifications
        for key, value := range original {
            newPath := path + "/" + key
            newValue, ok := modified[key]
            if !ok {
                // Property was removed
                ops = append(ops, map[string]interface{}{
                    "op":   "remove",
                    "path": newPath,
                })
            } else {
                ops = append(ops, recursiveDiff(newPath, value, newValue)...)
            }
        }

        // Iterate through the modified object to find additions
        for key, newValue := range modified {
            _, ok := original[key]
            if !ok {
                // Property was added
                newPath := path + "/" + key
                ops = append(ops, map[string]interface{}{
                    "op":   "add",
                    "path": newPath,
                    "value": newValue,
                })
            }
        }

    case []interface{}:
        modifiedList, ok := mod.([]interface{})
        if !ok {
            // If the original was an array, but the modified isn't, replace the entire array
            ops = append(ops, map[string]interface{}{
                "op":   "replace",
                "path": path,
                "value": mod,
            })
            return ops
        }

        // Compare arrays element by element
        for i, value := range original {
            if i >= len(modifiedList) {
                // Original array is longer, so remove extra elements from original
                ops = append(ops, map[string]interface{}{
                    "op":   "remove",
                    "path": fmt.Sprintf("%s/%d", path, i),
                })
                continue
            }

            newValue := modifiedList[i]
            newPath := fmt.Sprintf("%s/%d", path, i)
            ops = append(ops, recursiveDiff(newPath, value, newValue)...)
        }

        // Add any extra elements from modified array
        for i := len(original); i < len(modifiedList); i++ {
            newValue := modifiedList[i]
            newPath := fmt.Sprintf("%s/%d", path, i)
            ops = append(ops, map[string]interface{}{
                "op":   "add",
                "path": newPath,
                "value": newValue,
            })
        }

    default:
        // Simple values (strings, numbers, booleans)
        if orig != mod {
            ops = append(ops, map[string]interface{}{
                "op":   "replace",
                "path": path,
                "value": mod,
            })
        }
    }

    return ops
}

这个 main.go 文件定义了两个 handler:

  • mutateHandler: 一个 Mutating Admission Webhook,它会在 Pod 的 metadata 中添加一个 mutated-by: admission-webhook 的 label。
  • validateHandler: 一个 Validating Admission Webhook,它会验证 Pod 是否包含 required-label label。 如果没有,将会拒绝创建 Pod。

4. 生成 TLS 证书

Admission Webhooks 必须使用 HTTPS,所以我们需要生成 TLS 证书。可以使用 openssl 命令来生成自签名证书:

openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj '/CN=admission-webhook-example.default.svc'

cert.pemkey.pem 复制到 /etc/webhook/certs/ 目录下。

5. 构建和部署 Webhook

将 Webhook 构建成 Docker 镜像,并部署到 Kubernetes 集群中。

Dockerfile:

FROM golang:1.19-alpine AS builder

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .

RUN go build -o webhook .

FROM alpine:latest

WORKDIR /app

COPY --from=builder /app/webhook .
COPY cert.pem key.pem /etc/webhook/certs/

CMD ["./webhook"]

构建镜像:

docker build -t your-docker-repo/admission-webhook-example:latest .
docker push your-docker-repo/admission-webhook-example:latest

部署 Deployment 和 Service:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: admission-webhook-example
  labels:
    app: admission-webhook-example
spec:
  replicas: 1
  selector:
    matchLabels:
      app: admission-webhook-example
  template:
    metadata:
      labels:
        app: admission-webhook-example
    spec:
      containers:
      - name: webhook
        image: your-docker-repo/admission-webhook-example:latest
        ports:
        - containerPort: 8443
        volumeMounts:
        - name: webhook-certs
          mountPath: /etc/webhook/certs
          readOnly: true
      volumes:
      - name: webhook-certs
        secret:
          secretName: webhook-certs
---
apiVersion: v1
kind: Service
metadata:
  name: admission-webhook-example
spec:
  selector:
    app: admission-webhook-example
  ports:
  - port: 443
    targetPort: 8443

创建 Secret:

kubectl create secret tls webhook-certs --cert=cert.pem --key=key.pem

6. 配置 Admission Webhook

接下来,我们需要配置 MutatingWebhookConfiguration 和 ValidatingWebhookConfiguration,告诉 Kubernetes API Server 如何调用我们的 Webhook。

MutatingWebhookConfiguration:

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: mutating-webhook-configuration
webhooks:
  - name: mutate.example.com
    clientConfig:
      caBundle: <BASE64_ENCODED_CERT>
      service:
        name: admission-webhook-example
        namespace: default
        path: /mutate
    rules:
      - apiGroups:   [""]
        apiVersions: ["v1"]
        operations:  ["CREATE"]
        resources:   ["pods"]
    sideEffects: None
    admissionReviewVersions: ["v1"]

ValidatingWebhookConfiguration:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: validating-webhook-configuration
webhooks:
  - name: validate.example.com
    clientConfig:
      caBundle: <BASE64_ENCODED_CERT>
      service:
        name: admission-webhook-example
        namespace: default
        path: /validate
    rules:
      - apiGroups:   [""]
        apiVersions: ["v1"]
        operations:  ["CREATE"]
        resources:   ["pods"]
    sideEffects: None
    admissionReviewVersions: ["v1"]

cert.pem 的内容进行 Base64 编码,替换 <BASE64_ENCODED_CERT>

7. 测试 Webhook

创建一个 Pod,看看 Webhook 是否生效:

apiVersion: v1
kind: Pod
metadata:
  name: test-pod
spec:
  containers:
  - name: nginx
    image: nginx:latest

创建 Pod:

kubectl apply -f pod.yaml

检查 Pod 的 label:

kubectl get pod test-pod -o yaml

你会发现 Pod 的 metadata 中多了一个 mutated-by: admission-webhook 的 label。

再创建一个没有 required-label label 的 Pod,看看是否会被拒绝:

apiVersion: v1
kind: Pod
metadata:
  name: test-pod-no-label
spec:
  containers:
  - name: nginx
    image: nginx:latest

创建 Pod:

kubectl apply -f pod-no-label.yaml

你会看到类似下面的错误信息:

Error from server: admission webhook "validate.example.com" denied the request: Pod does not have the required-label label

第三幕:策略管理与最佳实践

开发完 Webhook 只是万里长征的第一步,如何有效地管理和使用这些 Webhook 才是关键。

1. 策略管理

  • 细粒度控制: 可以根据不同的 Namespace、用户、资源类型等条件,配置不同的 Webhook 策略,实现更细粒度的控制。
  • 策略版本控制: 使用 Git 等版本控制工具管理 Webhook 的配置,方便回滚和审计。
  • 策略测试: 在生产环境之前,先在测试环境验证 Webhook 策略的有效性。

2. 最佳实践

  • 保持 Webhook 的轻量级: 避免在 Webhook 中执行复杂的逻辑,以免影响 API Server 的性能。
  • 处理 Webhook 的异常情况: 编写健壮的错误处理代码,避免 Webhook 出现异常导致 API 请求失败。
  • 监控 Webhook 的性能: 监控 Webhook 的延迟和错误率,及时发现和解决问题。
  • 使用 Webhook 框架: 可以使用一些现成的 Webhook 框架,例如 Kubewarden, Kyverno 等,可以简化 Webhook 的开发和管理。

3. 使用 Kubewarden 和 Kyverno 管理策略

这两个工具都是云原生策略引擎,可以帮助你更方便地管理 Kubernetes 集群的策略。

  • Kubewarden: 使用 WebAssembly (WASM) 技术,可以编写各种语言的策略,例如 Go, Rust, AssemblyScript 等。
  • Kyverno: 使用 YAML 编写策略,简单易用,适合 Kubernetes 用户。

总结:Admission Webhooks 的无限可能

各位观众,今天的 Kubernetes Admission Webhooks 开发与策略管理脱口秀就到这里了。希望通过今天的讲解,大家对 Admission Webhooks 有了更深入的了解。

Admission Webhooks 就像 Kubernetes 集群的“任意门”,可以帮助我们实现各种各样的自动化和安全策略。只要你敢想,它就能帮你实现!

记住,有了 Admission Webhooks,你的 Kubernetes 集群,你做主! 🚀

感谢大家的观看,我们下期再见! 👋

发表回复

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