Admission Webhookのすゝめ

# はじめに
最近公私ともにContoller開発を行うに当たり、validateやmutateを行うWebhookについて触れる機会が多いのです。そこで、自分なりに理解している範囲で現在のwebhook事情についてまとめてみます。

# Webhookライブラリの種類
- [kubernetes-sigs/controller-runtime](https://github.com/kubernetes-sigs/controller-runtime/tree/master/pkg/webhook)
  言わずと知れたcontroller開発ライブラリです。
  この中で提供されているwebhookを利用することで、kubebuilderなどのフレームワークでもwebhookが簡単に実装可能となっています。
- [knative/pkg/webhook](https://github.com/knative/pkg/tree/main/webhook)
  サーバーレスな基盤を構築することが可能なknativeですが、パッケージが個別で公開されており、その中でwebhookライブラリが使用できます。若干学習コストが高いです。
- [slok/kubewebhook](https://github.com/slok/kubewebhook)
  非常に軽量でシンプルなライブラリです。[サンプル](https://github.com/slok/kubewebhook/tree/master/examples)が提供されており、これをいじるだけで実装出来ます。上記2つと異なり、証明書の自動ローテーションを行うには工夫が必要。
- [snorwin/k8s-generic-webhook](https://github.com/snorwin/k8s-generic-webhook)
  kubebuilderなどフレームワークで作成するwebhookによく似たコードで実装可能なシンプルなライブラリです。フレームワークでの開発経験がある方であれば、使いこなせるのではないでしょうか。

今回はシンプルで拡張性の高いkubewebhookに絞って紹介します。(私のお気に入りだからというのは内緒

# 特徴
実装が非常に簡単であり、ライブラリ自体のコードも読みやすく書かれています。
また、通常のvalidateやmutateだけでなく、各処理をchainとしてつなげることが可能です。
- 以下validatorの該当箇所抜粋
https://github.com/slok/kubewebhook/blob/master/pkg/webhook/validating/validator.go#L46-L60
```golang
type chain struct {
	validators []Validator
	logger     log.Logger
}


// NewChain returns a new chain of validators.
// - If any of the validators returns an error, the chain will end.
// - If any of the validators returns an stopChain == true, the chain will end.
// - If any of the validators returns as no valid, the chain will end.
func NewChain(logger log.Logger, validators ...Validator) Validator {
	return chain{
		validators: validators,
		logger:     logger,
	}
}
```
chainを用いた実装は以下のコードを確認ください。

https://github.com/slok/kubewebhook/tree/master/examples/multiwebhook/pkg/webhook

# kubewebhook提供サンプル
以下のサンプルを確認いただくと分かる通り、ほぼこれだけでmutateが完結します。
★流れ
実際のmutate処理作って
↓
MutatorFunc()に食わせてあげて
↓
Webhookの呼び出しをしてあげるだけ。
注意点として、処理対象のリソースの型を`metav1.Object`にアサーションしてあげるくらいです。
https://github.com/slok/kubewebhook/blob/master/examples/pod-annotate/main.go
```golang
func annotatePodMutator(_ context.Context, _ *kwhmodel.AdmissionReview, obj metav1.Object) (*kwhmutating.MutatorResult, error) {
	pod, ok := obj.(*corev1.Pod)
	if !ok {
		// If not a pod just continue the mutation chain(if there is one) and don't do nothing.
		return &kwhmutating.MutatorResult{}, nil
	}

	// Mutate our object with the required annotations.
	if pod.Annotations == nil {
		pod.Annotations = make(map[string]string)
	}
	pod.Annotations["mutated"] = "true"
	pod.Annotations["mutator"] = "pod-annotate"

	return &kwhmutating.MutatorResult{
		MutatedObject: pod,
	}, nil
}

func main() {
	logrusLogEntry := logrus.NewEntry(logrus.New())
	logrusLogEntry.Logger.SetLevel(logrus.DebugLevel)
	logger := kwhlogrus.NewLogrus(logrusLogEntry)

	cfg := initFlags()

	// Create our mutator
	mt := kwhmutating.MutatorFunc(annotatePodMutator)

	mcfg := kwhmutating.WebhookConfig{
		ID:      "podAnnotate",
		Obj:     &corev1.Pod{},
		Mutator: mt,
		Logger:  logger,
	}
	wh, err := kwhmutating.NewWebhook(mcfg)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error creating webhook: %s", err)
		os.Exit(1)
	}

	// Get the handler for our webhook.
	whHandler, err := kwhhttp.HandlerFor(kwhhttp.HandlerConfig{Webhook: wh, Logger: logger})
	if err != nil {
		fmt.Fprintf(os.Stderr, "error creating webhook handler: %s", err)
		os.Exit(1)
	}
	logger.Infof("Listening on :8080")
	err = http.ListenAndServeTLS(":8080", cfg.certFile, cfg.keyFile, whHandler)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error serving webhook: %s", err)
		os.Exit(1)
	}
}
```
本番で使用する場合のサンプルまであります。最高です。

https://github.com/slok/k8s-webhook-example

# kubewebhookを使用した実装サンプル
Image tagが付与されていない場合に`latest`tagをデフォルト値として付与するMutatorを作成してみました。(すでにkubernetesのデフォルトはlatestであると理解していますが、kubectl describeの見た目を変えたかったのです。

https://github.com/jnytnai0613/default-imagetag-webhook

# 出力結果
kubewebhookでmutateなりvaidateなりを行うと以下のようにjsonで出力され、`op`からどのようなoperationがなされたのかがわかるようになっています。今回はImage tagの書き換えをおこなったため、`replace`となっていますが、Fieldの追加を行うと、`add`と出力されます。また、どのような変更がされたかも確認可能となっており、非常に親切なログの出し方と思います。
```
$ kubectl logs default-imagetag-webhook-5bd6b8b685-t26t4 -f
time="2022-07-11T00:41:14Z" level=info msg="Listening on :8080"
time="2022-07-11T00:42:12Z" level=debug msg="Webhook mutating review finished with: '[{\"op\":\"replace\",\"path\":\"/spec/containers/0/image\",\"value\":\"nginx:latest\"}]' JSON Patch" dry-run=false kind=v1/Pod name= ns=default op=create path=/mutate request-id=682f8ab2-6ace-4663-a748-96299cf4f650 trace-id= webhhok-type=mutating webhook-id=imageTagMutator webhook-kind=mutating wh-version=v1
time="2022-07-11T00:42:12Z" level=info msg="Admission review request handled" dry-run=false duration=276.777541ms kind=v1/Pod name= ns=default op=create path=/mutate request-id=682f8ab2-6ace-4663-a748-96299cf4f650 svc=http.Handler trace-id= webhook-id=imageTagMutator webhook-kind=mutating wh-version=v1
```

# 証明書ローテーションについて
WebhookのPodとkube-apiserver[^1]間はTLSで認証にて通信を行います。
そのため、kube-apiserverで証明書がローテーションされる[^2]と、Webhook PodにあるHTTPサーバの証明書も何らかの方法でローテーションしてあげる必要があります。
ただ、kubewebhookには証明書の自動ローテーション機能は実装されていません。
そこで、contoroller-runtimeのcertwatcherを利用することで自動ローテーションが可能です。
https://github.com/kubernetes-sigs/controller-runtime/tree/master/pkg/certwatcher

使い方は、同じくcontoroller-runtimeのpkg/webhook/server.goをご確認ください。
https://github.com/kubernetes-sigs/controller-runtime/blob/master/pkg/webhook/server.go#L212-L296
certwatcherは証明書の変更を監視しており、変更があるとロード対象の証明書を変更します。
そして、webhook側はcertWatcher.GetCertificateでtls.ClientHelloInfoを待ち受けているので、リクエストがあるたびに証明書をロードします。そのため、常に最新の証明書が使えるというわけです。
```golang
func (s *Server) Start(ctx context.Context) error {
:
	cfg := &tls.Config{ //nolint:gosec
		NextProtos:     []string{"h2"},
		GetCertificate: certWatcher.GetCertificate,
		MinVersion:     tlsMinVersion,
	}
:
```

# 参考リンク
https://github.com/kubernetes-sigs/controller-runtime/tree/master/pkg/webhook
https://github.com/knative/pkg/tree/main/webhook
https://github.com/slok/kubewebhook
https://github.com/snorwin/k8s-generic-webhook

[^1]:ここでいうkube-apiserverとはValidatingWebhookConfigurationやMutatingWebhookConfigurationを指します。
[^2]:ValidatingWebhookConfigurationやMutatingWebhookConfigurationの証明書のローテーションは[cert-manager](https://github.com/cert-manager/cert-manager)で自動で行うか、自前で行うかの2択です。