好きな技術を布教したい 😗

〜 沼のまわりに餌をまく 〜

【Istio⛵️】サービスメッシュの登場経緯とIstioサイドカーインジェクションの仕組み


この記事から得られる知識

この記事を読むと、以下を "完全に理解" できます✌️

  • 代表的なサービスメッシュの種類について
  • Istioのサイドカーインジェクションの仕組みについて



01. はじめに


推し (Istio) が尊い🙏🙏🙏

istio-icon


さて、前回の記事の時と同様に、最近の業務でもオンプレとAWS上のIstio⛵️をひたすら子守りしています。

今回は、子守りの前提知識の復習もかねて、サービスメッシュを実装するIstioサイドカーインジェクションを記事で解説しました。

解説するのは、執筆時点 (2023/01/14) 時点で最新の 1.14 系のIstioです。

執筆時点 (2023/01/14) では、Istioが実装するサービメッシュには、『サイドカープロキシメッシュ』と『アンビエントメッシュ』があります。

サイドカープロキシメッシュの仕組みの軸になっているものは、サイドカーコンテナであるistio-proxyコンテナです。

Istioは、KubernetesのPodの作成時に、istio-proxyコンテナをPod内に自動的にインジェクション (注入) します

それでは、もりもり布教していきます😗

記事中のこのボックスは、補足情報を記載しています。

飛ばしていただいても大丈夫ですが、読んでもらえるとより理解が深まるはずです👍


02. サービスメッシュが登場した経緯

なぜサービスメッシュが登場したのか

そもそも、なぜサービスメッシュが登場したのでしょうか。

マイクロサービスアーキテクチャのシステムには、アーキテクチャ固有のインフラ領域の問題 (例:サービスディスカバリーの必要性、マイクロサービス間通信の暗号化、テレメトリー作成、など) があります。

アプリエンジニアが各マイクロサービス内にインフラ領域の問題に関するロジックを実装すれば、これらの問題の解決できます。

service-mesh_layer

しかし、アプリエンジニアはアプリ領域の問題に責務を持ち、インフラ領域の問題はインフラエンジニアで解決するようにした方が、互いに効率的に開発できます。

そこで、インフラ領域の問題を解決するロジックをサイドカーとして切り分けます。

service-mesh_sidecar


これにより、アプリエンジニアとインフラエンジニアの責務を分離可能になり、凝集度が高くなります。

また、インフラ領域の共通ロジックをサイドカーとして各マイクロサービスに提供できるため、単純性が高まります。

こういった流れの中で、サービスメッシュが登場しました。


サイドカープロキシメッシュ

Istioのサイドカーによるサービスメッシュ (サイドカープロキシメッシュ) は、

  • サイドカーコンテナ (istio-proxyコンテナ) が稼働するデータプレーン
  • サイドカーを中央集権的に管理するIstiod (discoveryコンテナ) が稼働するコントロールプレーン

からなります。

istio_sidecar-mesh_architecture



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プラグインによる処理 (例:アドオンビルトイン処理、独自処理) を発火させられます。

kubernetes_admission-controllers_architecture


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の具体的な実装については、サイドカーインジェクションの仕組みの中で説明していきます。

admission-controllers_mutating-admission

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にインジェクションされるのかを見ていきましょう。

最初に、サイドカーインジェクションのフローは以下の通りになっています。

(画像はタブ開き閲覧を推奨)

istio_container-injection_flow


クライアント ➡︎ kube-apiserver

ここで説明するフロー箇所

『クライアント ➡︎ kube-apiserver』の箇所を説明します。

(画像はタブ開き閲覧を推奨)

istio_container-injection_flow_red_1

(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ラベル値は、どんなエイリアスでもよいです。

よくあるエイリアスとしてdefaultstableを使用します👍


kube-apiserver ➡︎ Service

ここで説明するフロー箇所

『kube-apiserver ➡︎ Service』の箇所を説明します。

(画像はタブ開き閲覧を推奨)

istio_container-injection_flow_red_2

(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を配置しています。

istio_admission-controllers_mutating-admission


MutatingAdmissionWebhookプラグインが発火した場合、Serviceの/inject:443HTTPSプロトコルのリクエストを送信するようになっています。

また、宛先の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サーバー』の箇所を説明します。

(画像はタブ開き閲覧を推奨)

istio_container-injection_flow_red_3

(6) 15017番ポートにポートフォワーディング

Serviceは、/inject:443でリクエストを受信し、discoveryコンテナの15017番ポートにポートフォワーディングします。

istio_admission-controllers_mutating-admission


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 > - Istio / Application Requirements


kube-apiserver ⬅︎ Service ⬅︎ webhookサーバー (※逆向きの矢印)

ここで説明するフロー箇所

『kube-apiserver ⬅︎ Service ⬅︎ webhookサーバー』の箇所を説明します。

矢印が逆向きなことに注意してください。

(画像はタブ開き閲覧を推奨)

istio_container-injection_flow_red_4

(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処理でインジェクションするコンテナについて

本記事では詳しく言及しませんが、上記の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』の箇所を説明します。

(画像はタブ開き閲覧を推奨)

istio_container-injection_flow_red_5

(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』の箇所を説明します。

(画像はタブ開き閲覧を推奨)

istio_container-injection_flow_red_6

(12) コール完了を返信

kube-apiserverは、クライアントにレスポンスを受信します。

$ kubectl apply -f foo-pod.yaml

# kube-apiserverからレスポンスが返ってくる
pod "foo-pod" created


以降の仕組み

(画像はタブ開き閲覧を推奨)

istio_container-injection_flow_red_7


kube-apiserverは、他のNodeコンポーネント (kube-controlleretcd、kube-scheduler、kubelet、など) と通信し、Podを作成します。

このPodのマニフェストは、アプリコンテナの他に、istio-proxyコンテナとistio-initコンテナを持ちます。

結果として、サイドカーコンテナのistio-proxyコンテナをインジェクションしたことになります。


▶ kube-apiserverと他コンポーネントの通信について

本記事では詳しく言及しませんが、kube-apiserverと他コンポーネントの通信については、以下の記事が非常に参考になりました🙇🏻‍



05. おわりに

サービスメッシュの登場とIstioのサイドカーインジェクションの仕組みをもりもり布教しました。

Istioへの愛が溢れてしまいました。

今回登場したMutatingAdmissionWebhookプラグインに関して、私の関わっているプロダクトではIstio以外 (例:CertManager、Prometheus、AWSaws-eks-vpc-cniアドオン、など) でも使用しています✌️

そのため、MutatingAdmissionWebhookプラグインをどのように使っているのかを一度知れば、知識の汎用性が高いと考えています。

サイドカーインジェクションはIstioでも基本的な機能であり、もし未体験の方がいらっしゃれば、お手元でサイドカーコンテナが追加されることを確認していただくとよいかもしれません👍


記事関連のおすすめ書籍