KubernetesのDynamic Admission Controlを試してみる
Dynamic Admission Control とは
公式の説明はこちら
KubernetesでPodのようなオブジェクトを操作する際には、kubectl等のクライアントからkube-apiserverへのAPIリクエストが実行されます。この時、リクエストの認証および認可が行われた後の段階で、Admission Controlと呼ばれる追加の検証を行うことが可能です。
例えば、Pod作成時のSpecの中に runAsUser
の記述がある場合はリクエストを拒否するといった制御を行えます。使用できるAdmission Controllerの一覧は こちら に列挙されています。
現在これらのAdmission Controllerはkube-apiserverにビルトインで実装されているため、新しくAdmission Controllerを追加したくなった場合には、プラグインを実装してKubernetesのupstreamに取り込んでもらう必要があります。
しかし、自分たちのクラスタ用にカスタマイズしたAdmission Controllerを作りたいといったケースではupstreamに取り込んでもらうことは難しいでしょう。そのような時にDynamic Admission Controlが1つの手段として使用できます。
仕組み
Dynamic Admission Controlを使用するにあたっては、以下の2つのAdmission Controllerを使って設定を行います。
- ValidatingAdmissionWebhook
- MutatingAdmissionWebhook
Webhookという名の通り、kube-apiserverがオブジェクトの作成/更新等のリクエストを受け取った際にWebhookがトリガーされ、任意のエンドポイントへPOSTリクエストを送信してくれます。
このWebhookリクエストのBodyには、操作対象であるオブジェクトのManifestがJSON形式で含まれています。リクエストを受け取るHTTPサーバでは、その中身を見て結果をJSONで返すシンプルな処理を行います。
引用元: https://kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/
とりあえず試す
VolumeにhostPathを使用しているPodは作成を拒否するValidatingAdmissionWebhookのサンプルを以下のリポジトリで公開しています。
まずは試してみようということで、minikube(もしくはkind)のクラスタで実際に動くところを見てみましょう。サンプルはKubernetesのバージョンが1.17以降でなければ動作しませんので、必要に応じてminikubeのバージョンを上げてください。
次のコマンドを実行するとValidatingAdmissionWebhookの設定が追加されて、kube-public
namespaceにWebhookを受け取るHTTPサーバーが立ち上がります。
※Podの作成に影響を及ぼすため、Productionのクラスタなどで実行しないようにご注意ください
$ curl -s https://raw.githubusercontent.com/tokibi/sample-admission-controller/master/deploy/{configmap,deployment,webhook-configuration}.yaml | kubectl apply -f - secret/admission-controller-certs created deployment.apps/admission-controller created service/admission-controller created validatingwebhookconfiguration.admissionregistration.k8s.io/validating-webhook created $ kubectl get pod -n kube-public NAME READY STATUS RESTARTS AGE admission-controller-5c954f94c9-5dnbp 1/1 Running 0 9m12s
HTTPサーバーのPodがRunningになったことを確認したら、hostPathを使用していないPodと使用しているPodの2つを作成してみます。
$ cat <<EOF | kubectl apply -f - apiVersion: v1 kind: Pod metadata: name: valid-pod spec: containers: - name: valid-pod image: alpine:latest command: - "sleep" - "1" --- apiVersion: v1 kind: Pod metadata: name: invalid-pod spec: containers: - name: invalid-pod image: alpine:latest command: - "sleep" - "1" volumes: - name: hostpath-volume hostPath: # <- invalid path: /etc EOF
実行すると valid-pod
は正常に作成されますが、invalid-pod
はhostPathを使用しているため、エラーメッセージと共に作成が拒否されることがわかります。
pod/valid-pod created Error from server: error when creating "STDIN": admission webhook "validating-webhook.example.com" denied the request: pod "invalid-pod" creation denied, hostPath is not allowed
このようにAdmissionWebhookを使用して、Podだけに限らず任意のリソースの操作時に制御を行うことができます。
設定内容
とりあえずサンプルを使って動作を確認してみましたが、具体的に何を行ったかを順に解説していきます。
- kube-apiserverのオプションでAdmissionWebhookを有効化
- WebhookConfigurationを作成
- Webhookを受け取るAPIサーバーを実装
kube-apiserverのオプションでAdmissionWebhookを有効化
minikubeやkindのクラスタで起動しているkube-apiserverでは初期状態で有効化されているため、先ほどサンプルを試した際にはこの手順は省略されていました。その他のクラスタでは、必要に応じてkube-apiserverの --enable-admission-plugins
オプションに MutatingAdmissionWebhook
と ValidatingAdmissionWebhook
を追加してください。
参考までにminikubeのkube-apiserverのオプションでは、次のように最後から2, 3番目で指定されています。
$ minikube ssh "sudo cat /etc/kubernetes/manifests/kube-apiserver.yaml" | grep enable-admission-plugins - --enable-admission-plugins=NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,DefaultTolerationSeconds,NodeRestriction,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,ResourceQuota
Admission Controlは先頭から順に実行されるため、この場合は NodeRestriction
の検証が実施された後、MutatingAdmissionWebhook, ValidatingAdmissionWebhookの順に検証が実施されることになります。
WebhookConfigurationを作成
kube-apiserverの設定でAdmissionWebhookを有効にしただけでは、オブジェクトの操作時にWebhookは実行されません。AdmissionWebhookを追加できるようになっただけで、設定は別途行う必要があります。
この時に作成するのがWebhookConfigurationで、サンプルでは以下のようなManifestを定義してクラスタにapplyしています。kindは ValidatingWebhookConfiguration
になっていますので、検証のみを実施します。MutatingAdmissionWebhookを使用する場合は MutatingWebhookConfiguration
を別途作成してください。
--- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: name: validating-webhook namespace: kube-public webhooks: - name: validating-webhook.example.com admissionReviewVersions: - v1beta1 clientConfig: caBundle: LS0tLS1CRU... service: name: admission-controller namespace: kube-public path: "/" rules: - apiGroups: [""] apiVersions: ["v1"] operations: ["CREATE"] resources: ["pods"] sideEffects: None timeoutSeconds: 5
clientConfig
の箇所では、HTTPサーバーのTLS証明書を検証するために必要なCA証明書をbase64でエンコードしたものと、HTTPサーバーのPodの前段に設置されたServiceのセレクタを記載しています。
サンプルでは、kube-apiserverからのWebhookリクエストを受け取るサーバーもKubernetesのクラスタ内に起動していますのでServiceを指定していますが、以下のようにクラスタ外で起動しているサーバーをURL形式で指定することも可能です。
apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration ... webhooks: - name: my-webhook.example.com clientConfig: url: "https://my-webhook.example.com:9443/my-webhook-path" ...
引用元: https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#url
また、rules
の箇所ではkube-apiserverがリクエストを受け取った時にWebhookをトリガーする条件を記載します。サンプルではPodの作成時のみWebhookリクエストが送信されるようにしています。その他の設定項目については このあたり が参考になると思います。
この設定を行うことでWebhookリクエストが飛ぶようになります。
Webhookを受け取るHTTPサーバーを実装
Request
kube-apiserverからのWebhookリクエストを受け取って結果を返すHTTPサーバーを実装します。リクエストBodyに含まれるJSONデータの構造は 公式 で解説されていて、以下はその引用です。
{ "apiVersion": "admission.k8s.io/v1", "kind": "AdmissionReview", "request": { # Random uid uniquely identifying this admission call "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", # Fully-qualified group/version/kind of the incoming object "kind": {"group":"autoscaling","version":"v1","kind":"Scale"}, # Fully-qualified group/version/kind of the resource being modified "resource": {"group":"apps","version":"v1","resource":"deployments"}, # subresource, if the request is to a subresource "subResource": "scale", # Fully-qualified group/version/kind of the incoming object in the original request to the API server. # This only differs from `kind` if the webhook specified `matchPolicy: Equivalent` and the # original request to the API server was converted to a version the webhook registered for. "requestKind": {"group":"autoscaling","version":"v1","kind":"Scale"}, # Fully-qualified group/version/kind of the resource being modified in the original request to the API server. # This only differs from `resource` if the webhook specified `matchPolicy: Equivalent` and the # original request to the API server was converted to a version the webhook registered for. "requestResource": {"group":"apps","version":"v1","resource":"deployments"}, # subresource, if the request is to a subresource # This only differs from `subResource` if the webhook specified `matchPolicy: Equivalent` and the # original request to the API server was converted to a version the webhook registered for. "requestSubResource": "scale", # Name of the resource being modified "name": "my-deployment", # Namespace of the resource being modified, if the resource is namespaced (or is a Namespace object) "namespace": "my-namespace", # operation can be CREATE, UPDATE, DELETE, or CONNECT "operation": "UPDATE", "userInfo": { # Username of the authenticated user making the request to the API server "username": "admin", # UID of the authenticated user making the request to the API server "uid": "014fbff9a07c", # Group memberships of the authenticated user making the request to the API server "groups": ["system:authenticated","my-admin-group"], # Arbitrary extra info associated with the user making the request to the API server. # This is populated by the API server authentication layer and should be included # if any SubjectAccessReview checks are performed by the webhook. "extra": { "some-key":["some-value1", "some-value2"] } }, # object is the new object being admitted. # It is null for DELETE operations. "object": {"apiVersion":"autoscaling/v1","kind":"Scale",...}, # oldObject is the existing object. # It is null for CREATE and CONNECT operations. "oldObject": {"apiVersion":"autoscaling/v1","kind":"Scale",...}, # options contains the options for the operation being admitted, like meta.k8s.io/v1 CreateOptions, UpdateOptions, or DeleteOptions. # It is null for CONNECT operations. "options": {"apiVersion":"meta.k8s.io/v1","kind":"UpdateOptions",...}, # dryRun indicates the API request is running in dry run mode and will not be persisted. # Webhooks with side effects should avoid actuating those side effects when dryRun is true. # See http://k8s.io/docs/reference/using-api/api-concepts/#make-a-dry-run-request for more details. "dryRun": false } }
HTTPサーバーは基本的にこのJSONの中身を見て処理を実施していくことになります。サンプルではPodのspecにhostPathが使用されているか否かによって結果を変えていましたが、それは request.object
の中を見て判断しています。
Response
ValidatingAdmissionWebhookに対するレスポンスでは request.allowed
のbool値によってkube-apiserverへのリクエストを許可するかどうかを決定します。falseの場合は response.status.message
でオブジェクトを操作しようとしたクライアントにエラーメッセージを通知することができます。
{ "apiVersion": "admission.k8s.io/v1", "kind": "AdmissionReview", "response": { "uid": "<value from request.uid>", "allowed": false, "status": { "code": 403, "message": "You cannot do this because it is Tuesday and your name starts with A" } } }
引用元: https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#response
また、MutatingAdmissionWebhookに対するレスポンスでは patchType
と patch
によってManifestの変更内容を指定します。patch
の値には [{"op": "add", "path": "/spec/replicas", "value": 3}]
のように操作内容, 対象, 変更後の値をbase64でエンコードしたものを入力します。
{ "apiVersion": "admission.k8s.io/v1", "kind": "AdmissionReview", "response": { "uid": "<value from request.uid>", "allowed": true, "patchType": "JSONPatch", "patch": "W3sib3AiOiAiYWRkIiwgInBhdGgiOiAiL3NwZWMvcmVwbGljYXMiLCAidmFsdWUiOiAzfV0=" } }
引用元: https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#response
HTTPサーバーの行う処理はJSONを受け取ってJSONを返すだけですので、一から作成するのもそこまで大変ではないと思いますが、サンプルではAdmissionWebhook用のフレームワークである slok/kubewebhook を利用させていただきました。
まとめ
kube-apiserverに組み込まれるAdmission Controllerを新しく追加するのは非常に大変ですが、AdmissionWebhookによって簡単に色々なカスタマイズができそうです。