この記事はJX通信社 Advent Calendar 2019の1日目の記事です。
こんにちは、SREのたっち(TatchNicolas)です。
先日開催されたKubeCon 2019でもセッションで紹介されていた、Admission Webhooksについて書きます。
Admission WebhooksとはKubernetesリソースを操作(CREATE/UPDATE)する時に、作成や変更の内容をチェックしたり、書き換えたりすることができる機能です。
TL;DR
- Admission Webhooksを使うと、あらゆるKubernetesリソースの操作をトリガーに 「チェック(Validation)」「変更(Mutation)」 を行える
- 身近なところでは、Istioでサイドカーのauto-injectionで使われています
- どの種類のリソースにどんな操作をするときにWebhookを呼ぶかは細かく条件指定ができる
- 最低限自分で実装すべきは非常にシンプルなWebサーバのみで、簡単に試すことができる
- 実運用するには冪等性、可用性、適用範囲など考えることが色々
サンプルコードは以下に置いてあります。
やってみる
今回は、以下の二種類のAdmission Webhooksを作ってみます。
- 「Pod作成時に
env
ラベルにdev,stg,prd
のいずれかが付与されているかチェックする、なかったらリソース作成を拒否する」 = ValidatingWebhook - 「Pod作成時にリソースの名前にある接頭辞を付与する」 = MutatingWebhook
Kubernetes環境はAdmission Webhooksのbetaが取れている v1.16を使います。今回はMinikube v1.4.0を利用しました。*1
Webhookを書いてServiceとして動かす
では早速、Webhookを実装しましょう。要は、kube-apiserverからJSONを受け取ってJSONを返すことができれば良いので言語やフレームワークは何でも良いです。 前述の通り今回は手軽さ最優先でFlaskを使います。
最低限のValidationは非常にシンプルで、以下のようなレスポンスを返すだけです。allowed
が true
であればValidationは成功し、false
であれば失敗します。
{ "apiVersion": "admission.k8s.io/v1", "kind": "AdmissionReview", "response": { "uid": "<value from request.uid>", "allowed": false } }
実際に書いたコードはこちら。今回はstatus
フィールドにメッセージを入れてみました。たとえばkubectl
によるPod作成時にValidationに失敗した場合、このメッセージが返ってきます。
これをDeploymentとしてデプロイし、Serviceとして叩けるようにします。*2
今回は同じクラスタ内から利用するのでClusterIPだけで十分です。 真面目にやるなら、目的の異なるWebhookは別にデプロイしたほうが良いと思いますが、今回はお試しということで一つのアプリケーションとして書いてしまいました。
Admission WebhooksはHTTPS必須なので、Podを作る前にまずcfsslを使ってオレオレ証明書を作成します。
$ cat <<EOF | cfssl genkey - | cfssljson -bare server { "hosts": [ "sample-admission-webhook.default.svc" ], "CN": "sample-admission-webhook.default.svc", "key": { "algo": "ecdsa", "size": 256 } } EOF 2019/11/27 18:28:38 [INFO] generate received request 2019/11/27 18:28:38 [INFO] received CSR 2019/11/27 18:28:38 [INFO] generating key: ecdsa-256 2019/11/27 18:28:38 [INFO] encoded CSR
server-key.pem
と server.csr
が作成されます。続いて、Webhookに持たせるための証明書を作ります。
$ cat <<EOF | kubectl apply -f - apiVersion: certificates.k8s.io/v1beta1 kind: CertificateSigningRequest metadata: name: sample-admission-webhook.default spec: request: $(cat server.csr | base64 | tr -d '\n') usages: - digital signature - key encipherment - server auth EOF certificatesigningrequest.certificates.k8s.io/sample-admission-webhook.default created $ kubectl certificate approve sample-admission-webhook.default certificatesigningrequest.certificates.k8s.io/sample-admission-webhook.default approved
base64エンコードしてダウンロードし、そこからSecretリソースを作ります。
$ kubectl get csr sample-admission-webhook.default -o jsonpath='{.status.certificate}' | base64 --decode > server.crt $ kubectl create secret tls --save-config sample-admission-webhook-secret --key server-key.pem --cert server.crt secret/sample-admission-webhook-secret created
最後に、このSecretを使ってDeploymentを作成し、それに紐づくServiceも作成します。
apiVersion: apps/v1 kind: Deployment metadata: name: sample-admission-webhook spec: replicas: 1 selector: matchLabels: app: sample-admission-webhook template: metadata: labels: app: sample-admission-webhook spec: containers: - name: app image: tatchnicolas/sample-admission-webhook:0.10 imagePullPolicy: Always ports: - containerPort: 8080 volumeMounts: - name: tls mountPath: /tls command: ["gunicorn"] args: - "main:app" - "--bind" - "0.0.0.0:8080" - "--access-logfile" - "-" - "--certfile" - "/tls/tls.crt" - "--keyfile" - "/tls/tls.key" volumes: - name: tls secret: secretName: sample-admission-webhook-secret --- apiVersion: v1 kind: Service metadata: name: sample-admission-webhook spec: selector: app: sample-admission-webhook ports: - port: 443 protocol: TCP targetPort: 8080
Webhookの設定を登録する
さて、それではいよいよAdmission Webhooksの設定を登録します。必要なManifestは至ってシンプルです。
caBundle
フィールドにてクライアント(=kube-apiserver)がWebhookへリクエストを送る際に信用する証明書を指定することができます。
まず、ValidatingWebhookのほうから登録してみましょう。
apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: name: "sample-validating-webhook" webhooks: - name: "sample-validating-webhook.hoge.fuga.local" failurePolicy: Fail rules: - apiGroups: [""] operations: ["CREATE"] apiVersions: ["v1"] resources: ["pods"] scope: "Namespaced" clientConfig: caBundle: <server.crtの中身をbase64エンコードして貼る> service: namespace: default name: sample-admission-webhook path: /validate admissionReviewVersions: ["v1", "v1beta1"] timeoutSeconds: 5 sideEffects: None
続いてMutatingWebhookも同様に
apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration metadata: name: "sample-mutating-webhook" webhooks: - name: "sample-mutating-webhook.hoge.fuga.local" failurePolicy: Fail rules: - apiGroups: [""] operations: ["CREATE"] apiVersions: ["v1"] resources: ["pods"] scope: "Namespaced" clientConfig: caBundle: <server.crtの中身をbase64エンコードして貼る> service: namespace: default name: sample-admission-webhook path: /mutate admissionReviewVersions: ["v1", "v1beta1"] timeoutSeconds: 5 sideEffects: None
Manifestを適用したら、リソースが作成されたことを確認しましょう
$ kubectl get validatingwebhookconfigurations NAME CREATED AT sample-validating-webhook 2019-11-28T05:58:28Z $ kubectl get mutatingwebhookconfigurations NAME CREATED AT sample-mutating-webhook 2019-11-28T05:39:28Z
動きを確認する
では、実際に動作確認してみましょう。
apiVersion: v1 kind: Pod metadata: name: webhook-demo labels: env: stg spec: containers: - name: nginx image: nginx
こんなManifestからPodを作ってみましょう。
$ kubectl apply -f pod.yaml pod/dummy-prefix-webhook-demo created $ kubectl get pod NAME READY STATUS RESTARTS AGE dummy-suffix-webhook-demo 0/1 ContainerCreating 0 2s sample-admission-webhook-7ddfc8c784-6pf7k 1/1 Running 0 6m6s
Pythonで書いた処理の通り、名前が書き換わっています。
Validationのほうもチェックするために、env
ラベルが dev,stg,prd
のいずれにも一致しないPodを作ってみましょう。
apiVersion: v1 kind: Pod metadata: name: validation-demo labels: env: willberejected spec: containers: - name: nginx image: nginx
設定した通りのエラーメッセージが返ってきており、Podも作られていないことが確認できます。
$ kubectl apply -f invalid_pod.yaml Error from server: error when creating "invalid_pod.yaml": admission webhook "sample-validating-webhook.hoge.fuga.local" denied the request: Required label env is not set or its value is invalid
はまったところ
- k8s v1.14.8で利用できるバージョン(
admissionregistration.k8s.io/v1beta
)では、リクエスト先port
オプションを指定するとUnknown fieldって怒られる(サンプルには書いてあるのに...) - Podに対してWebhookを書くと、DeploymentやReplicaset経由で作られるPodについてもValidation/Mutationの対象となってしまうので、Webhookを開発中にそのDeploymentを更新したらWebhook用のPod自体がValidationに引っかかってコケてしまった
- あとは
kube-system
ネームスペースのPodに対して悪さをされると困るので、GitHubのサンプルコードではnamespaceSelector
objectSelector
を使って対象を絞っています
- あとは
感想
オレオレ証明書の準備等の手間はかかりましたが、Webhook自体は非常にシンプルに作ることができました。Kubernetes自体にコンパイルされるAdmission controllerを作るのは非常にハードルが高いですが、こうして疎結合に機能を拡張できるのはとても魅力的ですね。
Webhook自体の可用性が低いと条件に合致するリソースの操作全体に影響してしまったり、MutatingWebhookConfigurationのほうは「変更を加える」という性質上、複数回呼ばれてしまった場合に備えて処理を冪等にする必要があったり、本番運用の際は慎重になる必要があります。
しかし、HelmやKustomize等を使うのに比べてより根っこに近いところでガバナンスを効かせることができるので、Admission Webhooksは非常に強力な機能だと思います。上手に使って、リリースや運用の負荷を下げていきましょう。