はじめに
この記事は、前回書いた「kubebuilderでpodの監視してみた」の続き的な立ち位置で書いた記事となっています。
前回の記事では、主にKubebuilderを用いたKubernetesオペレータの説明と基本的な動作確認がメインとなっていました。今回は、同じオペレータを「Admission webhooks」を用いて機能拡張してみました。
Admission Webhooksについて
Admission Webhookは、リクエストがKubernetesのAPIサーバに送信された際に発火されるwebhookで、Mutating Admission webhook とValidating Admission webhook の2種類があります。
特定のカスタムリソースの作成・更新時に、リクエストはKubernetesのAPIサーバに送信されますが、その際にwebhook APIを呼び出して変更内容の検証や書き換えを行うようにすることができます。
まず、Mutating Admission webhookが先に発火されます。そこで、APIサーバに送信されるオブジェクトを必要に応じて強制的に変更しデフォルトの値に書き換えることができます。変更があれば変更が行われた後、Validating Admission webhookが発火され、オブジェクトの検証を行います。バリデーション違反があればエラーを返します。
簡単にまとめを行うと、Admission webhooksはカスタムリソースの検証を行い、その中でもMutating Admission webhookは先に発火され必要に応じてオブジェクトを変更します。Validating Admission webhookはオブジェクトの検証を行い、変更を行うことはないというものです。
kubebuilderでの実装
コードの流れ
kubebuilderでオペレータを開発している場合にこれらのwebhookを導入する手順を説明します。
以下のように、まずはwebhookを作成します。ここでは--defaulting
と--programmatic-validation
というオプションを指定して、必要に応じてリソースにデフォルト値を設定するwebhookとリソースのバリデーションを行うwebhookを追加します。
$ kubebuilder create webhook --group scaling --version v1 --kind PodScaler --defaulting --programmatic-validation
まずはコードの流れを確認しておきます。
cmd/main.goにて、webhookが有効化されていることを確認しwebhook の処理を呼び出す部分は以下の部分です:
// nolint:goconst
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
if err = webhookscalingv1.SetupPodScalerWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "PodScaler")
os.Exit(1)
}
}
SetupPodScalerWebhookWithManager
が呼び出されており、この処理が記述されている場所を確認してみます:
$ git grep "SetupPodScalerWebhookWithManager"
cmd/main.go: if err = webhookscalingv1.SetupPodScalerWebhookWithManager(mgr); err != nil {
internal/webhook/v1/podscaler_webhook.go:// SetupPodScalerWebhookWithManager registers the webhook for PodScaler in the manager.
internal/webhook/v1/podscaler_webhook.go:func SetupPodScalerWebhookWithManager(mgr ctrl.Manager) error {
internal/webhook/v1/webhook_suite_test.go: err = SetupPodScalerWebhookWithManager(mgr)
このことから、internal/webhook/v1/podscaler_webhook.go内を変更していけば良さそうです。
該当の関数は以下のようになっています。この関数では、PodScalerCustomValidator
と PodScalerCustomDefaulter
が webhook.CustomValidator
と webhook.CustomDefaulter
というインターフェースをそれぞれ実装しており、それらが呼び出されるように登録されています。
// SetupPodScalerWebhookWithManager registers the webhook for PodScaler in the manager.
func SetupPodScalerWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).For(&scalingv1.PodScaler{}).
WithValidator(&PodScalerCustomValidator{}).
WithDefaulter(&PodScalerCustomDefaulter{}).
Complete()
}
type PodScalerCustomDefaulter struct {}
var _ webhook.CustomDefaulter = &PodScalerCustomDefaulter{}
type PodScalerCustomValidator struct {}
var _ webhook.CustomValidator = &PodScalerCustomValidator{}
KubernetesのAPIサーバがリソースの作成や更新リクエストを受け取ると、これらのインターフェースを実装した関数(後に確認しますが、Default
, ValidateCreate
, ValidateUpdate
, ValidateDelete
)が呼び出されます。そのため、これらの関数内に必要なバリデーションやデフォルト値の適用ロジックを記述していけばよい形となっています。
Mutating Admission webhookの実装
以下に、Mutating Admission webhookの設定をしました。
カスタムリソースのspec.countフィールドにおいて理想のPod数を記述できるようにしていましたが、この値が0など、不正な値の時にデフォルト値の5を設定するようにしています。このように、条件に応じて自動でカスタムリソースを変更できます。
// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind PodScaler.
func (d *PodScalerCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error {
podscaler, ok := obj.(*scalingv1.PodScaler)
if !ok {
return fmt.Errorf("expected an PodScaler object but got %T", obj)
}
// Mutating Admission Webhookのデフォルト値を設定(spec.count)
if podscaler.Spec.Count < 1 {
podscaler.Spec.Count = 5
}
podscalerlog.Info("Defaulting for PodScaler", "name", podscaler.GetName())
return nil
}
Validating Admission webhookの実装
次に、Validating Admission webhookの設定を実装します。
カスタムリソースのspec.countフィールドやspec.selectorフィールドに不正な値が入っている場合にエラーを返しリクエストを拒否します。なお、一つ目のspec.countのバリデーションは、Mutating Admission webhookが正しく反映されていればデフォルト値が反映されるはずなので、本来は実行されないはずの箇所です。後で確認をしてみます。
// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type PodScaler.
func (v *PodScalerCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
podscaler, ok := obj.(*scalingv1.PodScaler)
if !ok {
return nil, fmt.Errorf("expected a PodScaler object but got %T", obj)
}
// validating admission webhookの設定
if podscaler.Spec.Count < 1 {
return nil, fmt.Errorf("count must be greater than 0")
}
if len(podscaler.Spec.Selector) == 0 {
return nil, fmt.Errorf("selector must be specified")
}
podscalerlog.Info("Validation for PodScaler upon creation", "name", podscaler.GetName())
return nil, nil
}
動作確認
準備
config/default/kustomization.yamlファイルを開き、resourcesの../webhook
と../certmanager
のコメントアウトを外すと共に、replacements配下のコメントアウトを外します。(Uncomment the following block if you have a ValidatingWebhookなどと書かれている部分)
次に、webhookを利用するための証明書を発行するcert-managerをデプロイします。
$ kubectl apply --validate=false -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml
ここまでできたら、マニフェストの更新とイメージビルド・クラスタへの反映を行います。
make manifests
make docker-build
make docker-push
make deploy
反映後、webhookの設定が正しく適用されているかどうかを確認します。
$ kubectl get mutatingwebhookconfigurations
NAME WEBHOOKS AGE
pod-scaler-mutating-webhook-configuration 1 17s
$ kubectl get validatingwebhookconfigurations
NAME WEBHOOKS AGE
pod-scaler-validating-webhook-configuration 1 17s
Mutating Admission webhookの動作確認
ここで、以下のようなカスタムリソースを反映したとします。
apiVersion: scaling.example.com/v1
kind: PodScaler
metadata:
name: example-podscaler
spec:
count: 0
selector:
app: nginx
spec.count(理想のPod数)が0であり、Mutating Admission webhookによりデフォルト値の5となることを確認したいと思います。同時に、Validating Admission webhookによりエラーが返されないことも確認します。
$ kubectl get podscaler example-podscaler -o yaml
でカスタムリソースの状態を確認すると、spec.countが5に変わっていることが確認できます。
apiVersion: scaling.example.com/v1
kind: PodScaler
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"scaling.example.com/v1","kind":"PodScaler","metadata":{"annotations":{},"name":"example-podscaler","namespace":"default"},"spec":{"count":0,"selector":{"app":"nginx"}}}
creationTimestamp: "2024-12-01T11:42:26Z"
generation: 8
name: example-podscaler
namespace: default
resourceVersion: "1163838"
uid: d0b15d62-af16-45f0-865f-36f423bf1a4d
spec:
count: 5
selector:
app: nginx
また、実際にPodも5個立っています。元々3つだったので追加で2つのPodがスケールして5個になっています。
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-86dcfdf4c6-2sqs2 1/1 Running 0 58s
nginx-86dcfdf4c6-4zztf 1/1 Running 0 43s
nginx-86dcfdf4c6-99s97 1/1 Running 0 13s
scaled-pod-28pkl 1/1 Running 0 72s
scaled-pod-bh9fz 1/1 Running 0 3m27s
Validating Admission webhookの動作確認
次に、以下のようにspec.selectorフィールドに不正な値を入れてみます。
apiVersion: scaling.example.com/v1
kind: PodScaler
metadata:
name: example-podscaler
spec:
count: 0
selector:
app:
このカスタムリソースを反映すると以下のエラーが出ます。admission webhook denied the requests とあり、Validating Admission webhookがリクエストを拒否できていることがわかります。
Error from server (Forbidden): error when applying patch:
{"metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"scaling.example.com/v1\",\"kind\":\"PodScaler\",\"metadata\":{\"annotations\":{},\"name\":\"example-podscaler\",\"namespace\":\"default\"},\"spec\":{\"count\":0,\"selector\":{\"app\":null}}}\n"}},"spec":{"count":0,"selector":{"app":null}}}
to:
Resource: "scaling.example.com/v1, Resource=podscalers", GroupVersionKind: "scaling.example.com/v1, Kind=PodScaler"
Name: "example-podscaler", Namespace: "default"
for: "config/samples/test.yaml": error when patching "config/samples/test.yaml": admission webhook "vpodscaler-v1.kb.io" denied the request: selector must be specified
終わりに
簡単な実装でAdmission Webhooksの動作確認をしてみました。Istioのsidecar injectionも同様の仕組みを使っており、かなり使われている場面は多いのでこれを機にさらに理解を深めていければと思っています。
ここまで読んでいただき、ありがとうございました。