この記事から得られる知識
この記事を読むと、以下を "完全に理解" できます✌️
- 代表的なサービスメッシュの種類について
- Istioのサイドカーインジェクションの仕組みについて
- この記事から得られる知識
- 01. はじめに
- 02. サービスメッシュが登場した経緯
- 03. admission-controllersアドオンについて
- 04. サイドカーインジェクションの仕組み
- 05. おわりに
- 記事関連のおすすめ書籍
01. はじめに
推し (Istio) が尊い🙏🙏🙏
さて、前回の記事の時と同様に、最近の業務でもオンプレとAWS上のIstio⛵️をひたすら子守りしています。
今回は、子守りの前提知識の復習もかねて、サービスメッシュを実装するIstioサイドカーインジェクションを記事で解説しました。
解説するのは、執筆時点 (2023/01/14) 時点で最新の 1.14
系のIstioです。
執筆時点 (2023/01/14) では、Istioが実装するサービメッシュには、『サイドカープロキシメッシュ』と『アンビエントメッシュ』があります。
サイドカープロキシメッシュの仕組みの軸になっているものは、サイドカーコンテナであるistio-proxy
コンテナです。
Istioは、KubernetesのPodの作成時に、istio-proxy
コンテナをPod内に自動的にインジェクション (注入) します
それでは、もりもり布教していきます😗
飛ばしていただいても大丈夫ですが、読んでもらえるとより理解が深まるはずです👍
02. サービスメッシュが登場した経緯
なぜサービスメッシュが登場したのか
そもそも、なぜサービスメッシュが登場したのでしょうか。
マイクロサービスアーキテクチャのシステムには、アーキテクチャ固有のインフラ領域の問題 (例:サービスディスカバリーの必要性、マイクロサービス間通信の暗号化、テレメトリー作成など) があります。
アプリエンジニアが各マイクロサービス内にインフラ領域の問題に関するロジックを実装すれば、これらの問題の解決できます。
しかし、アプリエンジニアはアプリ領域の問題に責務を持ち、インフラ領域の問題はインフラエンジニアで解決するようにした方が、互いに効率的に開発できます。
そこで、インフラ領域の問題を解決するロジックをサイドカーとして切り分けます。
これにより、アプリエンジニアとインフラエンジニアの責務を分離可能になり、凝集度が高くなります。
また、インフラ領域の共通ロジックをサイドカーとして各マイクロサービスに提供できるため、単純性が高まります。
こういった流れの中で、サービスメッシュが登場しました。
サービスメッシュのモデル
前述の通り、サービスメッシュの登場前は、アプリエンジニアが各マイクロサービス内にインフラ領域の問題に関するロジックを実装していました。
これを、『共有ライブラリモデル』と呼びます。
その後、『サイドカーモデル』とも呼ばれるサイドカープロキシメッシュが登場しました。
執筆時点 (2023/01/14) では、『カーネルモデル』とも呼ばれるサイドカーフリーメッシュが登場しています。
サイドカープロキシメッシュ
Istioのサイドカーによるサービスメッシュ (サイドカープロキシメッシュ) は、
- サイドカーコンテナ (
istio-proxy
コンテナ) が稼働するデータプレーン - サイドカーを中央集権的に管理するIstiod (
discovery
コンテナ) が稼働するコントロールプレーン
からなります。
03. admission-controllersアドオンについて
admission-controllersアドオンとは
IstioのPod内へのサイドカーインジェクションの前提知識として、admission-controllersアドオンを理解する必要があります。
もし、admission-controllersアドオンをご存知の方は、 04. サイドカーインジェクションの仕組み まで飛ばしてください🙇🏻
kube-apiserverでは、admission-controllersアドオンを有効化できます。
有効化すると、認証ステップと認可ステップの後にmutating-admissionステップとvalidating-admissionステップを実行でき、admissionプラグインの種類に応じた処理を挿入できます。
クライアント (kubectl
クライアント、Kubernetesリソース) からのリクエスト (例:Kubernetesリソースに対する作成/更新/削除、kube-apiserverからのプロキシへの転送) 時に、各ステップでadmissionプラグインによる処理 (例:アドオンビルトイン処理、独自処理) を発火させられます。
admissionプラグインの種類
admission-controllersアドオンのadmissionプラグインには、たくさんの種類があります。
IstioがPod内にサイドカーをインジェクションする時に使用しているアドオンは、『MutatingAdmissionWebhook』です。
- CertificateApproval
- CertificateSigning
- CertificateSubjectRestriction
- DefaultIngressClass
- DefaultStorageClass
- DefaultTolerationSeconds
- LimitRanger
- "MutatingAdmissionWebhook" 👈 これ
- NamespaceLifecycle
- PersistentVolumeClaimResize
- PodSecurity
- Priority
- ResourceQuota
- RuntimeClass
- ServiceAccount
- StorageObjectInUseProtection
- TaintNodesByCondition
- ValidatingAdmissionWebhook
MutatingAdmissionWebhookプラグイン
MutatingAdmissionWebhookプラグインとは
MutatingAdmissionWebhookプラグインを使用すると、mutating-admissionステップ時に、リクエスト内容を変更する処理をフックできます。
フックする具体的な処理として、webhookサーバーにAdmissionRequestリクエストとして送信することにより、レスポンスのAdmissionResponseに応じてリクエスト内容を動的に変更します。
MutatingWebhookConfigurationで、MutatingAdmissionWebhookプラグインの発火条件やwebhookサーバーの宛先情報を設定します。
MutatingWebhookConfigurationの具体的な実装については、サイドカーインジェクションの仕組みの中で説明していきます。
AdmissionReview、AdmissionRequest、AdmissionResponse
▼ AdmissionReview
AdmissionReviewは以下のようなJSONであり、kube-apiserverとwebhookサーバーの間でAdmissionRequestとAdmissionResponseを運びます。
{ "apiVersion": "admission.k8s.io/v1", "kind": "AdmissionReview", # AdmissionRequest "request": {}, # AdmissionResponse "response": {}, }
▼ AdmissionRequest
AdmissionRequestは以下のようなJSONです。
kube-apiserverがクライアントから受信した操作内容が持つことがわかります。
例で挙げたAdmissionRequestでは、クライアントがDeploymentをCREATE操作するリクエストをkube-apiserverに送信したことがわかります。
{ "apiVersion": "admission.k8s.io/v1", "kind": "AdmissionReview", # AdmissionRequest "request": { ... # 変更されるKubernetesリソースの種類を表す。 "resource": { "group": "apps", "version": "v1", "resource": "deployments" }, # kube-apiserverの操作の種類を表す。 "operation": "CREATE", ... } }
▼ AdmissionResponse
一方でAdmissionResponseは、例えば以下のようなJSONです。
AdmissionResponseは、マニフェスト変更処理をpatch
キーの値に持ち、これはbase64方式でエンコードされています。
{ "apiVersion": "admission.k8s.io/v1", "kind": "AdmissionReview", # AdmissionResponse "response": { "uid": "<value from request.uid>", # 宛先のwebhookサーバーが受信したか否かを表す。 "allowed": true, # PathによるPatch処理を行う。 "patchType": "JSONPatch", # Patch処理の対象となるKubernetesリソースと処理内容を表す。base64方式でエンコードされている。 "patch": "W3sib3AiOiAiYWRkIiwgInBhdGgiOiAiL3NwZWMvcmVwbGljYXMiLCAidmFsdWUiOiAzfV0=", }, }
エンコード値をデコードしてみると、例えば以下のようなpatch処理が定義されています。
# patchキーをbase64方式でデコードした場合 [{"op": "add", "path": "/spec/replicas", "value": 3}]
マニフェストに対する操作 (op
) 、キー (path
) 、値 (value
) が設定されています。
kube-apiserverがこれを受信すると、指定されたキー (.spec.replicas
) に値 (3
) に追加します。
04. サイドカーインジェクションの仕組み
全体のフロー
前提知識を踏まえた上で、admission-controllersアドオンの仕組みの中で、サイドカーのistio-proxy
コンテナがどのようにPodにインジェクションされるのかを見ていきましょう。
最初に、サイドカーインジェクションのフローは以下の通りになっています。
(画像はタブ開き閲覧を推奨)
クライアント ➡︎ kube-apiserver
ここで説明するフロー箇所
『クライアント ➡︎ kube-apiserver』の箇所を説明します。
(画像はタブ開き閲覧を推奨)
(1) Podの作成をリクエスト
まずは、クライアントがkube-apiserverにリクエストを送信するところです。
クライアント (Deployment、DaemonSet、StatefulSet、を含む) は、Podの作成リクエストをkube-apiserverに送信します。
この時のリクエスト内容は、以下の通りとします。
# Podを作成する。 $ kubectl apply -f foo-pod.yaml
# foo-pod.yamlファイル apiVersion: v1 kind: Pod metadata: name: foo-pod namespace: foo-namespace spec: containers: - name: foo image: foo:1.0.0 ports: - containerPort: 80
またNamespaceでは、あらかじめistio-proxy
コンテナのインジェクションが有効化されているとします。
Istioではv1.10
以降、リビジョンの番号のエイリアスを使用して、istio-proxyコンテナのインジェクションを有効化するようになりました。
apiVersion: v1 kind: Namespace metadata: name: foo-namespace labels: # istio-proxyコンテナのインジェクションを有効化する。 # エイリアスは自由 istio.io/rev: <エイリアス>
istio.io/rev
ラベル値のエイリアスについてistio.io/rev
ラベル値は、どんなエイリアスでもよいです。
よくあるエイリアスとして
default
やstable
を使用します👍
kube-apiserver ➡︎ Service
ここで説明するフロー箇所
『kube-apiserver ➡︎ Service』の箇所を説明します。
(画像はタブ開き閲覧を推奨)
(2) 認証/認可処理をコール
kube-apiserverは、認証ステップと認可ステップにて、クライアントからのリクエストを許可します。
(3) アドオンの処理をコール
kube-apiserverは、mutating-admissionステップにて、MutatingAdmissionWebhookプラグインの処理をコールします。
前提知識の部分で具体的な実装を省略しましたが、Istioのバージョン1.14.3
時点で、MutatingWebhookConfigurationは以下のようになっています。
Namespaceでサイドカーインジェクションを有効化する時に使用したエイリアスは、このMutatingWebhookConfigurationで実体のリビジョン番号と紐づいています。
$ kubectl get mutatingwebhookconfiguration istio-revision-tag-default -o yaml
apiVersion: admissionregistration.k8s.io/v1beta1 kind: MutatingWebhookConfiguration metadata: name: istio-revision-tag-default labels: app: sidecar-injector # エイリアスの実体 istio.io/rev: <リビジョン番号> # リビジョン番号のエイリアス istio.io/tag: <エイリアス> webhooks: - name: rev.namespace.sidecar-injector.istio.io # MutatingAdmissionWebhookプラグインの処理の発火条件を登録する。 rules: - apiGroups: [""] apiVersions: ["v1"] operations: ["CREATE"] resources: ["pods"] scope: "*" # Webhookの前段にあるServiceの情報を登録する。 clientConfig: service: name: istiod-<リビジョン番号> namespace: istio-system path: "/inject" # エンドポイント port: 443 caBundle: Ci0tLS0tQk ... # Namespace単位のサイドカーインジェクション # 特定のNamespaceでMutatingAdmissionWebhookプラグインの処理を発火させる。 namespaceSelector: matchExpressions: - key: istio.io/rev operator: DoesNotExist - key: istio-injection operator: DoesNotExist # Pod単位のサイドカーインジェクション # 特定のオブジェクトでMutatingAdmissionWebhookプラグインの処理を発火させる。 objectSelector: matchExpressions: - key: sidecar.istio.io/inject operator: NotIn values: - "false" - key: istio.io/rev operator: In values: - <エイリアス> ...
MutatingWebhookConfigurationには、MutatingAdmissionWebhookプラグインの発火条件やwebhookサーバーの宛先情報を定義します。
MutatingAdmissionWebhookプラグインの発火条件に関して、例えばIstioでは、 NamespaceやPod.metadata.labels
キーに応じてサイドカーインジェクションの有効化/無効化を切り替えることができ、これをMutatingAdmissionWebhookプラグインで制御しています。
webhookサーバーの宛先情報に関して、Istioではwebhookサーバーの前段にServiceを配置しています。
MutatingAdmissionWebhookプラグインが発火した場合、Serviceの/inject:443
にHTTPSプロトコルのリクエストを送信するようになっています。
また、宛先のServiceの名前がistiod-<リビジョン番号>
となっていることからもわかるように、Serviceは特定のバージョンのIstiodコントロールプレーンに対応しており、想定外のバージョンのIstiodコントロールプレーンを指定しないように制御しています。
一方で発火しなかった場合には、以降のAdmissionReviewの処理には進みません。
(4) AdmissionRequestに値を詰める
kube-apiserverは、mutating-admissionステップにて、クライアントからのリクエスト内容 (Podの作成リクエスト) をAdmissionReveiew構造体のAdmissionRequestに詰めます。
{ "apiVersion": "admission.k8s.io/v1", "kind": "AdmissionReview", # AdmissionRequest "request": { ... # 変更されるKubernetesリソースの種類を表す。 "resource": { "group": "core", "version": "v1", "resource": "pods" }, # kube-apiserverの操作の種類を表す。 "operation": "CREATE", ... } }
(5) AdmissionReviewを送信
kube-apiserverは、mutating-admissionステップにて、Serviceの/inject:443
にAdmissionReview構造体を送信します。
Service ➡︎ webhookサーバー
ここで説明するフロー箇所
『Service ➡︎ webhookサーバー』の箇所を説明します。
(画像はタブ開き閲覧を推奨)
(6) 15017
番ポートにポートフォワーディング
Serviceは、/inject:443
でリクエストを受信し、discovery
コンテナの15017
番ポートにポートフォワーディングします。
Istioのバージョン1.14.3
時点で、Serviceは以下のようになっています。
$ kubectl get svc istiod-service -n istio-system -o yaml
apiVersion: v1 kind: Service metadata: labels: app: istiod name: istiod-<リビジョン番号> namespace: istio-system spec: type: ClusterIP selector: app: istiod istio.io/rev: <リビジョン番号> ports: - name: grpc-xds port: 15010 protocol: TCP targetPort: 15010 - name: https-dns port: 15012 protocol: TCP targetPort: 15012 # webhookサーバーにポートフォワーディングする。 - name: https-webhook port: 443 protocol: TCP targetPort: 15017 - name: http-monitoring port: 15014 protocol: TCP targetPort: 15014
.spec.selector.istio.io/rev
キーに、ポートフォワーディング先のPodを指定するためのリビジョン番号が設定されており、このPodはdiscovery
コンテナを持ちます。
Istioは、discovery
コンテナ内でwebhookサーバーを実行し、15017
番ポートでリクエストを待ち受けます。
istio.io/rev
`discovery`コンテナの待ち受けポートについてここで、
discovery
コンテナがリクエストを待ち受けているポート番号を見てみると、15017
番ポートでリッスンしていることを確認できます👍
$ kubectl exec foo-istiod -n istio-system -- netstat -tulpn Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 127.0.0.1:9876 0.0.0.0:* LISTEN 1/pilot-discovery tcp6 0 0 :::15017 :::* LISTEN 1/pilot-discovery tcp6 0 0 :::8080 :::* LISTEN 1/pilot-discovery tcp6 0 0 :::15010 :::* LISTEN 1/pilot-discovery tcp6 0 0 :::15012 :::* LISTEN 1/pilot-discovery tcp6 0 0 :::15014 :::* LISTEN 1/pilot-discovery> - istio/pkg/kube/inject/webhook.go at 1.14.3 · istio/istio · GitHub > - https://istio.io/latest/docs/ops/deployment/requirements/#ports-used-by-istio
kube-apiserver ⬅︎ Service ⬅︎ webhookサーバー (※逆向きの矢印)
ここで説明するフロー箇所
『kube-apiserver ⬅︎ Service ⬅︎ webhookサーバー』の箇所を説明します。
矢印が逆向きなことに注意してください。
(画像はタブ開き閲覧を推奨)
(7) patch処理を定義
仕組みの中でも、ここは重要な部分です。
discovery
コンテナ内のwebhookサーバーは、リクエスト内容を書き換えるためのpatch処理を定義します。
webhookサーバーは、マニフェストの.spec.containers[1]
パスにistio-proxy
キーを追加させるようなpatch処理を定義します。
この定義によって、結果的にサイドカーのインジェクションが起こるということになります。
[ ... { "op": "add", # .spec.initContainers[1] を指定する。 "path": "/spec/initContainers/1", # マニフェストに追加される構造を表す。 "value": { "name": "istio-init", "resources": { ... } } }, { "op": "add", # .spec.containers[1] を指定する。 "path": "/spec/containers/1", # マニフェストに追加される構造を表す。 "value": { "name": "istio-proxy", "resources": { ... } } } ... ]
この時、サイドカーのテンプレートに割り当てられた値が、patch処理を内容を決めます。
... type SidecarTemplateData struct { TypeMeta metav1.TypeMeta DeploymentMeta metav1.ObjectMeta ObjectMeta metav1.ObjectMeta Spec corev1.PodSpec ProxyConfig *meshconfig.ProxyConfig MeshConfig *meshconfig.MeshConfig Values map[string]interface{} Revision string EstimatedConcurrency int ProxyImage string } ...
本記事では詳しく言及しませんが、上記のpatch処理ではサイドカーコンテナの
istio-proxy
コンテナの他に、InitContainerのistio-init
コンテナもインジェクション可能にします。
この
istio-init
コンテナは、istio-proxy
コンテナを持つPodです。
インバウンド/アウトバウンド通信の経路を制御するために、Pod内にiptablesのルールを適用する責務を担っています💪🏻
(8) AdmissionResponseに値を詰める
discovery
コンテナ内のwebhookサーバーは、patch処理の定義をAdmissionReveiew構造体のAdmissionResponseに詰めます。
patch
キーの値に、先ほどのpatch処理の定義をbase64方式でエンコードした文字列が割り当てられています。
{ "apiVersion": "admission.k8s.io/v1", "kind": "AdmissionReview", # AdmissionResponse "response": { "uid": "*****", "allowed": true, "patchType": "JSONPatch", # Patch処理の対象となるKubernetesリソースと処理内容を表す。base64方式でエンコードされている。 "patch": "<先ほどのpatch処理の定義をbase64方式でエンコードした文字列>", }, }
(9) AdmissionReviewを返信
discovery
コンテナ内のwebhookサーバーは、AdmissionReview構造体をレスポンスとしてkube-apiserverに返信します。
kube-apiserver ➡︎ etcd
ここで説明するフロー箇所
『kube-apiserver ➡︎ etcd』の箇所を説明します。
(画像はタブ開き閲覧を推奨)
(10) patch処理をコール
kube-apiserverは、AdmissionReview構造体を受信し、AdmissionResponseに応じてリクエスト内容を書き換えます。
patch処理の定義をAdmissionReview構造体から取り出し、クライアントからのリクエスト内容を書き換えます。
具体的には、istio-proxy
コンテナとistio-init
コンテナを作成するために、リクエストしたマニフェストの該当箇所にキーを追加します。
apiVersion: v1 kind: Pod metadata: name: foo-pod namespace: foo-namespace spec: containers: - name: foo image: foo:1.0.0 ports: - containerPort: 80 # kube-apiserverが追加 - name: istio-proxy ... # kube-apiserverが追加 initContainers: - name: istio-init ...
(11) マニフェストを永続化
kube-apiserverは、etcdにPodのマニフェストを永続化します。
クライアント ⬅︎ kube-apiserver
ここで説明するフロー箇所
『クライアント ⬅︎ kube-apiserver』の箇所を説明します。
(画像はタブ開き閲覧を推奨)
(12) コール完了を返信
kube-apiserverは、クライアントにレスポンスを受信します。
$ kubectl apply -f foo-pod.yaml # kube-apiserverからレスポンスが返ってくる pod "foo-pod" created
以降の仕組み
(画像はタブ開き閲覧を推奨)
kube-apiserverは、他のNodeコンポーネント (kube-controlleretcd、kube-scheduler、kubeletなど) と通信し、Podを作成します。
このPodのマニフェストは、アプリコンテナの他に、istio-proxy
コンテナとistio-init
コンテナを持ちます。
結果として、サイドカーコンテナのistio-proxy
コンテナをインジェクションしたことになります。
本記事では詳しく言及しませんが、kube-apiserverと他コンポーネントの通信については、以下の記事が非常に参考になりました🙇🏻
05. おわりに
サービスメッシュの登場とIstioのサイドカーインジェクションの仕組みをもりもり布教しました。
Istioへの愛が溢れてしまいました。
今回登場したMutatingAdmissionWebhookプラグインに関して、私の関わっているプロダクトではIstio以外 (例:CertManager、Prometheus、AWSのaws-eks-vpc-cniアドオンなど) でも使用しています✌️
そのため、MutatingAdmissionWebhookプラグインをどのように使っているのかを一度知れば、知識の汎用性が高いと考えています。
サイドカーインジェクションはIstioでも基本的な機能であり、もし未体験の方がいらっしゃれば、お手元でサイドカーコンテナが追加されることを確認していただくとよいかもしれません👍