MutatingWebhookConfiguration
MutatingWebhookConfiguration 是 Kubernetes 的一种动态 Admission Webhook,用于在资源对象被创建或更新时修改请求对象。
它在对象写入 etcd 前拦截 API 请求,可以自动注入字段、添加默认值或修改特定配置,从而实现自动化管理和安全策略。
Certs
因为apiserver会向webhook发送一个HTTPS的请求,所以必须需要提前生成证书,其中ca.crt需要base64编译后使用。tls.key和tls.crt需要给webhook服务部署使用。
bash
#!/bin/bash
set -e
# 参数:域名
DOMAIN="$1"
if [ -z "$DOMAIN" ]; then
echo "Usage: $0 <domain>"
exit 1
fi
OUT_DIR="./tls"
mkdir -p "$OUT_DIR"
CA_KEY="$OUT_DIR/ca.key"
CA_CERT="$OUT_DIR/ca.crt"
TLS_KEY="$OUT_DIR/tls.key"
TLS_CERT="$OUT_DIR/tls.crt"
# 生成 CA
echo "[*] Generating CA..."
openssl genrsa -out "$CA_KEY" 4096
openssl req -x509 -new -nodes -key "$CA_KEY" -subj "/CN=$DOMAIN-CA" -days 36500 -out "$CA_CERT"
# 生成服务器私钥和 CSR
echo "[*] Generating server key and CSR..."
openssl genrsa -out "$TLS_KEY" 4096
openssl req -new -key "$TLS_KEY" -subj "/CN=$DOMAIN" -out "$OUT_DIR/tls.csr"
# 生成 server 证书
echo "[*] Signing server certificate with CA..."
cat > "$OUT_DIR/openssl.cnf" <<EOF
[ v3_ext ]
subjectAltName = DNS:$DOMAIN
EOF
openssl x509 -req -in "$OUT_DIR/tls.csr" -CA "$CA_CERT" -CAkey "$CA_KEY" -CAcreateserial \
-out "$TLS_CERT" -days 36500 -extfile "$OUT_DIR/openssl.cnf" -extensions v3_ext
# 输出 CA base64
echo "[*] Base64 encoded CA certificate:"
cat "$CA_CERT" | base64 | tr -d '\n'
echo -e "\n"
# 提示如何创建 K8s Secret
echo "[*] 可以用以下命令创建 Kubernetes Secret:"
echo "kubectl -n default create secret tls caddy-webhook-tls \\"
echo " --cert=$TLS_CERT \\"
echo " --key=$TLS_KEY"
# 清理中间文件
rm -f "$OUT_DIR/tls.csr" "$OUT_DIR/openssl.cnf" "$OUT_DIR/ca.srl" "$OUT_DIR/.srl"
echo "[*] Done! Files in $OUT_DIR:"
echo " $TLS_KEY"
echo " $TLS_CERT"
echo " $CA_CERT"Webhook
go
package main
import (
"crypto/tls"
"encoding/json"
admissionv1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"log"
"net/http"
"time"
)
func mutate(w http.ResponseWriter, r *http.Request) {
var review admissionv1.AdmissionReview
if err := json.NewDecoder(r.Body).Decode(&review); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
log.Printf("review: Kind=%s, Namespace=%s, Name=%s",
review.Request.Kind.Kind,
review.Request.Namespace,
review.Request.Name)
pod := corev1.Pod{}
if err := json.Unmarshal(review.Request.Object.Raw, &pod); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if pod.Labels == nil {
pod.Labels = map[string]string{}
}
pod.Labels["mutated"] = "true" // 给pod添加标签
patch := []map[string]interface{}{ // 基于patch更新
{
"op": "add",
"path": "/metadata/labels",
"value": pod.Labels,
},
}
patchBytes, err := json.Marshal(patch)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
admissionResponse := admissionv1.AdmissionReview{
TypeMeta: metav1.TypeMeta{
APIVersion: "admission.k8s.io/v1",
Kind: "AdmissionReview",
},
Response: &admissionv1.AdmissionResponse{
UID: review.Request.UID,
Allowed: true,
Patch: patchBytes,
PatchType: func() *admissionv1.PatchType { pt := admissionv1.PatchTypeJSONPatch; return &pt }(),
},
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(admissionResponse)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/mutate", mutate)
server := &http.Server{
Addr: ":8843",
Handler: mux,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
},
}
log.Println("Webhook listen on: https://0.0.0.0:8843/mutate")
if err := server.ListenAndServeTLS("./tls/tls.crt", "./tls/tls.key"); err != nil {
log.Fatalf("服务启动失败: %v", err)
}
}dockerfile
FROM docker.leejay.top/golang:1.24.5-alpine AS builder
WORKDIR /app
RUN apk add --no-cache git
COPY go.mod caddy-webhook.go ./
RUN go env -w GOPROXY=https://goproxy.cn,direct && go mod tidy
RUN go build caddy-webhook.go
FROM docker.leejay.top/alpine:3.18
WORKDIR /app
COPY --from=builder /app/caddy-webhook .
EXPOSE 8080
CMD ["./caddy-webhook"]yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: caddy-webhook
labels:
app: caddy-webhook
spec:
replicas: 1
selector:
matchLabels:
app: caddy-webhook
template:
metadata:
name: caddy-webhook
labels:
app: caddy-webhook
spec:
containers:
- name: caddy-webhook
image: abcsys.cn:5000/go/caddy-webhook # webhook编译的服务镜像
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8843
volumeMounts:
- name: caddy-webhook
mountPath: /app/tls
readOnly: true
restartPolicy: Always
volumes:
- name: caddy-webhook
secret:
secretName: caddy-webhook-tls
---
apiVersion: v1
kind: Service
metadata:
name: caddy-webhook
namespace: default
spec:
selector:
app: caddy-webhook
ports:
- protocol: TCP
port: 443
targetPort: 8843
type: ClusterIP
---
apiVersion: v1
kind: Secret
metadata:
name: caddy-webhook-tls
namespace: default
type: kubernetes.io/tls
data:
tls.crt: <tls.crt>
tls.key: <tls.key>Mwc
yaml
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: caddy-inject
webhooks:
- name: caddy.inject.kubernetes.io
sideEffects: None # 是否有副作用
failurePolicy: Fail # 调用webhook失败会抛出错误
admissionReviewVersions:
- "v1" # 传递给webhook的资源对象版本
clientConfig:
# url和service属性只能二选一
# url: https://caddy-webhook:8843/mutate # 回调的域名是caddy-webhook
service:
name: caddy-webhook # apiserver回调的域名是caddy-webhook.default.svc
namespace: default
port: 443 # svc port default 443
path: /mutate
caBundle: <caBundle>
rules: # 匹配符合的版本和操作
- apiGroups: [""]
apiVersions: [ "v1" ]
operations: [ "CREATE", "UPDATE" ]
resources: [ "pods" ]
namespaceSelector: {} # 匹配ns
objectSelector: # 匹配符合标签的资源对象
matchExpressions:
- key: app
operator: In
values:
- caddy-inject
- 在包含标签
app=caddy-inject的pods对象创建或更新的时候,会传递admissionReview-v1到https://caddy-webhook:8843/mutate,如果调用失败会抛出异常。- caBundle:将用于验证 Webhook 的服务证书。即webhook的tls证书对应的
ca.crt文件base64后的值。- 请注意不同的回调方式,对应的域名也不同,service的方式回调的是
caddy-webhook.default.svc,对应的证书也需要匹配。
测试
全部部署完毕后,我们只需要创建一个包含标签app=caddy-inject的pod,就会回调webhook服务,给pod加入一个mutated=true的标签。
yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx-test
namespace: default
labels:
app: caddy-inject
spec:
containers:
- name: nginx-test
image: abcsys.cn:5000/public/nginx
imagePullPolicy: IfNotPresent
restartPolicy: Always附录
本地调试
若需要apiserver请求本地的webhook服务,且没有域名的话,建议直接修改apiserver的域名解析,映射本地IP。
yaml
apiVersion: v1
kind: Pod
metadata:
name: kube-apiserver
namespace: kube-system
labels:
component: kube-apiserver
tier: control-plane
spec:
hostAliases:
- ip: 10.50.8.44
hostnames:
- "caddy-webhook"同时修改mwc的配置。
yaml
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: caddy-inject
webhooks:
- name: caddy.inject.kubernetes.io
sideEffects: None # 是否有副作用
failurePolicy: Fail # 调用webhook失败会抛出错误
admissionReviewVersions:
- "v1" # 传递给webhook的资源对象版本
clientConfig:
url: https://caddy-webhook:8843/mutate # 回调的域名是caddy-webhook
service:
name: caddy-webhook # apiserver回调的域名是caddy-webhook.default.svc
namespace: default
port: 443 # svc port default 443
path: /mutate
🎈🎈