侧边栏壁纸
博主头像
汪洋

即使慢,驰而不息,纵会落后,纵会失败,但一定可以达到他所向的目标。 - 鲁迅

  • 累计撰写 217 篇文章
  • 累计创建 82 个标签
  • 累计收到 270 条评论

Kubernetes - webhook mutating

汪洋
2025-08-15 / 0 评论 / 1 点赞 / 53 阅读 / 12,275 字

在 Kubernetes(k8s)中,Mutating Webhook 是一种用于在资源创建 / 更新等操作的准入阶段(Admission Phase) 动态修改资源对象的机制。它可以在资源被 Kubernetes API Server 持久化之前,对资源的配置进行自动调整(例如注入默认值、添加标签、注入 Sidecar 容器等),从而简化用户配置或强制团队规范。

一、相关概念

核心作用

Mutating Webhook 的核心是 “修改”:在资源满足触发条件时,通过 HTTP/HTTPS 请求调用用户自定义的 Web 服务(Webhook 服务),由该服务返回需要修改的内容,最终 API Server 根据修改内容更新资源。

工作流程

当用户通过 kubectl或 API 创建 / 更新一个资源(如 Pod、Deployment)时,流程如下:

  • 资源请求先经过认证(Authentication) 和授权(Authorization) 阶段
  • 进入准入控制(Admission Control) 阶段,先执行所有 Mutating Webhook(修改资源),再执行 Validating Webhook(验证资源,不修改)
  • 若所有 Webhook 通过,资源被持久化到 etcd;若失败,请求被拒绝

关键时机

Mutating Webhook 运行在资源被持久化之前,且先于 Validating Webhook 执行(确保修改后的资源会被验证)。

典型使用场景

  • 自动注入 Sidecar 容器:例如 Istio 服务网格中,自动为 Pod 注入代理 Sidecar(istio-proxy),无需用户手动配置
  • 设置默认值:为未指定 resources.limits 的 Pod 自动添加默认资源限制,或为 Namespace 自动添加默认标签
  • 强制规范:例如强制所有 Pod 添加 app: 标签,或修改镜像拉取策略为 Always
  • 敏感信息处理:自动替换明文密码为 Secret 引用

如何实现一个 Mutating Webhook?

实现步骤可分为 3 部分:部署 Webhook 服务、配置证书、定义 Webhook 规则

1. 部署 Webhook 服务

Webhook 服务是一个 HTTP/HTTPS 服务(K8s 要求 HTTPS),需部署在 K8s 集群内(通常用 Deployment+Service 暴露),负责接收 API Server 的请求并返回修改指令。

服务接收的请求格式(简化)

{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "request": {
    "uid": "xxx",  // 请求唯一ID
    "object": { ... },  // 待修改的资源对象(如Pod)
    "operation": "CREATE"  // 操作类型:CREATE/UPDATE/DELETE/CONNECT
  }
}

服务返回的响应格式(简化)

{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "response": {
    "uid": "xxx",  // 对应请求的uid
    "allowed": true,  // 是否允许操作
    "patch": "base64编码的JSON Patch",  // 要修改的内容(JSON Patch格式)
    "patchType": "JSONPatch"
  }
}

例如:若要给 Pod 添加标签 env: prod,JSON Patch 为[{"op":"add","path":"/metadata/labels/env","value":"prod"}],base64 编码后放入 patch 字段

2. 配置 HTTPS 证书

K8s API Server 调用 Webhook 时要求 HTTPS(避免中间人攻击),因此需要为 Webhook 服务配置 TLS 证书。通常有两种方式:

  • 手动生成证书(自签或 CA 签发),通过 Secret 挂载到 Webhook 服务
  • 使用工具自动管理(如cert-manager),自动生成和轮换证书

3. 定义 MutatingWebhookConfiguration

通过 K8s 的 MutatingWebhookConfiguration 资源配置 Webhook 的触发规则,告诉 API Server:“哪些资源、在什么操作下需要调用我的 Webhook 服务”

