🤿

Kubernetes Admission Webhook Deep Dive

2022/11/22に公開

Kubernetes Admission Webhook Deep Dive

この記事は CloudNative Days Tokyo 2022 のセッション「Kubernetes Admission Webhook Deep Dive」の補足です。

controller-runtime での 3 種類の Webhook 実装方法

controller-runtime では、Admission Webhook を実装するための interface が 3 種類用意されています。

Defaulter/Validator

Defaulter と Validator は、Kubebuilder で Webhook のコードを生成した場合に利用される方式です。
Mutating Webhook を実装する場合は Defaulter を、Validating Webhook を実装する場合は Validator を利用します。

https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/webhook/admission#Defaulter

type Defaulter interface {
	runtime.Object
	Default()
}

https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/webhook/admission#Validator

type Validator interface {
	runtime.Object
	ValidateCreate() error
	ValidateUpdate(old runtime.Object) error
	ValidateDelete() error
}

リソースを表現する構造体のメソッドとして定義しなければならないため、Kubernetesの標準リソースや第三者のカスタムリソースは扱いにくくなっています。

具体的な実装方法は以下のコードをご覧ください。

CustomDefaulter/CustomValidator

CustomDefaulter と CustomValidator は、Defaulter と Validator よりも拡張性の高い実装方式です。
Mutating Webhook を実装する場合は CustomDefaulter を、Validating Webhook を実装する場合は CustomValidator を利用します。

https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/webhook/admission#CustomDefaulter

type CustomDefaulter interface {
	Default(ctx context.Context, obj runtime.Object) error
}

https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/webhook/admission#CustomValidator

type CustomValidator interface {
	ValidateCreate(ctx context.Context, obj runtime.Object) error
	ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) error
	ValidateDelete(ctx context.Context, obj runtime.Object) error
}

Defaulter/Validatorとは違い、リソースを表現する構造体のメソッドとして定義する必要はないため、Kubernetesの標準リソースや第三者のカスタムリソースも扱うことができます。
引数として渡ってくるオブジェクトはすでにデコード済みなので、以下のように適応する型にキャストして利用することができます。

pod, ok := obj.(*corev1.Pod)
if !ok {
    return fmt.Errorf("unknown newObj type %T", obj)
}

また引数として渡ってくる context から admission.Request を取り出すことができます。

req, err := admission.RequestFromContext(ctx)

具体的な実装方法は以下のコードをご覧ください。

Handler

Handler は最も拡張性の高い実装方式です。
Defaulter/Validator や CustomDefaulter/CustomValidator と違い、受け取ったオブジェクトのデコード処理や、JSON Patchの生成処理などを自前で実装する必要があります。
Mutating Webhook と Validating Webhook のどちらを実装する場合も同じ interface を利用します。

https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/webhook/admission#Handler

type Handler interface {
	Handle(context.Context, Request) Response
}

なお、オブジェクトのデコード処理に必要な Decoder は、以下のように DecoderInjector interface を実装することで取得できます。

type deploymentValidator struct {
	decoder *admission.Decoder
}

func (v *deploymentValidator) InjectDecoder(d *admission.Decoder) error {
	v.decoder = d
	return nil
}

Decoder を利用して、以下のようにデコード処理をおこなうことができます。

func (v *deploymentValidator) Handle(ctx context.Context, req admission.Request) admission.Response {
	dep := &appsv1.Deployment{}
	err := v.decoder.Decode(req, dep)
	if err != nil {
		return admission.Errored(http.StatusBadRequest, err)
	}

	return admission.Allowed("ok")
}

デコード処理を自前で実装できるため、1 つの Webhook で複数種類のオブジェクトを処理することもできます。
(サンプルプログラムでは、Deployment と Scale を 1 つの Webhook で処理しています)

具体的な実装方法は以下のコードをご覧ください。

クライアント認証

ここでは Kubebuilder で生成したプロジェクトで Admission Webhook のクライアント認証を有効にする方法を紹介します。
必要なファイルは以下のリポジトリにあります。

はじめに cfssl を利用して証明書の作成をおこないます。
各種設定ファイルは https://github.com/zoetrope/sample-webhook/tree/main/certs をご覧ください。

# CA証明書の作成
cfssl gencert -initca ./certs/ca-csr.json | cfssljson -bare ./certs/clientca

