lxcfs-hostpath-injector
Nov 14, 2019 21:30 · 2632 words · 6 minute read
有关 Kubernetes 集群中利用 LXCFS 提升容器的隔离能力,阿里云提供了一个利用 Initializer 扩展机制对资源创建进行拦截和注入处理的能力的方案 https://github.com/denverdino/lxcfs-initializer,实现对 lxcfs 所托管文件的自动化挂载。快速了解 Initializer,可以查看 https://medium.com/ibm-cloud/kubernetes-initializers-deep-dive-and-tutorial-3bc416e4e13e
但是 Kubernetes 从 1.14 版之后就移除了 Initializer,取而代之的是 admission webhooks。
Webhooks vs Initializers
根据 Kubernetest社区的反馈和 External Admission Webhooks 与 Initializers 在 alpha 阶段的使用情况,社区决定将 webhooks 提升至 beta 阶段,并拆分为 MutatingAdmissionWebhook 和 ValidatingAdmissionWebhook。
- Webhooks 可以 CRUD 和提交资源,而 Initializers 在 DELETE 请求时无法提交资源。
- 在创建之前 Webhooks 不允许查询资源,而 Initializers 通过查询参数
?includeUninitialized=true
来观察未初始化的资源,这让资源创建变得透明。 - Initializers 会将创建前的状态保存至 etcd,相比 Webhooks 更吃资源。
- Webhooks 的失败策略比 Initializers 更强大。在 Webhooks 配置中设置失败策略来避免资源创建时 hang 住,而 Initializers 的某些 bug 可能会导致资源创建阻塞。
将 Webhooks 提升至 beta 状态可能是一个未来对它更多支持的信号,如果首先考虑稳定性,建议使用 Webhooks。
MutatingAdmissionWebhook 工作原理
MutatingAdmissionWebhook 在 apiserver 将 API 持久化至 etcd 前拦截与 MutatingWebhookConfiguration
定义好的规则相匹配的请求。apiserver 将准入请求发送至 webhook 服务器来执行变异(修改)。webhook 本质上是一个遵循 Kubernetes API 的 HTTP 服务器应用程序。
MutatingAdmissionWebhook 要三样东西:
-
MutatingWebhookConfiguration 配置对象
通过
MutatingWebhookConfiguration
将 MutatingAdmissionWebhook 注册到 apiserver:- 如何连接 webhook admission server
- 如何验证 webhook admission server
- webhook admission server 的 URL 路径
- 定义哪种资源及其处理方法的规则
- 如何处理来自 webhook admission server 的一些无法识别的错误
-
MutatingAdmissionWebhook 本身
MutatingAdmissionWebhook 是个插件型的准入控制器,从
MutatingWebhookConfiguration
获取准入 webhook。MutatingAdmissionWebhook 会观察 apiserver 并拦截匹配规则的请求,调用 webhook。 -
Webhook Admission Server 服务器应用程序
Webhook Admission Server 是遵循 Kubernetes API 的 HTTP 服务器应用程序。每个发送至 apiserver 的请求,MutatingAdmissionWebhook 发送
AdmissionReview
对象至对应的 webhook admission server。webhook admission server 从AdmissionReview
获取像object
、oldobject
、userInfo
这样的信息,并反馈AdmissionReview
,携带Allowed
和Result
字段还有用于变异(修改) Pod 的 patch 等信息。
现在非常火热的的 Service Mesh 应用 istio 就是通过 MutatingAdmissionWebhook 来自动将 Envoy 这个 sidecar 容器注入到 Pod 中去的:https://istio.io/docs/setup/additional-setup/sidecar-injection/
确认 Kubernetes 集群是否支持 MutatingAdmissionWebhook
MutatingAdmissionWebhook 需要版本 1.9.0+ Kubernetes,通过下面的命令来确认:
$ kubectl api-versions | grep admissionregistration.k8s.io/v1beta1
admissionregistration.k8s.io/v1beta1
设计与实现
github repo: https://github.com/crazytaxii/lxcfs-hostpath-injector
首先需要构建一个 HTTP 服务器应用来提供 webhook 能力,参考 Kubernetes apiserver,我使用 cobra 来提供 CLI 接口。
webhook 服务器应用程序同时也是一个命令行应用,在启动时提供一些必要的参数:
fs.IntVar(&opt.Port, "port", defaultPort, "Webhook server port")
fs.StringVar(&opt.CertFile, "tls-cert-file", defaultCertFile, "File containing the x509 Certificate for HTTPS")
fs.StringVar(&opt.KeyFile, "tls-key-file", defaultKeyFile, "File containing the x509 private key to --tls-cert-file")
fs.StringVar(&opt.ConfigFile, "sidecar-config-file", webhook.DefaultConfigFile, "File containing sidecar configuration")
}
port
:服务器应用程序监听的端口,默认为 443tls-cert-file
:SSL 证书路径tls-key-file
:SSL 私钥路径sidecar-config-file
:配置文件路径
https://github.com/crazytaxii/lxcfs-hostpath-injector/blob/master/cmd/injector/app/server.go
// Section 1
cfg, err := webhook.LoadWebhookServerConfig(opt.ConfigFile)
if err != nil {
klog.Errorf("Failed to laod sidecar config: %v", err)
return err
}
// Section 2
// TLS
pair, err := tls.LoadX509KeyPair(opt.CertFile, opt.KeyFile)
if err != nil {
klog.Errorf("Failed to load key pair: %v", err)
return err
}
whsvr := &webhook.WebhookServer{
Config: cfg,
Server: &http.Server{
Addr: fmt.Sprintf(":%v", opt.Port),
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{pair},
},
},
}
// Section 3
mux := http.NewServeMux()
mux.HandleFunc("/mutate", whsvr.Mutate)
whsvr.Server.Handler = mux
// Section 4
go func() {
if err := whsvr.Server.ListenAndServeTLS("", ""); err != nil {
klog.Errorf("Failed to listen and serve webhook server: %v", err)
}
}()
- 载入相应的配置文件
- apiserver 与 webhook server 之间通讯必须走 HTTPS,需要 SSL 证书
- mux 用于路由与相应的 Handler 函数绑定
- 服务器应用程序开始监听
当 Pod 创建请求提交至 apiserver 时,将被拦截并向 webhook server 发送请求,/mutate 路径与下面的函数绑定:
// Mutate method for webhook server.
func (whsvr *WebhookServer) Mutate(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 {
klog.Error("empty body")
http.Error(w, "empty body", http.StatusBadRequest)
return
}
// verify the content type
contentType := r.Header.Get("Content-Type")
if contentType != "application/json" {
klog.Errorf("Content-Type=%s, expect application/json", contentType)
http.Error(w, "invalid Content-Type, expect `application/json`", http.StatusUnsupportedMediaType)
return
}
// Section 1
// The AdmissionReview that was sent to the webhook.
requestedAdmissionReview := &v1beta1.AdmissionReview{}
var admissionResponse *v1beta1.AdmissionResponse
if _, _, err := deserializer.Decode(body, nil, requestedAdmissionReview); err != nil {
klog.Errorf("Can't decode body: %v", err)
admissionResponse = toAdmissionResponseErr(err)
} else {
// Section 2
// pass to admitFunc
admissionResponse = whsvr.mutate(requestedAdmissionReview)
}
// The AdmissionReview that will be returned.
responseAdmissionReview := &v1beta1.AdmissionReview{}
if admissionResponse != nil {
responseAdmissionReview.Response = admissionResponse
if requestedAdmissionReview.Request != nil {
responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID
}
}
// Section 3
resp, err := json.Marshal(responseAdmissionReview)
if err != nil {
klog.Errorf("Can't encode response: %v", err)
http.Error(w, fmt.Sprintf("could not encode response: %v", err), http.StatusInternalServerError)
}
_, err = w.Write(resp)
if err != nil {
klog.Errorf("Can't write response: %v", err)
http.Error(w, fmt.Sprintf("could not encode response: %v", err), http.StatusInternalServerError)
}
}
Mutate
函数负责处理 HTTP 请求并返回结果:
- 首先将 HTTP 请求反序列化成
AdmissionReview
对象,其中包含了完整的 Pod 资源信息 - 调用
mutate()
函数来创建 patch 注入 hostPath 卷 - 最后序列化包含 patch 的
AdmissionReview
,发回 apiserver
https://github.com/crazytaxii/lxcfs-hostpath-injector/blob/master/pkg/webhook/webhook.go
func (whsvr *WebhookServer) mutate(ar *v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
req := ar.Request
var pod corev1.Pod
if err := json.Unmarshal(req.Object.Raw, &pod); err != nil {
klog.Errorf("Could not unmarshal raw object: %v", err)
return toAdmissionResponseErr(err)
}
klog.Infof("AdmissionReview for Kind=%v, Namespace=%v Name=%v UID=%v patchOperation=%v UserInfo=%v",
req.Kind, req.Namespace, req.Name, req.UID, req.Operation, req.UserInfo)
// Determine whether to perform mutation.
if !whsvr.mutationRequired(&pod.ObjectMeta) {
klog.Info("Skipping mutation")
return &v1beta1.AdmissionResponse{
Allowed: true,
}
}
annotations := map[string]string{admissionWebhookAnnotationStatusKey: "injected"}
patchBytes, err := createPatch(&pod, whsvr.Config.SidecarConfig, annotations)
if err != nil {
klog.Errorf("Could not create patch: %v", err)
return toAdmissionResponseErr(err)
}
return &v1beta1.AdmissionResponse{
Allowed: true,
Patch: patchBytes,
PatchType: func() *v1beta1.PatchType {
pt := v1beta1.PatchTypeJSONPatch
return &pt
}(),
}
}
mutate
方法调用了 mutationRequired 根据 Pod 携带的注解(annotations)来判断是否需要修改。如果注解中携带了 sidecar-injector.lxcfs/inject: true
,createPatch 生成 patch,如果没有或者值非 true
都会被忽略。完整的处理逻辑查看 https://github.com/crazytaxii/lxcfs-hostpath-injector/blob/master/pkg/webhook/webhook.go。
编译与构建镜像
$ git clone https://github.com/crazytaxii/lxcfs-sidecar-injector.git
$ cd lxcfs-sidecar-injector
$ export GO111MODULE=on
$ make build
$ make image
部署
在 Kubernetes 集群中创建相关 Deployment 和 Service 资源对象来部署 webhook server。HTTPS 服务器需要预先提供 SSL 证书和私钥,程序启动参数中读取的证书和私钥文件是通过一个 secret 对象挂载进来:
spec:
containers:
- name: lxcfs-sidecar-injector
image: crazytaxii/lxcfs-sidecar-injector:latest
imagePullPolicy: IfNotPresent
args:
- --tls-cert-file=/etc/webhook/certs/cert.pem
- --tls-key-file=/etc/webhook/certs/key.pem
ports:
- containerPort: 443
volumeMounts:
- name: webhook-certs
mountPath: /etc/webhook/certs
readOnly: true
volumes:
- name: webhook-certs
secret:
secretName: lxcfs-sidecar-injector-certs
在生产环境中要走 HTTPS,我们可以使用类似于 cert-manager 之类的工具来自动处理 SSL 证书。需要注意的是这里设置的 CA 证书是需要让 apiserver 能够验证的,这里重用了 istio 项目中的证书签名请求脚本。通过发送请求到 apiserver,获取认证信息,然后使用获得的结果来创建需要的 Secret。
$ ./kubernetes/webhook-create-signed-cert.sh
$ kubectl apply -f ./kubernetes/deployment.yaml
$ kubectl apply -f ./kubernetes/service.yaml
现在 webhook 服务运行起来了,它已经可以接收来自 apiserver 的请求。但是我们还需要在 kubernetes 上创建 MutatingWebhookConfiguration。查看 MutatingWebhookConfiguration,我注意到它里面包含一个 CA_BUNDLE 的占位符。
webhooks:
- name: lxcfs-sidecar-injector-svc.default.svc.cluster.local
clientConfig:
service:
name: lxcfs-sidecar-injector-svc
namespace: default
path: /mutate
caBundle: ${CA_BUNDLE}
CA 证书应提供给 admission webhook 配置 MutatingWebhookConfiguration 对象,这样 apiserver 才可以信任 webhook server 提供的 SSL 证书。因为上面已经使用 Kubernetes API 签署了证书,所以就用 kubeconfig 中的 CA 证书来简化操作。这里提供了一个小脚本用来替换 CA_BUNDLE 这个占位符,运行下面命令即可:
$ cat ./kubernetes/mutatingwebhook.yaml |\
./kubernetes/webhook-patch-ca-bundle.sh >\
./kubernetes/mutatingwebhookconfigurations.yaml
$ kubectl apply -f ./kubernetes/mutatingwebhookconfigurations.yaml
这样整个 admission webhook server 就部署好了。
测试
在注解中添加约定的 sidecar-injector.lxcfs/inject: "true"
并带上 requests 和 limits 字段(资源限制):
spec:
template:
metadata:
annotations:
sidecar-injector.lxcfs/inject: "true"
$ kubectl apply -f ./kubernetes/test-deloyment.yaml
$ kubectl exec -it $(kubectl get pod -l app=web -o jsonpath="{.items[0].metadata.name}") -- free -h
total used free shared buffers cached
Mem: 256M 2.9M 253M 0B 0B 364K
-/+ buffers/cache: 2.5M 253M
Swap: 0B 0B 0B
容器内部读取的资源配置被成功限制。