示例配置

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: example-mutating-webhook
webhooks:
- name: example-webhook.example.com  # Webhook名称(需符合域名格式)
  clientConfig:
    service:
      name: example-webhook-service  # Webhook服务的Service名称
      namespace: default  # 服务所在命名空间
      path: "/mutate"  # 服务接收请求的路径
    caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0t...  # 信任的CA证书(base64编码)
  rules:
  - operations: ["CREATE"]  # 触发操作:创建资源时
    apiGroups: [""]  # 目标API组(空表示core组)
    apiVersions: ["v1"]  # 目标API版本
    resources: ["pods"]  # 目标资源(这里是Pod)
  namespaceSelector:  # 仅匹配特定命名空间的资源(可选)
    matchLabels:
      env: prod
  failurePolicy: Fail  # 若Webhook调用失败,是否拒绝请求(Fail/Ignore)
  timeoutSeconds: 5  # 超时时间(默认10s)
  • rules:定义触发 Webhook 的资源(如pods、deployments)、操作(CREATE/UPDATE等)
  • namespaceSelector/objectSelector:进一步过滤目标资源(例如只处理 env=prod 命名空间的 Pod)
  • failurePolicy:若 Webhook 服务不可用,Fail表示拒绝请求,Ignore表示跳过 Webhook 继续处理

与 Validating Webhook 的区别

  • Mutating Webhook:修改资源(返回patch),可添加 / 删除 / 修改字段
  • Validating Webhook:仅验证资源(不修改),返回allowed: true/false,用于检查资源是否符合规范(如禁止特权容器)

两者可配合使用:先通过 Mutating 修改资源,再通过 Validating 验证修改后的资源是否合法

调试技巧

  • 查看 API Server 日志(kubectl logs -n kube-system kube-apiserver-xxx),排查 Webhook 调用失败原因(如证书无效、服务不可达)
  • 在 Webhook 服务中打印请求 / 响应日志,确认是否正确处理
  • 使用 kubectl describe mutatingwebhookconfiguration 检查 Webhook 配置是否生效

二、跟我一步一步开发一个 Mutating Webhook

模拟需求

所有在 default 名字空间创建的 pod,都会自动添加一个"created-by": "pod-label-mutator", "managed-by": "webhook", "environment": "default" 的标签(来自于同学的面试问题)

基于 golang 编写 Kubernetes Mutating Webhook

安装 golang 环境

cd /tmp
wget https://dl.google.com/go/go1.20.linux-amd64.tar.gz

sudo tar -C /usr/local -xzf go1.20.linux-amd64.tar.gz

vi ~/.bash_profile
    # Golang环境变量
    export GOROOT=/usr/local/go
    export GOPATH=$HOME/go
    export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
source ~/.bash_profile

go version

mkdir -p $HOME/go/{src,pkg,bin}

# 临时配置国内 golang 库代理
# 设置代理(以七牛云为例,可替换为其他代理地址)
export GOPROXY=https://goproxy.cn,direct
# 可选:设置校验和数据库代理(解决 sumdb 访问问题)
export GOSUMDB=sum.golang.google.cn

程序关键部分

  • Webhook 服务:
    • 使用标准库的net/http实现 HTTPS 服务器
    • 处理/mutate端点的 POST 请求
    • 解析 Kubernetes 的 AdmissionReview 请求
    • 生成适当的 JSON Patch 来添加标签
  • 标签添加逻辑:
    • 定义了要添加的默认标签(created-by、managed-by和environment)
    • 检查 Pod 是否已有这些标签,如果没有则添加
    • 生成 JSON Patch 描述这些修改
  • 部署配置:
    • Deployment 定义了如何运行 Webhook 服务
    • Service 暴露 Webhook 服务供 Kubernetes API Server 访问
    • MutatingWebhookConfiguration 配置了何时触发 Webhook

程序体

package main

import (
        "encoding/json"
        "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/types" // 新增:导入UID类型所在的包
)

// 要添加的默认标签
var defaultLabels = map[string]string{
        "created-by":  "pod-label-mutator",
        "managed-by":  "webhook",
        "environment": "default",
}

