图解Kubernetes 中的 Webhook

277 阅读4分钟

什么是 MutatingWebhook?

Kubernetes 中的 MutatingWebhook 是一种 Webhook,允许在将资源保留在 Kubernetes API 服务器中之前对其进行修改。这可用于在创建或更新资源时对资源实施某些策略或配置。
例如,MutatingWebhook 可用于自动向 Kubernetes pod 添加特定标签或注释,或将环境变量注入容器。Webhook 由特定事件(例如创建或更新)触发,并在持久化之前修改对象。

MutatingWebhooks 在 Kubernetes MutatingWebhookConfiguration 对象中定义,该对象指定 Webhook 应用的资源类型、调用 Webhook 的端点以及触发 Webhook 的操作类型(创建、更新、删除)。
一旦定义了 webhook 并将其注册到 API 服务器,当创建、更新或删除匹配的资源时,它将自动调用。它可以根据其验证过程接受或拒绝资源。

以下是向 pod 添加标签的 MutatingWebhook 清单文件示例:

apiVersion: admissionregistration.k8s.io/v1  
kind: MutatingWebhookConfiguration  
metadata:  
name: add-label-webhook  
webhooks:  
- name: add-label.example.com  
clientConfig:  
url: https://webhook.example.com/add-label  
caBundle:  
...  
rules:  
- operations: [ "CREATE", "UPDATE" ]  
apiGroups: ["", "apps", "batch"]  
apiVersions: ["v1", "v1beta1", "v1beta2"]  
resources: ["pods"]  
failurePolicy: Ignore

值得注意的是,这只是一个示例,确切的配置将取决于具体的用例和要求。
MutatingWebhook 的 Kubernetes 清单文件通常包含以下字段:

apiVersion:资源所属的 Kubernetes API 的版本。

kind:清单中定义的资源类型。对于 MutatingWebhook,这将是“MutatingWebhookConfiguration”。

元数据:有关资源的元数据,包括其名称和标签。

webhooks:webhooks 数组,每个 webhooks 包含以下字段:

  • name : webhook 的唯一名称
  • clientConfig:调用 webhook 的客户端的配置,包括 url 端点和用于验证 webhook 的服务器证书的 caBundle。caBundle 是一个 PEM 编码的 CA 包,将用于验证 webhook 服务器的证书。
  • Rules:用于触发 Webhook 的规则数组,包括 Webhook 所应用的操作(创建、更新、删除)和资源。
  • failurePolicy:处理失败的策略,例如 Ignore 或 Fail。

在 Kubernetes 中改变 Webhook 的一些其他用例包括:

  • 自动 sidecar 注入:mutating webhook 可用于自动将 sidecar 容器注入到 pod 中,例如提供日志记录或监控功能。
  • 自动配置注入:mutating Webhook 可用于自动将配置文件或环境变量注入容器中,例如提供数据库连接详细信息或 API 密钥。
  • 自动安全增强:mutating Webhook 可用于自动向 pod 添加安全相关配置,例如添加安全上下文或配置网络策略。
  • 自动缩放:可以使用 mutating webhook 根据 pod 的资源使用情况自动配置水平 pod 自动缩放器 (HPA)。
  • 自动修补:mutating Webhook 可用于自动修补运行易受攻击的软件版本的容器。
  • 自动验证:mutating Webhook 可用于在创建 pod 或服务之前自动验证其配置,如果不满足某些条件则拒绝请求。

以下是 YAML 中的变异 Webhook 清单文件的示例,该文件将标签注入到 my-namespace 命名空间中的 pod 中:

apiVersion: admissionregistration.k8s.io/v1  
kind: MutatingWebhookConfiguration  
metadata:  
name: inject-labels-webhook  
webhooks:  
- name: inject-labels.example.com  
rules:  
- apiGroups: [""]  
apiVersions: ["v1"]  
resources: ["pods"]  
namespaceSelector:  
matchExpressions:  
- {key: "name", operator: "In", values: ["core-services"]}  
failurePolicy: Fail  
clientConfig:  
service:  
name: webhook-service  
namespace: webhook-namespace  
path: "/inject"  
caBundle: <base64 encoded ca bundle>

image.png

此清单创建一个名为ject-labels-webhook_的MutatingWebhook 配置和一个名为_inject-labels.example.com_的Webhook 。webhook 配置为仅对**v1 apiGroup 中的 pod 进行变异,如果变异失败则失败。Webhook 还配置为仅改变 my-namespace 命名空间中的 pod。Webhook 由 webhook-namespace 命名空间中名为 webhook-service 的服务支持,并调用该服务上的/inject路径。clientConfig 还包含一个 caBundle 字段,该字段应包含用于保护 Webhook 服务的证书的 Base64 编码 CA 捆绑包。 此清单使用 namespaceSelector 字段来指定 webhook 应该仅改变 my-namespace 命名空间中的 pod。matchExpressions 字段用于指定名称标签应使用 In 运算符与值 my-namespace 匹配。这意味着 webhook 只会在 my-namespace 命名空间中的 pod 资源上调用。

以下是Go 中 mutating Webhook 示例,它将标签注入到 pod 中:

package main  
  
import (  
"encoding/json"  
"fmt"  
"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/runtime"  
"k8s.io/apimachinery/pkg/runtime/serializer"  
)  
  
var (  
runtimeScheme = runtime.NewScheme()  
codecs = serializer.NewCodecFactory(runtimeScheme)  
deserializer = codecs.UniversalDeserializer()  
)  
  
func main() {  
http.HandleFunc("/inject", inject)  
http.ListenAndServe(":8080", nil)  
}  
  
func inject(w http.ResponseWriter, r *http.Request) {  
var body []byte  
if r.Body != nil {  
if data, err := ioutil.ReadAll(r.Body); err == nil {  
body = data  
}  
}  
  
if len(body) == 0 {  
http.Error(w, "no body found", http.StatusBadRequest)  
return  
}  
  
contentType := r.Header.Get("Content-Type")  
if contentType != "application/json" {  
http.Error(w, "invalid Content-Type, want `application/json`", http.StatusUnsupportedMediaType)  
return  
}  
  
var admissionResponse *admissionv1.AdmissionResponse  
ar := admissionv1.AdmissionReview{}  
if _, _, err := deserializer.Decode(body, nil, &ar); err != nil {  
admissionResponse = &admissionv1.AdmissionResponse{  
Result: &metav1.Status{  
Message: err.Error(),  
},  
}  
} else {  
admissionResponse = mutatePods(ar)  
}  
  
admissionReview := admissionv1.AdmissionReview{}  
if admissionResponse != nil {  
admissionReview.Response = admissionResponse  
if ar.Request != nil {  
admissionReview.Response.UID = ar.Request.UID  
}  
}  
  
resp, err := json.Marshal(admissionReview)  
if err != nil {  
http.Error(w, fmt.Sprintf("could not encode response: %v", err), http.StatusInternalServerError)  
}  
if _, err := w.Write(resp); err != nil {  
http.Error(w, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError)  
}  
}  
  
func mutatePods(ar admissionv1.AdmissionReview) *admissionv1.AdmissionResponse {  
req := ar.Request  
  
var pod corev1.Pod