除了 Kubernetes 默认的 admission controller,我们可以使用 admission webhooks 作为准入链的一部分。admission webhooks 调用 webhook 服务以在 pod 创建时变更配置,如注入标签,或者在准入过程中验证 pod 配置。下图为准入控制器流程:
Admission webhooks 有两种类型:
Mutating admission webhooks
用于在资源存储之前通过 mutating webhooks 进行修改Validating admission webhooks
用于在资源存储之前通过 validating webhooks 自定义策略验证资源
当 API 请求进入时,mutating 和 validating 控制器使用配置中的外部 webhooks 列表并发调用,规则如下:
- 如果所有的 webhooks 批准请求,准入控制链继续流转
- 如果有任意一个 webhooks 阻止请求,那么准入控制请求终止,并返回第一个 webhook 阻止的原因。其中,多个 webhooks 阻止也只会返回第一个 webhook 阻止的原因
- 如果在调用 webhook 过程中发生错误,那么请求会被终止或者忽略 webhook
准入控制器和 webhook 服务器之间通信需要使用 TLS 进行安全保护,因此要针对相应的 webhook 服务器生成 TLS 证书,这个后面会介绍具体流程。
Admission webhooks 可以使用如下几个场景:
- 通过 mutating webhook 注入 side-car 到 Pod(istio 的 side-car 就是采用这种方式注入的)
- 限制项目使用某个资源(如限制用户创建的 Pod 使用超过限制的资源等)
- 自定义资源的字段复杂验证(如 CRD 资源相关字段的规则验证等)
本文通过一个较为实用的场景来自定义编写 admission webhooks,使用 Kubernetes 的过程中可能遇到过 DNS 5s 延迟的问题,产生问题的原因可以具体参考官方 issue DNS intermittent delays of 5s 。这里我们通过编写 admission mutating webhook 注入 DNS 配置,以达到解决问题的目的。因为我们是针对 Pod 资源的 admission webhook,对于 CRD 的资源我们可以使用 kubebuilder 来生成脚手架。但是对于 core 类型的资源如 Pod,kubebuilder 并不支持,我们需要直接使用 controller-runtime 编写,官方提供了内建资源 webhook 的示例 controller-runtime/examples/builtins/ (CRD 资源建议参考 kubebuilder webhook)。validating 和 mutating webhook 类似,这里我们以 mutating webhook 为例。
首先构建 main
函数,main 主要是构建 manager 然后创建 webhooks 并注册 API:
package main
import (
"flag"
"os"
"github.com/spf13/pflag"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client/config"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/manager/signals"
"sigs.k8s.io/controller-runtime/pkg/webhook"
)
var (
scheme = runtime.NewScheme()
log = logf.Log.WithName("pod-admission-webhook")
)
func init() {
logf.SetLogger(zap.New())
}
func main() {
entryLog := log.WithName("entrypoint")
var (
metricsAddr, certDir string
port int
)
pflag.IntVar(&port, "port", 9443, "pod-admission-webhook listen port.")
pflag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
pflag.StringVar(&certDir, "cert-dir", "", "CertDir is the directory that contains the server key and certificate. "+
"if not set, webhook server would look up the server key and certificate in "+
"{TempDir}/k8s-webhook-server/serving-certs. The server key and certificate "+
"must be named tls.key and tls.crt, respectively.")
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
pflag.Parse()
// Setup a Manager
entryLog.Info("setting up manager")
mgr, err := manager.New(config.GetConfigOrDie(), manager.Options{
Scheme: scheme,
MetricsBindAddress: metricsAddr,
Port: port,
CertDir: certDir,
})
if err != nil {
entryLog.Error(err, "unable to start manager")
os.Exit(1)
}
// Setup webhooks
entryLog.Info("setting up webhook server")
hookServer := mgr.GetWebhookServer()
entryLog.Info("registering webhooks to the webhook server")
hookServer.Register("/mutate-pod", &webhook.Admission{Handler: &podMutate{Client: mgr.GetClient()}})
entryLog.Info("starting manager")
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
entryLog.Error(err, "problem running manager")
os.Exit(1)
}
}
构建 podMutate 函数,注入 DNS 配置:
package main
import (
"context"
"encoding/json"
"net/http"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)
type podMutate struct {
Client client.Client
decoder *admission.Decoder
}
func (p *podMutate) Handle(ctx context.Context, req admission.Request) admission.Response {
pod := &corev1.Pod{}
podMutateLog := log.WithName("podMutate")
err := p.decoder.Decode(req, pod)
if err != nil {
podMutateLog.Error(err, "failed decoder pod")
return admission.Errored(http.StatusBadRequest, err)
}
podDNSConfig := []corev1.PodDNSConfigOption{}
ndotsValue := "2"
ndotsOpt := corev1.PodDNSConfigOption{
Name: "ndots",
Value: &ndotsValue,
}
podDNSConfig = append(podDNSConfig, ndotsOpt)
timeoutValue := "1"
timeoutOpt := corev1.PodDNSConfigOption{
Name: "timeout",
Value: &timeoutValue,
}
podDNSConfig = append(podDNSConfig, timeoutOpt)
reopenOpt := corev1.PodDNSConfigOption{
Name: "single-request-reopen",
}
podDNSConfig = append(podDNSConfig, reopenOpt)
if pod.Spec.DNSConfig == nil {
pod.Spec.DNSConfig = &corev1.PodDNSConfig{
Options: podDNSConfig,
}
} else {
if len(pod.Spec.DNSConfig.Options) == 0 {
pod.Spec.DNSConfig.Options = podDNSConfig
}
}
marshaledPod, err := json.Marshal(pod)
if err != nil {
podMutateLog.Error(err, "failed marshal pod")
return admission.Errored(http.StatusInternalServerError, err)
}
return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod)
}
// podMutate implements admission.DecoderInjector.
// A decoder will be automatically injected.
// InjectDecoder injects the decoder.
func (p *podMutate) InjectDecoder(d *admission.Decoder) error {
p.decoder = d
return nil
}
通过以上代码,一个简单 mutating webhook 服务就完成了。
代码逻辑完成只是第一步,前面提到 API Server 和 webhook server 通信是基于 TLS 的,下面我们介绍如果配置 TLS。
创建 csr.conf
文件:
[ req ] default_bits = 2048 prompt = no default_md = sha256 req_extensions = req_ext distinguished_name = dn [ dn ] C = CN ST = Zhejiang L = Hangzhou O = opskumu OU = opskumu CN = pod-admission-webhook.kube-system [ req_ext ] subjectAltName = @alt_names [ alt_names ] DNS.1 = pod-admission-webhook DNS.2 = pod-admission-webhook.kube-system DNS.3 = pod-admission-webhook.kube-system.svc [ v3_ext ] authorityKeyIdentifier=keyid,issuer:always basicConstraints=CA:FALSE keyUsage=keyEncipherment,dataEncipherment extendedKeyUsage=serverAuth,clientAuth subjectAltName=@alt_names
通过以下命令生成证书,证书有效期根据实际情况修改,这里使用 10000 天:
openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -subj "/CN=pod-admission-webhook.kube-system.svc" -days 10000 -out ca.crt
openssl genrsa -out tls.key 2048
openssl req -new -key tls.key -out tls.csr -config csr.conf
openssl x509 -req -in tls.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out tls.crt -days 10000 -extensions v3_ext -extfile csr.conf
注意证书生成的 CN
字段,组成格式为 <serviceName>.<namespace>.svc
组成,示例服务名为 pod-admission-webhook,部署在 kube-system 空间。
- mutatingwebhook.yaml
证书生成之后,创建 Kubernetes 部署文件:
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: pod-admission-webhook
webhooks:
- name: pod-admission-webhook.kube-system.svc
clientConfig:
caBundle: <ca base64>
service:
name: pod-admission-webhook
namespace: kube-system
path: "/mutate-pod"
rules:
- operations: ["CREATE"]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
failurePolicy: Fail
namespaceSelector:
matchLabels:
pod-admission-webhook-injection: enabled
sideEffects: None
admissionReviewVersions: ["v1", "v1beta1"]
注意以上通过 namespaceSelector
来决定是否执行 webhook。如果要开启,则需要添加对应 labels,如:
kubectl label ns <namespace> pod-admission-webhook-injection=enabled
- secret.yaml
kubectl create secret tls pod-admission-webhook --dry-run=client --cert=tls.crt --key=tls.key --namespace kube-system -o yaml > secret.yaml
- service.yaml
apiVersion: v1
kind: Service
metadata:
labels:
app: pod-admission-webhook
name: pod-admission-webhook
namespace: pod-admission-webhook
spec:
ports:
- name: 443-9443
port: 443
protocol: TCP
targetPort: 9443
selector:
app: pod-admission-webhook
type: ClusterIP
- deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: pod-admission-webhook
namespace: kube-system
labels:
app: pod-admission-webhook
spec:
replicas: 1
selector:
matchLabels:
app: pod-admission-webhook
template:
metadata:
labels:
app: pod-admission-webhook
spec:
containers:
- name: pod-admission-webhook
image: <image>
command:
- "/pod-admission-webhook"
args:
- "--cert-dir"
- "/certs"
imagePullPolicy: IfNotPresent
volumeMounts:
- name: webhook-certs
mountPath: /certs
readOnly: true
readinessProbe:
failureThreshold: 3
initialDelaySeconds: 10
periodSeconds: 5
successThreshold: 1
tcpSocket:
port: 8080
timeoutSeconds: 1
resources:
limits:
cpu: "1"
memory: 1Gi
requests:
cpu: 125m
memory: 500Mi
volumes:
- name: webhook-certs
secret:
secretName: pod-admission-webhook
根据实际情况修改相应的字段,如镜像、空间、命名等等,通过 kubectl
部署以上服务。
至此,一个简单的自定义 admission webhook 流程全部完成,完整代码见 admission-webhook-example