Istio の Sidecar Injection はどうなっているのか
先日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.patchType
と response.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 を作成
- Admission Webhook を受ける API サーバとして
- Service リソース
- kube-apiserver から
istio-sidecar-injector
Pod への通信のため
- kube-apiserver から
- 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] というソフトウェアがあるため、気が向いたらこれについての記事も書こうと思います。
Discussion