Istio の Sidecar Injection はどうなっているのか

2020/09/28に公開

先日Service Meshを実現するソフトウェアの検証を行っていた際、Istio がどのようにして Pod の Sidecar として Envoy を配置しているのかが不思議だったため、その実現方法について調べました。

Kubernetes の拡張機能について

Istio の Sidecar Injection についてのドキュメント に以下の記述がありました。

Automatic sidecar injection

Sidecars can be automatically added to applicable Kubernetes pods using a mutating webhook admission controller provided by Istio.

そこで、Kubernetes の mutating webhook admission controller とはなんぞやと調べてみたところ、Kubernetesの拡張機能の一つである Admission Webhook という機能を用いたものである事がわかりました。

そもそも Kubernetes には拡張機能の実装方法として以下の3種類が提供されています。

このうち Admission Webhook による機能拡張は、最も耳にすると思われる Custom Resource と比べて自由度が落ちる分実装方法が簡単なものになっています。
以降は上記の拡張機能のうち、Istio Sidecar Injection でも利用されている Admission Webhook ついて述べます。

Admission Webhook

Admission Webhook は Kubernetes v1.16 より GA となった機能です (apiVersion: admissionregistration.k8s.io/v1)。当エントリでは検証用クラスタのバージョンが v1.14.6-gke.2 であるため beta 版 (admissionregistration.k8s.io/v1beta1) を利用します。

Admission Webhook には以下の二種類があります。

  • Validating Webhook : API リクエストの検証
  • Mutating Webhook : API リクエストの変更

Validating Webhook は Resource に対する任意のオペレーション (CREATE / UPDATE / DELETE / CONNECT) の際に、その Resource の検証を Webhook 越しに 自前APIサーバにて行うための機能です。

対して Mutating Webhook は Resource に対する任意のオペレーションの際に、そのResourceの変更をWebhook 越しに 自前APIサーバにて行うための機能です。Istio の Sidecar Injection は当機能により Pod Resource に対して Sidecar Pod を追加しています。

以下の画像はKubernetesのAdmission Control処理について書かれています。[1]
このうちMutating Webhookは認証認可の後に、Validation Webhookはetcdに保存される直前にそれぞれ実行されます。

Validating Webhook, Mutating Webhook ともに、Webhook サーバへのリクエストは以下になります (公式ドキュメントからの抜粋)。なお、json なので実際のリクエストではコメントアウト部分は存在しません。

クリックで展開
{
  # Deprecated in v1.16 in favor of admission.k8s.io/v1
  "apiVersion": "admission.k8s.io/v1beta1",
  "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.
    # Only sent by v1.15+ API servers.
    "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.
    # Only sent by v1.15+ API servers.
    "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.
    # Only sent by v1.15+ API servers.
    "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 (and for DELETE operations in API servers prior to v1.15.0)
    "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.
    # Only sent by v1.15+ API servers.
    "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
  }
}

"apiVersion": "admission.k8s.io/v1""kind": "AdmissionReview" はこのリクエストが AdimissionWebhookであることを示します。"request" 以下にはクライアントからkube-apiserver へのリクエストの内容、"object" にはリクエストにより作成/更新されるオブジェクト情報、"oldObject" はリクエストにより更新されるオブジェクトの更新前の情報、"options" にはオプションが、"dryRun" には true/false でdryRunかどうか がでそれぞれ入ります。

以上に対し、Validation Webhook のレスポンスは以下となります。response.allowed フィールドの値によって検証に通過したかどうかがわかります。

クリックで展開
{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "response": {
    "uid": "<value from request.uid>",
    "allowed": true,  # または false
    # 任意で status を返すことも可能
    "status": {
      "code": 403,
      "message": "You cannot do this because it is Tuesday and your name starts with A"
    }
  }
}

Mutation Webhook のレスポンスは以下となります。Validation Webhook のレスポンスと比較して response.patchTyperesponse.patch フィールドが増えています。response.patchType には response.patch の形式 (現状 JSONPatch[2] のみ対応) が、 response.patch にはWebhookサーバによってpatchされるリソース情報が response.patchType 形式で記述された上でbase64エンコードされた値がそれぞれ入ります。