func main() {
        http.HandleFunc("/mutate", mutateHandler)
        log.Println("Starting webhook server on :443")
        
        // 注意:实际生产环境需要使用合法的TLS证书
        if err := http.ListenAndServeTLS(":443", "/etc/tls/tls.crt", "/etc/tls/tls.key", nil); err != nil {
                log.Fatalf("Failed to start server: %v", err)
        }
}

func mutateHandler(w http.ResponseWriter, r *http.Request) {
        var review admissionv1.AdmissionReview
        if err := json.NewDecoder(r.Body).Decode(&review); err != nil {
                log.Printf("Error decoding request: %v", err)
                http.Error(w, "Invalid request body", http.StatusBadRequest)
                return
        }

        // 提取Pod对象
        pod := &corev1.Pod{}
        if err := json.Unmarshal(review.Request.Object.Raw, pod); err != nil {
                log.Printf("Error unmarshalling pod: %v", err)
                // 直接传递types.UID类型,无需转换
                sendResponse(w, review.Request.UID, false, nil)
                return
        }

        // 生成需要添加的标签
        patchOps := generateLabelPatch(pod)
        if len(patchOps) == 0 {
                sendResponse(w, review.Request.UID, true, nil)
                return
        }

        // 发送包含补丁的响应
        sendResponse(w, review.Request.UID, true, patchOps)
}

// 生成添加标签的JSON Patch操作
func generateLabelPatch(pod *corev1.Pod) []map[string]interface{} {
        var patchOps []map[string]interface{}
        
        // 如果Pod没有标签,先创建labels字段
        if pod.ObjectMeta.Labels == nil {
                patchOps = append(patchOps, map[string]interface{}{
                        "op":    "add",
                        "path":  "/metadata/labels",
                        "value": defaultLabels,
                })
                return patchOps
        }

        // 为不存在的标签添加补丁
        for key, value := range defaultLabels {
                if _, exists := pod.ObjectMeta.Labels[key]; !exists {
                        patchOps = append(patchOps, map[string]interface{}{
                                "op":    "add",
                                "path":  "/metadata/labels/" + key,
                                "value": value,
                        })
                }
        }

        return patchOps
}

// 发送 AdmissionReview 响应
// 注意:uid参数类型改为types.UID,与Kubernetes API类型一致
func sendResponse(w http.ResponseWriter, uid types.UID, allowed bool, patchOps []map[string]interface{}) {
        var patchBytes []byte
        var err error
        
        if patchOps != nil && len(patchOps) > 0 {
                patchBytes, err = json.Marshal(patchOps)
                if err != nil {
                        log.Printf("Error marshaling patch: %v", err)
                        http.Error(w, "Error creating patch", http.StatusInternalServerError)
                        return
                }
        }

        response := admissionv1.AdmissionReview{
                TypeMeta: metav1.TypeMeta{
                        APIVersion: "admission.k8s.io/v1",
                        Kind:       "AdmissionReview",
                },
                Response: &admissionv1.AdmissionResponse{
                        UID:     uid, // 直接使用types.UID类型,无需转换
                        Allowed: allowed,
                },
        }

        // 如果有补丁,添加到响应中
        if len(patchBytes) > 0 {
                response.Response.Patch = patchBytes
                response.Response.PatchType = &[]admissionv1.PatchType{admissionv1.PatchTypeJSONPatch}[0]
        }

        w.Header().Set("Content-Type", "application/json")
        if err := json.NewEncoder(w).Encode(response); err != nil {
                log.Printf("Error encoding response: %v", err)
                http.Error(w, "Error sending response", http.StatusInternalServerError)
        }
}

打包为镜像

# 使用官方Go镜像作为构建阶段
FROM golang:1.20-alpine AS builder

WORKDIR /app

# 复制go模块文件并下载依赖
COPY go.mod go.sum ./
RUN go mod download

# 复制源代码
COPY . .

# 构建应用
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o pod-label-mutator .

# 使用轻量级Alpine镜像作为运行阶段
FROM alpine:3.17

WORKDIR /root/

# 从构建阶段复制二进制文件
COPY --from=builder /app/pod-label-mutator .

# 创建存放证书的目录
RUN mkdir -p /etc/tls

