Kubernetes Admission Webhookでリソース作成を自在にコントロールする

この記事は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サーバのみで、簡単に試すことができる
  • 実運用するには冪等性、可用性、適用範囲など考えることが色々

サンプルコードは以下に置いてあります。

github.com

やってみる

f:id:TatchNicolas:20191129002303p:plain
Kubernetes Admission Webhooksの位置付け

今回は、以下の二種類の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は非常にシンプルで、以下のようなレスポンスを返すだけです。allowedtrue であれば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.pemserver.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は非常に強力な機能だと思います。上手に使って、リリースや運用の負荷を下げていきましょう。

参考資料

*1:執筆時点で最新のMinikubeにはregistry-credsのaddonに不具合があり、Docker Desktopではk8sバージョンがv1.16いなかったため

*2:ServiceのほかにURLによる指定でクラスタ外のWebhookを呼ぶこともできます