# クライアント証明書の作成
cfssl genkey ./certs/client.json | cfssljson -bare ./certs/client

# クライアント証明書に署名する
cfssl sign -ca ./certs/clientca.pem -ca-key ./certs/clientca-key.pem -config ./certs/ca-config.json ./certs/client.csr | cfssljson -bare ./certs/client

つぎに kubeconfig.yaml を作成します。
name には Admission Webhook サービスの DNS 名 <サービス名>.<サービスのNamespace>.svc を指定します。
client-certificateclient-key には、先ほど作成したクライアント証明書と秘密鍵を指定します。

apiVersion: v1
kind: Config
users:
  - name: sample-webhook-webhook-service.sample-webhook-system.svc
    user:
      client-certificate: /etc/certs/client.pem
      client-key: /etc/certs/client-key.pem

つづいて API Server の設定ファイル admissionconfig.yaml を作成します。
kubeConfigFile には先ほど作成した kubeconfig.yaml を指定します。

apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
  - name: ValidatingAdmissionWebhook
    configuration:
      apiVersion: apiserver.config.k8s.io/v1
      kind: WebhookAdmissionConfiguration
      kubeConfigFile: "/etc/cluster/kubeconfig.yaml"
  - name: MutatingAdmissionWebhook
    configuration:
      apiVersion: apiserver.config.k8s.io/v1
      kind: WebhookAdmissionConfiguration
      kubeConfigFile: "/etc/cluster/kubeconfig.yaml"

そして API Server の --admission-control-config-file オプションに admissionconfig.yaml を渡します。
kind を利用している場合は、以下のような設定ファイルを用意してクラスターを起動します。

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: sample-webhook-dev
nodes:
  - role: control-plane
    kubeadmConfigPatches:
      - |
        kind: ClusterConfiguration
        apiServer:
            extraArgs:
              admission-control-config-file: /etc/cluster/admissionconfig.yaml
            extraVolumes:
              - name: cluster
                hostPath: /etc/cluster
                mountPath: /etc/cluster
                readOnly: true
                pathType: "DirectoryOrCreate"
              - name: certs
                hostPath: /etc/certs
                mountPath: /etc/certs
                readOnly: true
                pathType: "DirectoryOrCreate"
    extraMounts:
      - hostPath: ./cluster
        containerPath: /etc/cluster
        readOnly: true
      - hostPath: ./certs
        containerPath: /etc/certs
        readOnly: true

つぎに先ほど作成した CA 証明書を Secret リソースにします。

cd certs
kubectl create secret generic webhook-client-cert -n sample-webhook-system --from-file=clientca.pem

この Secret リソースを Admission Webhook の Pod から利用できるようにマウントします。
なお、サーバー証明書とクライアント証明書を同じディレクトリに配置する必要があるため、Projected Volumes を利用しています。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: controller-manager
  namespace: system
spec:
  template:
    spec:
      containers:
        - name: manager
          ports:
            - containerPort: 9443
              name: webhook-server
              protocol: TCP
          volumeMounts:
            - mountPath: /tmp/k8s-webhook-server/serving-certs
              name: cert
              readOnly: true
      volumes:
        - name: cert
          projected:
            defaultMode: 420
            sources:
              - secret:
                  name: webhook-server-cert
              - secret:
                  name: webhook-client-cert

最後に Admission Webhook のプログラム中でクライアント証明書の CA 証明書のファイル名を設定します。
controller-runtime を利用している場合は、manager の GetWebhookServer()ClientCAName を指定するだけです。

func main() {
	// 省略

	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
		Scheme:                 scheme,
		MetricsBindAddress:     metricsAddr,
		Port:                   9443,
		HealthProbeBindAddress: probeAddr,
		LeaderElection:         enableLeaderElection,
		LeaderElectionID:       "0c46b1d7.zoetrope.github.io",
	})

	// 省略

	wh := mgr.GetWebhookServer()
	// クライアント証明書のCA証明書のファイル名を設定する
	wh.ClientCAName = "clientca.pem"

	// 省略

	if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
		setupLog.Error(err, "problem running manager")
		os.Exit(1)
	}
}

以上で、Admission Webhook のクライアント認証が有効化されます。

参考: https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#authenticate-apiservers

Discussion