# 暴露端口
EXPOSE 443

# 运行应用
CMD ["./pod-label-mutator"]

已推送至阿里云镜像仓库,镜像名称为 registry.cn-hangzhou.aliyuncs.com/wangyangshare/share:mutatetest

编写 deployment 文件,部署 webhook 至集群

apiVersion: apps/v1
kind: Deployment
metadata:
  name: pod-label-mutator
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: pod-label-mutator
  template:
    metadata:
      labels:
        app: pod-label-mutator
    spec:
      containers:
      - name: pod-label-mutator
        image: registry.cn-hangzhou.aliyuncs.com/wangyangshare/share:mutatetest
        ports:
        - containerPort: 443
        volumeMounts:
        - name: tls-certs
          mountPath: /etc/tls
          readOnly: true
        resources:
          requests:
            memory: "64Mi"
            cpu: "250m"
          limits:
            memory: "128Mi"
            cpu: "500m"
      volumes:
      - name: tls-certs
        secret:
          secretName: pod-label-mutator-tls
---
apiVersion: v1
kind: Service
metadata:
  name: pod-label-mutator-svc
  namespace: default
spec:
  selector:
    app: pod-label-mutator
  ports:
  - port: 443
    targetPort: 443

配置验证证书

Kubernetes 对 TLS 证书的验证更严格,要求使用 SAN(Subject Alternative Names)扩展字段指定域名,而不是依赖传统的 Common Name (CN) 字段。现代 TLS 标准(如 RFC 6125)已逐渐弃用通过 CN 验证域名的方式,转而要求通过 SAN 字段验证,较新的 Kubernetes 版本会严格执行这一规范。

步骤 1:创建 OpenSSL 配置文件(关键:添加 SAN)

创建一个 openssl.cnf 配置文件,明确指定 SAN 字段(包含 Webhook 服务的 DNS 名称):

