WEBKT

Go 实战:Kubernetes Admission Webhook 实现 Sidecar 自动注入,你需要考虑的都在这

20 0 0 0

1. 啥是 Admission Webhook?

2. 整体思路

3. Go 代码实现

3.1 项目初始化

3.2 定义 Webhook Server

3.3 辅助函数

3.4 证书生成

3.5 构建镜像

4. Kubernetes 配置

4.1 部署 Webhook Server

4.2 配置 MutatingWebhookConfiguration

5. 测试

5.1 为 Namespace 添加 Label

5.2 创建 Pod

6. 需要考虑的问题

7. 总结

想用 Go 撸一个 Kubernetes Admission Webhook,在 Pod 创建的时候,自动给 Pod 注入 Sidecar 容器?这绝对是个好主意! 很多时候,我们需要在不修改应用代码的情况下,给应用增加一些额外的功能,比如监控、日志、安全等等,Sidecar 模式简直是完美解决方案。 Kubernetes 的 Admission Webhook 机制,则为我们提供了在 Pod 创建时动态修改其配置的能力。 那么,怎么用 Go 把它实现呢? 又有哪些坑需要提前避开? 别着急,咱一步一步来。

1. 啥是 Admission Webhook?

先简单科普一下。 Kubernetes Admission Webhook 就像 Kubernetes API Server 的“守门员”,Pod 提交创建请求,要先经过它这一关。 它可以拦截 API 请求,在对象被持久化到 etcd 之前,对请求进行验证(Validating Admission Webhook)或者修改(Mutating Admission Webhook)。 我们这里要实现 Sidecar 自动注入,属于 Mutating Admission Webhook 的范畴。

2. 整体思路

简单来说,我们需要做这么几件事:

  1. 编写 Webhook Server: 接收 Kubernetes API Server 发送的 AdmissionReview 请求,根据请求中的 Pod 信息,判断是否需要注入 Sidecar,如果需要,则修改 Pod 的 spec,添加 Sidecar 容器的定义。
  2. 配置 Kubernetes: 告诉 Kubernetes API Server,哪些请求需要发送到我们的 Webhook Server。 这需要创建 MutatingWebhookConfiguration 对象。
  3. 部署 Webhook Server: 将 Webhook Server 部署到 Kubernetes 集群中,并确保 API Server 可以访问到它。

3. Go 代码实现

3.1 项目初始化

首先,创建一个 Go 项目:

mkdir sidecar-injector
cd sidecar-injector
go mod init sidecar-injector

然后,引入需要的 Kubernetes 依赖:

go get k8s.io/api k8s.io/apimachinery k8s.io/client-go

3.2 定义 Webhook Server

创建一个 main.go 文件,作为 Webhook Server 的入口。 核心逻辑是处理 AdmissionReview 请求,并返回 AdmissionReview 响应。 响应中需要包含 patch 字段,告诉 API Server 如何修改 Pod 的配置。