クリックで展開
{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "response": {
    "uid": "<value from request.uid>",
    "allowed": true,  # または false
    "patchType": "JSONPatch",
    # `[{"op": "add", "path": "/spec/replicas", "value": 3}]` を base64 エンコードした値
    "patch": "W3sib3AiOiAiYWRkIiwgInBhdGgiOiAiL3NwZWMvcmVwbGljYXMiLCAidmFsdWUiOiAzfV0="
  }
}

なお、Admission Webhook による API サーバとの通信は HTTPS のみ許可されていることに注意してください。

Istio Sidecar Injection の動作を確認する

当検証で利用したファイル群は以下になります。Istio の Sidecar Injection の動作のみを確かめたかったので、https://github.com/istio/istio 以下にある Helm チャートのうち Sidecar Injection のマニフェストのみを利用しました。

マニフェストファイルにより作成されるリソースは以下です。

  • MutatingWebhookConfiguration リソース
    • 当リソースを作成することで AdmissionControl 処理時に任意のAdmission Webhookを飛ばすことが出来る
  • Deployment リソース
    • Admission Webhook を受ける API サーバとして istio-sidecar-injector Pod を作成
  • Service リソース
    • kube-apiserver から istio-sidecar-injector Pod への通信のため
  • ConfigMap リソース
    • istio-sidecar-injector の設定ファイル
  • Secret リソース
    • TLS 通信のための証明書/証明書鍵
  • ServiceAccount, ClusterRole, ClusterRoleBinding
    • 認可設定、任意

余談ですが、 kube-apiserver から ClusterIP への疎通性が無いと Admission Webhook に失敗することを確認したため、自前クラスタで動作確認する方は注意してください。(ここでハマりました...)

以上のリソース作成後、 istio-injection: enabled という label のついた namespace に Pod をデプロイすることで、Mutating Webhook が動作することが確認できます。以下は Pod デプロイ時の istio-sidecar-injector のログです、Response の jsonPatch フィールドにてどのような値を格納しているかが確認できました。

2019-11-04T09:13:46.008842Z     info    AdmissionReview for Kind=/v1, Kind=Pod Namespace=istio-workspace Name= (hello-world-7b487c785d-***** (actual name not yet known)) UID=67900444-fee3-11e9-bc7a-4201ac144009 Rfc6902PatchOperation=CREATE UserInfo={system:serviceaccount:kube-system:replicaset-controller 9b35f7c4-e4e8-11e9-999a-4201ac144004 [system:serviceaccounts system:serviceaccounts:kube-system system:authenticated] map[]}
2019-11-04T09:13:46.015027Z     info    AdmissionResponse: patch=[{"op":"add","path":"/spec/initContainers","value":[{"name":"istio-init","image":"gcr.io/istio-release/proxy_init:release-1.1-latest-daily","args":["-p","15001","-u","1337","-m","REDIRECT","-i","*","-x","","-b","80","-d","15020"],"resources":{"limits":{"cpu":"100m","memory":"50Mi"},"requests":{"cpu":"10m","memory":"10Mi"}},"imagePullPolicy":"IfNotPresent","securityContext":{"capabilities":{"add":["NET_ADMIN"]},"runAsUser":0,"runAsNonRoot":false}}]},{"op":"add","path":"/spec/containers/-","value":{"name":"istio-proxy","image":"gcr.io/istio-release/proxyv2:release-1.1-latest-daily","args":["proxy","sidecar","--domain","$(POD_NAMESPACE).svc.cluster.local","--configPath","/etc/istio/proxy","--binaryPath","/usr/local/bin/envoy","--serviceCluster","hello-world.$(POD_NAMESPACE)","--drainDuration","45s","--parentShutdownDuration","1m0s","--discoveryAddress","istio-pilot.istio-system:15010","--zipkinAddress","zipkin.istio-system:9411","--connectTimeout","10s","--proxyAdminPort","15000","--concurrency","2","--controlPlaneAuthPolicy","NONE","--statusPort","15020","--applicationPorts","80"],"ports":[{"name":"http-envoy-prom","containerPort":15090,"protocol":"TCP"}],"env":[{"name":"POD_NAME","valueFrom":{"fieldRef":{"fieldPath":"metadata.name"}}},{"name":"POD_NAMESPACE","valueFrom":{"fieldRef":{"fieldPath":"metadata.namespace"}}},{"name":"INSTANCE_IP","valueFrom":{"fieldRef":{"fieldPath":"status.podIP"}}},{"name":"ISTIO_META_POD_NAME","valueFrom":{"fieldRef":{"fieldPath":"metadata.name"}}},{"name":"ISTIO_META_CONFIG_NAMESPACE","valueFrom":{"fieldRef":{"fieldPath":"metadata.namespace"}}},{"name":"ISTIO_META_INTERCEPTION_MODE","value":"REDIRECT"},{"name":"ISTIO_METAJSON_LABELS","value":"{\"app\":\"hello-world\",\"pod-template-hash\":\"7b487c785d\"}\n"}],"resources":{"limits":{"cpu":"2","memory":"1Gi"},"requests":{"cpu":"100m","memory":"128Mi"}},"volumeMounts":[{"name":"istio-envoy","mountPath":"/etc/istio/proxy"},{"name":"istio-certs","readOnly":true,"mountPath":"/etc/certs/"},{"name":"default-token-rtbz2","readOnly":true,"mountPath":"/var/run/secrets/kubernetes.io/serviceaccount"}],"readinessProbe":{"httpGet":{"path":"/healthz/ready","port":15020},"initialDelaySeconds":1,"periodSeconds":2,"failureThreshold":30},"imagePullPolicy":"IfNotPresent","securityContext":{"runAsUser":1337,"readOnlyRootFilesystem":true}}},{"op":"add","path":"/spec/volumes/-","value":{"name":"istio-envoy","emptyDir":{"medium":"Memory"}}},{"op":"add","path":"/spec/volumes/-","value":{"name":"istio-certs","secret":{"secretName":"istio.default","optional":true}}},{"op":"add","path":"/spec/securityContext","value":{}},{"op":"add","path":"/metadata/annotations","value":{"sidecar.istio.io/status":"{\"version\":\"523d1f2baed6b9cf134bc09c9adc201c44db2ff050a45fdff001bc8327b97703\",\"initContainers\":[\"istio-init\"],\"containers\":[\"istio-proxy\"],\"volumes\":[\"istio-envoy\",\"istio-certs\"],\"imagePullSecrets\":null}"}}]

