kubernetes 动态webhook的使用
准入控制器概念k8s apiserver 在处理每一个操作资源对象的请求时,在经过认证(是否为合法用户)/ 鉴权(用户是否拥有权限)后,并不会直接根据端点资源类型和 rest 动作直接执行操作,在这中间,请求会被一系列准入控制器插件(Admission Controller)进行拦截,校验其是否合乎要求,亦或是对特定资源进行配置。准入机制如果所有的 webhooks 批准请求,准入控制链继续流转;
准入控制器概念
k8s apiserver 在处理每一个操作资源对象的请求时,在经过认证(是否为合法用户)/ 鉴权(用户是否拥有权限)后,并不会直接根据端点资源类型和 rest 动作直接执行操作,在这中间,请求会被一系列准入控制器插件(Admission Controller)进行拦截,校验其是否合乎要求,亦或是对特定资源进行配置。
准入机制
- 如果所有的 webhooks 批准请求,准入控制链继续流转;
- 如果有任意一个 webhooks 阻止请求,那么准入控制请求终止,并返回第一个 webhook 阻止的原因。其中,多个 webhooks 阻止也只会返回第一个 webhook 阻止的原因;
- 如果在调用 webhook 过程中发生错误,那么请求会被终止或者忽略 webhook
准入控制器类型
- 变更 (Mutating):这种控制器可以解析请求,并在请求向下发送之前对请求进行更改;
- 验证 (Validating): 这种控制器可以解析请求并根据特定数据进行验证;
3.Both:这种控制器可以执行变更和验证两种操作,如默认开启的 LimitRanger 准入控制器在变更阶段使用默认的资源配置来限制容器对资源的使用,并在验证阶段确保容器的资源限制不超过预期的。
对应的,准入控制分为两个阶段,变更控制器会优先与验证控制器在第一阶段执行,需要注意的是变更控制器会更改请求对象,验证控制器则不行。
动态准入控制
除了 k8s 自带的默认控制器,我们还可以以 webhook 的形式去编写自定义的准入控制器,对特定资源的请求进行个性化的修改和验证,典型的场景有通过 mutating 类型 webhook 注入 side-car 到 pod,利用 validating 类型 webhook 实现 crd 资源相关字段的规则验证。
自定义动态准入控制器
这里我们根据官方提供的demo来自定义一个简单的准入控制器,实现以下目标功能:
- 验证 pod 名称是否符合规范;
- 修改 pod 主容器镜像;
- 注入 initContainer 容器。
配置步骤:
1. 通过 cfssl 生成 ca 证书:
root# cat ca-config.json
{
"signing": {
"default": {
"expiry": "8760h"
},
"profiles": {
"server": {
"usages": ["signing"],
"expiry": "8760h"
}
}
}
}
root# cat ca-csr.json
{
"CN": "Kubernetes",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "zh",
"L": "zj",
"O": "hz",
"OU": "CA"
}
]
}
root# cfssl gencert -initca ca-csr.json | cfssljson -bare ca
root# ls
ca-key.pem
ca.csr
ca.pem
server.pem
2. 配置 k8s secret
root# kubectl create secret tls myhook --cert=server.pem --key=server-key.pem -n kube-system
root# kubectl get secret -n kube-system |grep myhook
myhook kubernetes.io/tls 2 64m
3. 配置一个 MutatingWebhook 类型准入控制器
root# cat admission.yaml
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: myhook
webhooks:
- clientConfig:
caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURZakNDQWtxZ0F3SUJBZ0lVTW9GMWlJN1p5YnR3R21pTmZXZ0Q2OWFjSEJVd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1NURUxNQWtHQTFVRUJoTUNlbWd4Q3pBSkJnTlZCQWNUQW1KcU1Rc3dDUVlEVlFRS0V3SmlhakVMTUFrRwpBMVVFQ3hNQ1EwRXhFekFSQmdOVkJBTVRDa3QxWW1WeWJtVjBaWE13SGhjTk1qRXhNakkxTVRVMU1qQXdXaGNOCk1qWXhNakkwTVRVMU1qQXdXakJKTVFzd0NRWURWUVFHRXdKNmFERUxNQWtHQTFVRUJ4TUNZbW94Q3pBSkJnTlYKQkFvVEFtSnFNUXN3Q1FZRFZRUUxFd0pEUVRFVE1CRUdBMVVFQXhNS1MzVmlaWEp1WlhSbGN6Q0NBU0l3RFFZSgpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFMNEtQbEl5K1lpZ1k0MmM0RXNaOE80RHpFaE5VOXVHCjV1ejJmTmZEUi95MlpkSSttcDJENFJIc3UyTzhiRW1oTHBQZklieWhTL0Vxa2Y4L3hvNHhnVmhvK3Z3eUg5Rm4KNm53OWk5Slp0Qzc5RWY4dlhCbkprbWo4Qi9mTGxZVWluT3RzMVFqZ1FiN0dhWUgxQjY4NHdhK2VBSHZMSHAvYwpRZFkvSG1Wd2VwNG9odnM1allLVGRvT25QMWlhNUJtU2ZaZEY4bFcvZzF6ODRYTDFkdG55dXQyRkdTTFZpNmgyCmtZK1pHNlRLWGxtL1kyUldmYTZzOEZrMDBkcXJybTVVVmVlY0ZFQUQ3VjY0S3lxdFpWcXRIUzJrd1ArbzRWSHcKS0pDNjZEVmdwNTV1NGFlbTQ1bmJQNG5SekQ5VHNoWXl1RE8vdTBkS1dvZlp6a21WZHpReERTc0NBd0VBQWFOQwpNRUF3RGdZRFZSMFBBUUgvQkFRREFnRUdNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdIUVlEVlIwT0JCWUVGSjJHCjhNYnpiVW83S3pmOGRzdHJoaXc2L1FZR01BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQjZkb3ZXQjB6ajJRUUwKYlZ0akdTQ0JBZHFNeXFvakRqRk9JMmhFWWR3dVIyc2oxN2xSQjU1UGxzeHdkWU04VGp1UEUyUXAvbjVZNFJXRgpkMFVWeDdCSTJGeG84V0ZmaEZnaGo4cm9uMHhOay84UlBvYk5vUVM0NC9kRHZHMWZLbFYyUlJFMXBMdVBNTWRPCjZxcXc3TTBKd2Q1K051OEc0M1VyZU5aTHpKSUlic3UyRmwyZ3JicG56ZGhUUXlNMUdwamMxR3BiMSt4dUZxYkcKd1R5SjNhNG9DaVlZRHhCUmVRWHJWeFcwaHB3Y2EyeW5wenY1YmIyd3FBRXVJemZEK2llYkZTaEFVSTJPQzJ1YwpXMXpYSlkyTnl6eEdCdmVtRWRtQ1VVTWh3S3RPdDhIdTlSRVhJUk5LSnExSzRSTjZNOTVpMmpPZFhGZjRaQ2JtCmZuc0svbHppCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K # caBundle 为ca客户端证书,由 cat ca.pem |base64 解码而来
service:
name: myhook
namespace: kube-system
path: /pods # webhook url
failurePolicy: Fail
sideEffects: NoneOnDryRun
name: myhook.wholeeprime.com
objectSelector: # 资源匹配器
matchLabels:
app: "wholeeprime"
admissionReviewVersions: ["v1", "v1beta1"]
namespaceSelector: {}
rules:
- apiGroups: [""] # 列出要匹配的 API 组。"" 是核心 API 组。"*" 匹配所有 API 组。 kubectl api-resources查看
apiVersions: ["v1"] # 列出要匹配的 API 版本。"*" 匹配所有 API 版本。
operations: ["CREATE"] # 列出要匹配的操作。 可以是 CREATE、UPDATE、DELETE、CONNECT 或 * 以匹配所有内容。
resources: ["pods"] # 列出了一个或多个要匹配的资源。
scope: "Namespaced" # 指定要匹配的范围。有效值为 "Cluster"、"Namespaced" 和 "*"。
4. 编写 webhook
// main.go
package main
import (
"encoding/json"
"k8s.io/klog/v2"
"log"
"myhook/lib"
"io/ioutil"
"k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"net/http"
)
func main() {
http.HandleFunc("/pods", func(w http.ResponseWriter, r *http.Request) {
log.Println(r.RequestURI)
var body []byte
if r.Body != nil {
if data, err := ioutil.ReadAll(r.Body); err == nil {
body = data
}
}
//第二步
reqAdmissionReview := v1.AdmissionReview{} //请求
rspAdmissionReview := v1.AdmissionReview{ //响应 ---只构建了一部分
TypeMeta: metav1.TypeMeta{
Kind: "AdmissionReview",
APIVersion: "admission.k8s.io/v1",
},
}
//第三步。 把body decode 成对象
deserializer := lib.Codecs.UniversalDeserializer()
if _, _, err := deserializer.Decode(body, nil, &reqAdmissionReview); err != nil {
klog.Error(err)
rspAdmissionReview.Response = lib.ToV1AdmissionResponse(err)
} else {
rspAdmissionReview.Response = lib.AdmitPods(reqAdmissionReview) //我们的业务
}
rspAdmissionReview.Response.UID = reqAdmissionReview.Request.UID
respBytes,_ := json.Marshal(rspAdmissionReview)
w.Write(respBytes)
})
tlsConfig := lib.Config{
CertFile: "/etc/webhook/certs/tls.crt",
KeyFile: "/etc/webhook/certs/tls.key",
}
server := &http.Server{
Addr: ":443",
TLSConfig: lib.ConfigTLS(tlsConfig),
}
klog.Error(server.ListenAndServeTLS("",""))
}
// lib/pod.go
package lib
import (
"fmt"
"k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog/v2"
)
func pathContainer() []byte{
str:=`[
{
"op" : "replace" ,
"path" : "/spec/containers/0/image" ,
"value" : "nginx:1.19-alpine"
},
{
"op": "add",
"path": "/spec/initContainers",
"value": [{
"name": "myinit",
"image": "busybox:1.28",
"command": ["sh", "-c", "echo The app is running!"]
}]
}
]`
return []byte(str)
}
func AdmitPods(ar v1.AdmissionReview) *v1.AdmissionResponse {
podResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
if ar.Request.Resource != podResource {
err := fmt.Errorf("expect resource to be %s", podResource)
klog.Error(err)
return ToV1AdmissionResponse(err)
}
raw := ar.Request.Object.Raw
pod := corev1.Pod{}
deserializer := Codecs.UniversalDeserializer()
if _, _, err := deserializer.Decode(raw, nil, &pod); err != nil {
klog.Error(err)
return ToV1AdmissionResponse(err)
}
reviewResponse := v1.AdmissionResponse{}
if pod.Name=="dasha"{ // 模拟验证镜像名称是否符合规范
reviewResponse.Allowed = false
reviewResponse.Result = &metav1.Status{Code:503,Message: "pod name cannot be dasha"}
}else{
reviewResponse.Allowed = true
reviewResponse.Patch = pathContainer()
pt:=v1.PatchTypeJSONPatch // 通过patch补丁方式来实现主容器镜像的修改以及initContainer的注入,补丁策略需要为PatchTypeJSONPatch
reviewResponse.PatchType=&pt
}
return &reviewResponse
}
// lib/scheme.go
var Codecs = serializer.NewCodecFactory(scheme)
func init() {
addToScheme(scheme)
}
func addToScheme(scheme *runtime.Scheme) {
utilruntime.Must(corev1.AddToScheme(scheme))
utilruntime.Must(admissionv1beta1.AddToScheme(scheme))
utilruntime.Must(admissionregistrationv1beta1.AddToScheme(scheme))
utilruntime.Must(admissionv1.AddToScheme(scheme))
utilruntime.Must(admissionregistrationv1.AddToScheme(scheme))
}
// lib/config.go
package lib
import (
"crypto/tls"
"k8s.io/klog/v2"
)
// Config contains the server (the webhook) cert and key.
type Config struct {
CertFile string
KeyFile string
}
func ConfigTLS(config Config) *tls.Config {
sCert, err := tls.LoadX509KeyPair(config.CertFile, config.KeyFile)
if err != nil {
klog.Fatal(err)
}
return &tls.Config{
Certificates: []tls.Certificate{sCert},
// TODO: uses mutual tls after we agree on what cert the apiserver should use.
// ClientAuth: tls.RequireAndVerifyClientCert,
}
}
5. 将 webhook 打包成镜像
root# cat Dockerfile
FROM golang:1.16-alpine
MAINTAINER dasha@test.com
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk add --no-cache openssl-dev tzdata libffi-dev gcc g++ make
ENV TIME_ZONE=Asia/Shanghai
ENV GOPROXY="https://goproxy.cn,direct"
RUN echo "${TIME_ZONE}" > /etc/timezone \
&& ln -sf /usr/share/zoneinfo/${TIME_ZONE} /etc/localtime
VOLUME ["/tmp"]
WORKDIR /go/src/myhook
ADD . .
RUN go build -i -o "myhook" -gcflags "-N -l" main.go
WORKDIR /usr/local/bin/myhook
RUN mv /go/src/myhook/myhook .
ENTRYPOINT ["/bin/sh", "-c", "./myhook"]
root# docker build -t harbor.test.com/prod/admission:v1 .
root# docker push harbor.test.com/prod/admission:v1 # 推送到镜像仓库
6. 在 k8s 集群中部署 webhook
root# cat webhook.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myhook
namespace: kube-system
spec:
replicas: 1
selector:
matchLabels:
app: myhook
template:
metadata:
labels:
app: myhook
spec:
containers:
- name: myhook
image: harbor.test.com/prod/admission:v1
imagePullPolicy: IfNotPresent
volumeMounts:
- name: hooktls
mountPath: /etc/webhook/certs
readOnly: true
ports:
- containerPort: 443
imagePullSecrets:
- name: harbor-key
volumes:
- name: hooktls
secret:
secretName: myhook
---
apiVersion: v1
kind: Service
metadata:
name: myhook
namespace: kube-system
labels:
app: myhook
spec:
type: ClusterIP
ports:
- port: 443
targetPort: 443
selector:
app: myhook
#部署准入控制器和webhook
root# kubectl apply -f webhook.yaml
root# kubectl apply -f admission.yaml
#查看
root# kubectl get MutatingWebhookConfiguration |grep myhook
myhook 1 33m
root# kubectl get deploy -n kube-system|grep myhook
myhook 1/1 1 1 35m
6. 创建一个 pod 对我们自定义的准入控制器进行测试
# 验证pod名称是否符合规范
root# cat pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: daicheng
namespace: default
labels:
app: wholeeprime
spec:
containers:
- name: nginx
image: nginx:1.18-alpine
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
root# kubectl apply -f pod.yaml
Error from server: error when creating "pod.yaml": admission webhook "myhook.test.com" denied the request: pod name cannot be dasha
#验证是否能够修改pod主容器镜像版本和注入初始化容器
root# cat pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: dasha-test
namespace: default
labels:
app: dasha-test
spec:
containers:
- name: nginx
image: nginx:1.18-alpine
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
root# kubectl apply -f pod.yaml
pod/dasha-test created
root# kubectl describe pod/dasha-test
...
参考文章以及相关实现代码:
https://kubernetes.io/zh/docs/reference/access-authn-authz/admission-controllers/
https://kubernetes.io/zh/docs/reference/access-authn-authz/extensible-admission-controllers/
https://github.com/kubernetes/kubernetes/tree/588a4569a768306ea91821ff916185d65faa85a8/test/images/agnhost/webhook
更多推荐
所有评论(0)