package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
injectAnnotation = "sidecar-injector/inject"
sidecarContainerName = "my-sidecar" // Sidecar 容器的名称
sidecarImage = "busybox:latest" // Sidecar 镜像
)
func main() {
http.HandleFunc("/mutate", mutateHandler)
log.Fatal(http.ListenAndServeTLS(":8443", "tls.crt", "tls.key", nil))
}
func mutateHandler(w http.ResponseWriter, r *http.Request) {
// 1. 校验请求
if r.Header.Get("Content-Type") != "application/json" {
http.Error(w, "Invalid Content-Type, expected application/json", http.StatusUnsupportedMediaType)
return
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to read request body: %v", err), http.StatusBadRequest)
return
}
// 2. 反序列化 AdmissionReview 请求
ar := v1.AdmissionReview{}
if err := json.Unmarshal(body, &ar); err != nil {
http.Error(w, fmt.Sprintf("Failed to unmarshal AdmissionReview: %v", err), http.StatusBadRequest)
return
}
// 3. 处理 AdmissionReview 请求
respAr := admit(ar)
// 4. 序列化 AdmissionReview 响应
resp, err := json.Marshal(respAr)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to marshal AdmissionReview response: %v", err), http.StatusInternalServerError)
return
}
// 5. 返回响应
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(resp)
}
func admit(ar v1.AdmissionReview) *v1.AdmissionReview {
req := ar.Request
if req == nil {
log.Println("AdmissionReview request is nil")
return &v1.AdmissionReview{
Response: &v1.AdmissionResponse{
UID: req.UID,
Allowed: true,
},
}
}
log.Printf("AdmissionReview for Kind=%v, Namespace=%v Name=%v UID=%v Operation=%v UserInfo=%v",
req.Kind, req.Namespace, req.Name, req.UID, req.Operation, req.UserInfo)
// 只处理 Pod 的创建请求
if req.Kind.Kind != "Pod" {
log.Printf("Ignoring non-Pod resource: %s", req.Kind.Kind)
return &v1.AdmissionReview{
Response: &v1.AdmissionResponse{
UID: req.UID,
Allowed: true,
},
}
}
// 4. 反序列化 Pod 对象
pod := corev1.Pod{}
if err := json.Unmarshal(req.Object.Raw, &pod); err != nil {
log.Printf("Failed to unmarshal Pod object: %v", err)
return &v1.AdmissionReview{
Response: &v1.AdmissionResponse{
UID: req.UID,
Allowed: true,
Result: &metav1.Status{
Message: err.Error(),
},
},
}
}
// 5. 判断是否需要注入 Sidecar
if !shouldInjectSidecar(&pod) {
log.Println("Sidecar injection not required")
return &v1.AdmissionReview{
Response: &v1.AdmissionResponse{
UID: req.UID,
Allowed: true,
},
}
}
// 6. 构造 Sidecar 容器
sidecarContainer := corev1.Container{
Name: sidecarContainerName,
Image: sidecarImage,
Command: []string{"sh", "-c", "while true; do sleep 3600; done"},
}
// 7. 生成 patch
patchBytes, err := createPatch(&pod, sidecarContainer)
if err != nil {
log.Printf("Failed to create patch: %v", err)
return &v1.AdmissionReview{
Response: &v1.AdmissionResponse{
UID: req.UID,
Allowed: true,
Result: &metav1.Status{
Message: err.Error(),
},
},
}
}
// 8. 构造 AdmissionReview 响应
resp := v1.AdmissionReview{
Response: &v1.AdmissionResponse{
UID: req.UID,
Allowed: true,
PatchType: func() *v1.PatchType {
pt := v1.PatchTypeJSONPatch
return &pt
}(),
Patch: patchBytes,
},
}
return &resp
}
// 判断是否需要注入 Sidecar
func shouldInjectSidecar(pod *corev1.Pod) bool {
annotations := pod.ObjectMeta.Annotations
if annotations == nil {
return false
}
inject, ok := annotations[injectAnnotation]
if !ok {
return false
}
return inject == "true"
}
// 生成 JSON Patch
func createPatch(pod *corev1.Pod, sidecarContainer corev1.Container) ([]byte, error) {
patch := []map[string]interface{}{{
"op": "add",
"path": "/spec/containers/-",
"value": sidecarContainer,
}}
patchBytes, err := json.Marshal(patch)
if err != nil {
return []byte{}, err
}
return patchBytes, nil
}

3.3 辅助函数

上面的代码中,用到了几个辅助函数,我们来分别实现一下。

  • shouldInjectSidecar: 判断 Pod 是否需要注入 Sidecar。 这里我们通过 Pod 的 annotation 来控制,如果 Pod 的 sidecar-injector/inject annotation 的值为 true,则注入 Sidecar。
  • createPatch: 生成 JSON Patch,用于修改 Pod 的配置。 JSON Patch 是一种轻量级的 Patch 格式, Kubernetes API Server 可以根据 Patch 的内容,对 Pod 的配置进行增、删、改、查操作。

3.4 证书生成

Admission Webhook 需要使用 HTTPS 协议,所以我们需要生成 TLS 证书。 可以使用 openssl 命令生成自签名证书:

openssl req -x509 -newkey rsa:2048 -keyout tls.key -out tls.crt -days 365 -nodes -subj '/CN=sidecar-injector.default.svc'

注意: CN (Common Name) 需要设置为 Webhook Server 的 Service 名称。 例如,如果你的 Webhook Server 部署在 default namespace 下,Service 名称为 sidecar-injector,则 CN 应该设置为 sidecar-injector.default.svc

3.5 构建镜像

编写 Dockerfile:

FROM golang:1.18-alpine AS builder

WORKDIR /app

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

COPY . .

RUN go build -o sidecar-injector

FROM alpine:latest

WORKDIR /app

COPY --from=builder /app/sidecar-injector .
COPY tls.crt tls.key ./

CMD ["./sidecar-injector"]

构建镜像并推送到镜像仓库:

docker build -t your-registry/sidecar-injector:latest .
docker push your-registry/sidecar-injector:latest

4. Kubernetes 配置

4.1 部署 Webhook Server

创建一个 Deployment,将 Webhook Server 部署到 Kubernetes 集群中。