[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = req_ext

[dn]
C = CN
ST = Beijing
L = Beijing
O = example
OU = webhook
CN = pod-label-mutator-svc.default.svc  # 传统CN(可保留,但需确保与SAN一致)

[req_ext]
subjectAltName = DNS:pod-label-mutator-svc.default.svc  # 关键:添加SAN,指定服务的DNS名称
  • SAN 必须包含 Webhook 服务的完整 DNS 名称(格式:service-name.namespace.svc),即 pod-label-mutator-svc.default.svc
  • 若 Webhook 服务有其他访问域名,也需添加到 SAN 中(用逗号分隔,如 DNS:xxx,DNS:yyy)
步骤 2:重新生成证书(CA + 服务器证书)

使用上述配置文件生成包含 SAN 的证书

# 1. 生成 CA 私钥和 CA 证书(用于签名服务器证书)
openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -days 3650 -out ca.crt -subj "/CN=pod-label-mutator-ca"

# 2. 生成 Webhook 服务器私钥
openssl genrsa -out tls.key 2048

# 3. 生成证书签名请求(CSR),使用上面的配置文件(包含SAN)
openssl req -new -key tls.key -out tls.csr -config openssl.cnf

# 4. 用 CA 证书签名生成服务器证书(包含SAN)
openssl x509 -req -in tls.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out tls.crt -days 3650 -extensions req_ext -extfile openssl.cnf
步骤 3:验证证书是否包含 SAN

生成证书后,检查 tls.crt 中是否包含 X509v3 Subject Alternative Name 字段

openssl x509 -in tls.crt -noout -text | grep -A 1 "Subject Alternative Name"

若输出类似 DNS:pod-label-mutator-svc.default.svc,则表示 SAN 配置成功
步骤 4:创建/更新 Secret

更新存储证书的 Secret(确保 Webhook 服务使用新证书)

# 先删除旧 Secret(若存在)
kubectl delete secret pod-label-mutator-tls -n default
# 创建新 Secret(包含新生成的 tls.crt 和 tls.key)
kubectl create secret tls pod-label-mutator-tls -n default --cert=tls.crt --key=tls.key
步骤 4:部署 deployment
kubectl apply -f deployment.yaml

配置 MutatingWebhookConfiguration

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: pod-label-mutator-webhook
webhooks:
- name: pod-label-mutator.default.svc
  clientConfig:  # 修正拼写错误:将clientConfigxq改为clientConfig
    service:
      name: pod-label-mutator-svc
      namespace: default
      path: "/mutate"
    caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURIekNDQWdlZ0F3SUJBZ0lVQ21zVFVSRmNnL0dlQ2ZOdG81a1hwaXpIbWdnd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0h6RWRNQnNHQTFVRUF3d1VjRzlrTFd4aFltVnNMVzExZEdGMGIzSXRZMkV3SGhjTk1qVXdPREUxTURJMQpOVE16V2hjTk16VXdPREV6TURJMU5UTXpXakFmTVIwd0d3WURWUVFEREJSd2IyUXRiR0ZpWld3dGJYVjBZWFJ2CmNpMWpZVENDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFNQzVoci9Cck45U2tsVlcKQVBMVXAyVjZkb3ZBVENYUUZlbzFZSTk3Zk9vTWc1eVlOUEJQRGtQcmVIanhtYkhHcEQ4bG04RkJNdmprN3N5KwpkQUVmaE0wTGFXYTRVZHJueS9kS0xWOStmQXlqZ2QwRlRGWjQ1eEcrVWE4ekxJdE5SdVdnaG9ucXdPZ2hpaGJaCkJiakcrd2tDL2tpUDQ1eitQL2IzSG5kUVlEVFhLaGl5T1JPdUluZG91aXYxVjIvQ1RSckhuYjZYQlE2M0gzdzcKSlRGeHZzeXUreStleEE1MDg0Smp0YXFmUU9iazRvMjZsUTdHbUJuZTVDYU91ak9sSmMvdVZMT2NlRmRvTUt4eApmNkpoazZSL0l3U2NXdHYvV3lqazVPcU90VVZ6UzhjVVVOdUN4OUQ4cW9JTnJOVzV1bkNic1AzdUQ0NmpuczhwCktnMC90SE1DQXdFQUFhTlRNRkV3SFFZRFZSME9CQllFRkRMcWduSFBPVmF5VllPNitBTjNNMlpGODBiVk1COEcKQTFVZEl3UVlNQmFBRkRMcWduSFBPVmF5VllPNitBTjNNMlpGODBiVk1BOEdBMVVkRXdFQi93UUZNQU1CQWY4dwpEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBRTUyMkFkZmJzdXJtM2w0c3Flai9vQ0dMMW1pZUhDSGJzd1l2NUd0CnlFa09YcldKdXBVSGg2VDhKWGdnaitGMzBwRlZaSHpRd2gvK2RBOENFRnVlMkJhNzc5K2M0bGZuRDJvMURSZmkKVjBQQmNkUU0rNHNyOU9tZGJxN0s3ZjU2QVB2ZXliVU1Xa2VmSzB0WVZ1UG9hUVZXcklRWmxDaUFRYndzNFFnbQp1UG9iSEVoYTZjcThlZGVBbUc1M2J5THY1UFlsM3QvTG84ZENLZFN5NkpqeGd5VkFtZUMzUitUc3lUMWk2a2x5CkJmVEdUbjBZY3JOU2dIc2c1eWdkTWxiVGNCbmhrZzZzNUZiaDlNbnJCS2FrZFZ6ZytXcXZJcllpc2hpOWsvT3cKVFJXTm9mS2c0UDZtZ3pwMnZubWRLaHZOeXlOYVNjRTdCdWVKNk5xSWdiV2VmL2M9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
  rules:
  - operations: ["CREATE"]
    apiGroups: [""]
    apiVersions: ["v1"]
    resources: ["pods"]
  failurePolicy: Fail
  sideEffects: None
  admissionReviewVersions: ["v1"]
  timeoutSeconds: 5

验证结果

# 部署一个 myapp deployment 进行尝试确认
kubectl  create deployment myapp --image=docker.cnb.cool/wangyanglinux/docker-images-chrom/myapp:v4.0

查看 pod 标签是否自动添加

1755229934481.png


截屏2025-08-15 11.53.03.png

0

评论区