Pod デプロイ後のコンテナ数が2であることも確認できます。
なお、istio-sidecar-injector 以外のコンポーネントを動作していないため、istio-proxy コンテナは readinessProbe に失敗し ready にならないです。今回は Sidecar Injection の検証なので無視しました。

$ kubectl get pod -n istio-workspace
NAME                           READY   STATUS    RESTARTS   AGE
hello-world-7b487c785d-b8sj6   1/2     Running   0          13m

まとめ

当検証にて、Istio が Kubernetes の Admission Webhook という機能を利用して Sidecar Injection を行っていることを確認しました。

istio-sidecar-injector のソースコード[3] を見てみると、net/http の ListenAndServeTLS を実行し通信の待ち受けをおこなうだけでなく、patchCertLoop という関数内で証明書ファイルに更新が走った際もホットリロードするような仕組みが確認できました。
自分で実際に Admission Webhook を受けるサーバを実装する際はこのような仕組みを自前実装するか、Kubebuilder などの SDK を用いると良いそうです。(https://ymmt.hatenablog.com/entry/2019/08/10/150614 を参考にさせていただきました)

また、Admission Webhook には Mutating Webhook によるリソースの変更だけでなく Validating Webhook による構文チェックを行うことができます
ポリシーエンジンである Open Policy Agent と Validating Webhook を組み合わせることで Kubernetes ポリシーの拡張を行える kube-mgmt[4] や、これらのリソース操作を透過的に扱う CRD を提供する Gatekeeper[5] というソフトウェアがあるため、気が向いたらこれについての記事も書こうと思います。

脚注
  1. 画像引用: https://kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/ ↩︎

  2. http://jsonpatch.com/ ↩︎

  3. https://github.com/istio/istio/tree/master/sidecar-injector/ ↩︎

  4. https://github.com/open-policy-agent/kube-mgmt ↩︎

  5. https://github.com/open-policy-agent/gatekeeper
    ↩︎

GitHubで編集を提案

Discussion