好的,各位观众,各位朋友,欢迎来到今天的 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 的工作流程大致如下:
- 用户通过 kubectl 或 API 客户端发送请求到 Kubernetes API Server。
- API Server 接收到请求后,会先经过认证 (Authentication) 和鉴权 (Authorization)。
- 如果认证和鉴权都通过了,API Server 会调用 Admission Controller 进行准入控制。
- Admission Controller 会依次调用配置好的 Admission Webhooks。
- Mutating Admission Webhooks 先执行,对请求对象进行修改。
- Validating Admission Webhooks 后执行,对请求对象进行验证。
- 如果所有 Webhooks 都通过了,API Server 才会将请求对象持久化到 etcd 中。
- 如果任何一个 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.pem
和 key.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 集群,你做主! 🚀
感谢大家的观看,我们下期再见! 👋