apiVersion: apps/v1
kind: Deployment
metadata:
name: sidecar-injector
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: sidecar-injector
template:
metadata:
labels:
app: sidecar-injector
spec:
containers:
- name: sidecar-injector
image: your-registry/sidecar-injector:latest
imagePullPolicy: Always
ports:
- containerPort: 8443
volumeMounts:
- name: tls
mountPath: /app/tls
readOnly: true
volumes:
- name: tls
secret:
secretName: sidecar-injector-tls

创建一个 Service,暴露 Webhook Server 的服务。

apiVersion: v1
kind: Service
metadata:
name: sidecar-injector
namespace: default
spec:
selector:
app: sidecar-injector
ports:
- port: 443
targetPort: 8443

创建一个 Secret,存储 TLS 证书。

kubectl create secret tls sidecar-injector-tls --cert=tls.crt --key=tls.key -n default

4.2 配置 MutatingWebhookConfiguration

创建一个 MutatingWebhookConfiguration 对象,告诉 Kubernetes API Server,哪些请求需要发送到我们的 Webhook Server。

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: sidecar-injector
webhooks:
- name: sidecar-injector.example.com
clientConfig:
service:
name: sidecar-injector
namespace: default
path: /mutate
caBundle: $(base64 < tls.crt)
rules:
- operations: ["CREATE"]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
namespaceSelector:
matchExpressions:
- key: sidecar-injector
operator: In
values: [enabled]
admissionReviewVersions: ["v1", "v1beta1"]
sideEffects: None

注意:

  • clientConfig.service.nameclientConfig.service.namespace 需要设置为 Webhook Server 的 Service 名称和 Namespace。
  • clientConfig.caBundle 需要设置为 TLS 证书的 Base64 编码。
  • rules 定义了哪些请求需要发送到 Webhook Server。 这里我们配置的是,当创建 Pod 时,将请求发送到 Webhook Server。
  • namespaceSelector 定义了哪些 Namespace 下的 Pod 会被注入 Sidecar。 这里我们配置的是,只有当 Namespace 包含 sidecar-injector=enabled label 时,才会注入 Sidecar。

5. 测试

5.1 为 Namespace 添加 Label

kubectl label namespace default sidecar-injector=enabled

5.2 创建 Pod

创建一个 Pod,并添加 sidecar-injector/inject=true annotation。

apiVersion: v1
kind: Pod
metadata:
name: test-pod
annotations:
sidecar-injector/inject: "true"
spec:
containers:
- name: main
image: nginx:latest

查看 Pod 的 containers,可以看到 Sidecar 容器已经被注入。

kubectl describe pod test-pod

6. 需要考虑的问题

  • 性能影响: Admission Webhook 会增加 API Server 的请求处理时间,需要考虑 Webhook Server 的性能,避免影响 Kubernetes 集群的整体性能。
  • 高可用: 需要确保 Webhook Server 的高可用,避免单点故障。
  • 错误处理: Webhook Server 需要处理各种错误情况,例如请求超时、证书错误等等。 需要有完善的错误处理机制,避免影响 Pod 的创建。
  • 安全性: Webhook Server 需要进行身份验证和授权,避免未经授权的访问。
  • 版本兼容性: 需要考虑 Kubernetes API 的版本兼容性,避免因为 API 版本升级导致 Webhook Server 无法正常工作。
  • Sidecar 容器配置: Sidecar 容器的配置需要灵活可配置,例如镜像版本、资源限制、环境变量等等。 可以通过 ConfigMap 或者环境变量来配置 Sidecar 容器。
  • 注入策略: 可以通过 Namespace Label、Pod Annotation 等方式来控制 Sidecar 的注入策略。 需要根据实际情况选择合适的注入策略。
  • 与其他 Webhook 的冲突: 如果集群中存在多个 Admission Webhook,需要考虑它们之间的冲突。 可以通过 matchPolicy 字段来控制 Webhook 的匹配顺序。

7. 总结

使用 Go 语言编写 Kubernetes Admission Webhook,实现 Sidecar 自动注入,是一个非常有用的技巧。 它可以帮助我们在不修改应用代码的情况下,给应用增加额外的功能。 但是,在实际应用中,需要考虑很多问题,例如性能、高可用、错误处理、安全性等等。 只有充分考虑这些问题,才能编写出高质量的 Admission Webhook。 希望本文能够帮助你更好地理解 Kubernetes Admission Webhook 的原理和使用方法。 赶紧动手试试吧!

Sidecar大玩家 KubernetesAdmission WebhookGo

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/10164