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

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

【Istio⛵️】安全なアップグレード手法の仕組み



01. はじめに

隠しません。

有吉弘行のサンデーナイトドリーマーが生きがいです。

さて今回は、Istioの安全なアップグレード手法の仕組みに関する記事を投稿しました🚀

執筆時点 (2023/02/26) では、IstioのIstiodコントロールプレーン (以降、Istiodとします) のアップグレード手法には、『インプレース方式』と『カナリア方式』があります。

また合わせてアップグレードが必要なIstioのIngressGatewayにも、その手法に『インプレース方式』と『カナリア方式』があります。

今回の安全なアップグレード手法として、Istiodでは『カナリアアップグレード』、IngressGatewayでは『インプレースアップグレード』を採用します。

istio_upgrade_list

それでは、Istioの安全なアップグレード手法の仕組みをもりもり布教しようと思います😗 (沼のまわりに餌をまく)

↪️ 参考:


02. Istioのアップグレード手法を説明する前に

カナリアリリースとは

Istiodのカナリアアップグレードが理解しやすくなるように、カナリアリリースから説明したいと思います。

カナリアリリースは、実際のユーザーにテストしてもらいながらリリースする手法です。

もしカナリアリリースをご存知の方は、 03. アップグレード手法の概要 まで飛ばしてください🙇🏻‍♂️


カナリアリリースの手順

カナリアリリースは、一部のユーザーを犠牲にすることになる一方で、アプリを実地的にテストできる点で優れています。

手順を交えながら説明します。

↪️ 参考:CanaryRelease

【1】

旧環境のアプリを残したまま、新環境をリリースします。

この段階では、全てのユーザー (100%) を旧環境にルーティングします。

canary-release_1

【2】

ロードバランサーで重み付けを変更し、一部のユーザー (ここでは10%) を新環境にルーティングします。

canary-release_2

【3】

ユーザーの手を借りて新環境を実地的にテストします (例:該当のエラーメトリクスが基準値を満たすか) 。

canary-release_3

【4】

新環境に問題が起こらなければ、重み付けを段階的に変更し、最終的には全てのユーザー (100%) を新環境にルーティングします。

canary-release_4


カナリアリリース』の呼称の由来

カナリアリリースについては、その呼称の由来を知ると、より理解が深まります。

カナリアリリースは、20世紀頃の炭坑労働者の危機察知方法に由来します。

炭鉱内には有毒な一酸化炭素が発生する場所がありますが、これは無色無臭なので、気づくことに遅れる可能性があります。

そこで当時の炭鉱労働者は、一酸化炭素に敏感な『カナリア』を炭鉱内に持ち込み、カナリアの様子から一酸化炭素の存在を察知するようにしていたそうです。

つまり、先の『犠牲になる一部のユーザー』が、ここでいうカナリアというわけです😨

canary_release_origin
画像引用:George McCaa, U.S. Bureau of Mines

↪️ 参考:


03. アップグレード手法の概要

手順

カナリアリリースについて理解したところで、Istioの安全なアップグレード手法の概要を説明します。

おおよそ以下の手順からなります。

【1】

旧Istiodが稼働しています。

istio_canary-upgrade_1

【2】

新Istiod (discoveryコンテナ) をインストールします。

istio_canary-upgrade_2

【3】

新Istiodのistio-proxyコンテナをインジェクションできるように、Webhookの宛先のServiceを変更します。

この手順は重要で、後のistioctl tag setコマンドの箇所で詳細を説明しています。

【4】

IngressGatewayをインプレースアップグレードします。

istio_canary-upgrade_4

【5】

一部のNamespaceで、istio-proxyコンテナをカナリアアップグレードします。

ここで、カナリアリリースのような重み付けがなく、カナリアアップグレードの『カナリア』という呼称に違和感を持つ方がいるかもしれません。

これについては、全てのNamespaceのistio-proxyコンテナを一斉にアップグレードするのではなく、段階的にアップグレードしていく様子を『カナリア』と呼称している、と個人的に推測しています。

もし『カナリアアップグレード』の由来をご存じの方は、教えていただきたいです🙇🏻‍♂️

istio_canary-upgrade_5

【6】

ユーザーの手を借りて、実地的にテストします (例:該当のエラーメトリクスが基準値以下を満たすか) 。

istio_canary-upgrade_6

【7】

新Istiodのistio-proxyコンテナに問題が起こらなければ、他のNamespaceでもistio-proxy コンテナを段階的にカナリアアップグレードしていきます。

一方でもし問題が起これば、Namespaceのistio-proxyコンテナとIngressGatewayをダウングレードします。

istio_canary-upgrade_7

【8】

最後に、旧Istiodをアンインストールします。

istio_canary-upgrade_8

↪️ 参考:Istio / Canary Upgrades


04. アップグレード手法の詳細

手順

ここからは、03. アップグレード手法の概要 を深ぼっていきます。

ヤサイニンニクアブラマシマシな説明になってしまったので、ここまでを食べ切れた方のみ進むことをお勧めします🥺

今回は、ドキュメントで一番優先して記載されているistioctlコマンドを使用した手順を説明します。

もちろん、他のツール (例:Helm、ArgoCD) を使用してもアップグレードできます。

細かな手順が異なるだけで、アップグレード手法の概要に関しては同じです🙆‍♂️

それでは、03. アップグレード手法の概要 の【1】〜【8】に対応させながら説明していくゾ。


前提

Namespace

まず最初に、前提となる状況を設定しておきます。

istio_canary-upgrade_rollout-restart_1

各Namespaceのistio.io/revラベルにdefaultが設定されているとします。

$ kubectl get namespace -L istio.io/rev

NAME              STATUS   AGE   REV
foo               Active   34d   default
bar               Active   34d   default
baz               Active   34d   default
istio-ingress     Active   34d   default

...

マニフェストに書き起こすと以下のようになっています。

apiVersion: v1
kind: Namespace
metadata:
  name: foo
  labels:
    istio.io/rev: default

エイリアスはどんな値でも問題なく、よくあるエイリアスとしてdefaultstableなどを使用します。

このistio.io/revラベルがあることで、そのNamespaceのPodにistio-proxyコンテナを自動的にインジェクションします。

istio-proxyコンテナのインジェクションについては、こちら記事で説明しており、もし気になる方はこちらもよろしくどうぞ🙇🏻‍♂️

Istiod

istio_canary-upgrade_rollout-restart_1

すでに1-14-6のIstiodが動いており、1-15-4カナリアアップグレードします。

IstiodはDeployment配下のPodであり、このPodはIstiodの実体であるdiscoveryコンテナを持ちます。

$ kubectl get deployment -n istio-system -l app=istiod

NAME                   READY   UP-TO-DATE   AVAILABLE   AGE
istiod-1-14-6          1/1     1            1           47s # 1-14-6

IngressGateway

IngressGatewayはIstiodとは異なるNamespaceで動いており、インプレースアップグレードします。

istio_canary-upgrade_rollout-restart_1

IngressGatewayはistio-proxyコンテナを持ちます。

$ kubectl get deployment -n istio-ingress

NAME                   READY   UP-TO-DATE   AVAILABLE   AGE
istio-ingressgateway   1/1     1            1           47s

補足として、セキュリティのベストプラクティスでは、IstiodとIngressGatewayは異なるNamespaceで動かすことが推奨されています。

↪️ 参考:Istio / Installing Gateways

マイクロサービス

各Namespaceでマイクロサービスが動いています。

マイクロサービスのPodはistio-proxyコンテナを持ちます。

$ kubectl get deployment -n foo

NAME   READY   UP-TO-DATE   AVAILABLE   AGE
foo    2/2     1            1           47s
...
$ kubectl get deployment -n bar

NAME   READY   UP-TO-DATE   AVAILABLE   AGE
bar    2/2     1            1           47s
..
$ kubectl get deployment -n baz

NAME   READY   UP-TO-DATE   AVAILABLE   AGE
baz    2/2     1            1           47s
...


【1】 アップグレード前の検証

ここで実施すること

アップグレード前に、現在のKubernetes Clusterがアップグレード要件を満たしているかを検証します。

↪️ 参考:Before you upgrade

istioctl x precheckコマンド

istioctl x precheckコマンドを実行し、アップグレード要件を検証します。

$ istioctl x precheck

✅ No issues found when checking the cluster.Istiois safe to install or upgrade!
  To get started, check out https://istio.io/latest/docs/setup/getting-started/

問題がなければ、istioctlコマンドはNo issue ...の文言を出力します。

もし、問題がある場合、istioctlコマンドはエラー文言を出力します。

例えば、Istioのistio-proxyコンテナのインジェクションではkube-apiserverと通信する必要があります。

そのため、kube-apiserverのバージョンが古すぎるせいでIstioが非対応であると、エラーになります。

kubectl getコマンド

▼ IstiodのDeployment

kubectl getコマンドを実行し、現在のIstiodのバージョンを確認します👀

まずはIstiodのDeploymentを確認すると、1-14-6のDeploymentがあります。

$ kubectl get deployment -n istio-system -l app=istiod

NAME                   READY   UP-TO-DATE   AVAILABLE   AGE
istiod-1-14-6          1/1     1            1           47s # 1-14-6

istio-proxyコンテナのインジェクションの仕組みでいうと、以下の赤枠の要素です👇

istio_canary-upgrade_webhook_1-1

▼ Webhookの宛先のService

次に、 Serviceを確認すると、1-14-6のServiceがあります。

$ kubectl get service -n istio-system -l app=istiod

NAME            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                                 AGE
istiod-1-14-6   ClusterIP   10.96.93.151     <none>        15010/TCP,15012/TCP,443/TCP,15014/TCP   109s # 1-14-6

このServiceは、kube-apiserverからIstiodへのWebhookを仲介することにより、istio-proxyコンテナのインジェクションを可能にします。

istio-proxyコンテナのインジェクションの仕組みでいうと、以下の赤枠の要素です👇

istio_canary-upgrade_webhook_1-2

▼ 宛先のServiceを決めるMutatingWebhookConfiguration

最後に、MutatingWebhookConfigurationを確認すると、istio-revision-tag-<エイリアス>istio-sidecar-injector-<リビジョン番号>のMutatingWebhookConfigurationがあります。

$ kubectl get mutatingwebhookconfigurations

NAME                            WEBHOOKS   AGE
istio-revision-tag-default      2          114s  # カナリアアップグレード用
istio-sidecar-injector-1-14-6   2          2m16s # インプレースアップグレード用のため今回は言及しない

istio-proxyコンテナのインジェクションの仕組みでいうと、以下の赤枠の要素です👇

istio_canary-upgrade_webhook_1-3

これらのうち、前者 (istio-revision-tag-<エイリアス>) をカナリアアップグレードのために使用します。

このMutatingWebhookConfigurationは、Webhookの宛先のServiceを決めるため、結果的にistio-proxyコンテナのバージョンを決めます。

ここで、MutatingWebhookConfigurationのistio.io/revラベルとistio.io/tagラベルの値も確認しておきます。

$ kubectl get mutatingwebhookconfiguration istio-revision-tag-default -o yaml \
    | yq '.metadata.labels'

...

istio.io/rev: 1-14-6
istio.io/tag: default

...

istio.io/revラベルはIstiodのバージョン、istio.io/tagラベルはこれのエイリアスを表しています。

また、.webhooks[].namespaceSelectorキー配下のistio.io/revキーの検知ルールを確認します。

$ kubectl get mutatingwebhookconfiguration istio-revision-tag-default -o yaml \
    | yq '.webhooks[]'

...

namespaceSelector:
  matchExpressions:
    - key: istio.io/rev
      operator: In
      values:
        - default

...

合わせて、.webhooks[].clientConfig.serviceキー配下のServiceを名前を確認します。

$ kubectl get mutatingwebhookconfiguration istio-revision-tag-default -o yaml \
    | yq '.webhooks[].clientConfig'

...

service:
  name: istiod-1-14-6

...

整理すると、Namespaceでistio.io/revラベルにdefaultを設定しておけば、MutatingWebhookConfigurationがそれを検知し、特定のIstioのバージョンのServiceにWebhookを送信できるようになっています。

↪️ Istio / Safely upgrade the Istio control plane with revisions and tags


【2】 新Istiodのインストール

ここで実施すること

それでは、新Istiodをインストールします。

↪️ 参考:Control plane

istioctl versionコマンド

新しくインストールするIstiodのバージョンは、istioctlコマンドのバージョンで決まります。

そこで、istioctl versionコマンドを実行し、これのバージョンを確認します。

$ istioctl version

client version: 1.15.4        # アップグレード先のバージョン
control plane version: 1.14.6 # 現在のバージョン
data plane version: 1.14.6

istioctl installコマンド

カナリアアップグレードの場合、istioctl installコマンドを実行します。

ドキュメントではrevisionキーの値がcanaryですが、今回は1-15-4とします。

この値は、Istioが使用する様々なKubernetesリソースの接尾辞や、各種リソースのistio.io/revラベルの値になります。

$ istioctl install --set revision=1-15-4

WARNING: Istio is being upgraded from 1.14.6 -> 1.15.4
WARNING: Before upgrading, you may wish to use 'istioctl analyze' to check for IST0002 and IST0135 deprecation warnings.

✅ Istio core installed
✅ Istiod installed
✅ Ingress gateways installed
✅ Installation complete

Thank you for installing Istio 1.15.  Please take a few minutes to tell us about your install/upgrade experience!

kubectl getコマンド

▼ IstiodのDeployment

kubectl getコマンドを実行し、istioctl installコマンドで何をインストールしたのかを確認します👀

まずはIstiodのDeploymentを確認すると、1-15-4というDeploymentが新しく増えています。

$ kubectl get deployment -n istio-system -l app=istiod

NAME            READY   UP-TO-DATE   AVAILABLE   AGE
istiod-1-14-6   1/1     1            1           47s # 1-14-6
istiod-1-15-4   1/1     1            1           47s # 1-15-4

接尾辞の1-15-4は、revisionキーの値で決まります。

この段階では、旧Istiodと新Istioが並行的に稼働しており、kube-apiserverはまだ旧Istiodと通信しています

今の状況は以下の通りです👇

istio_canary-upgrade_webhook_2-1

▼ Webhookの宛先のService

次に Webhookの宛先のServiceを確認すると、istiod-1-15-4というServiceが新しく増えています。

$ kubectl get service -n istio-system -l app=istiod

NAME            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                                 AGE
istiod-1-14-6   ClusterIP   10.96.93.151     <none>        15010/TCP,15012/TCP,443/TCP,15014/TCP   109s # 1-14-6
istiod-1-15-4   ClusterIP   10.104.186.250   <none>        15010/TCP,15012/TCP,443/TCP,15014/TCP   87s  # 1-15-4

この段階では、まだWebhookの宛先はistiod-1-14-6のServiceです。

今の状況は以下の通りです👇

istio_canary-upgrade_webhook_2-2

▼ Webhookの宛先のServiceを決めるMutatingWebhookConfiguration

最後にMutatingWebhookConfigurationを確認すると、istio-sidecar-injector-1-15-4というMutatingWebhookConfigurationが新しく増えています。

$ kubectl get mutatingwebhookconfigurations

NAME                            WEBHOOKS   AGE
istio-revision-tag-default      2          114s  # カナリアアップグレードで使用する
istio-sidecar-injector-1-14-6   2          2m16s
istio-sidecar-injector-1-15-4   2          2m16s

カナリアアップグレードでは、istio-revision-tag-<エイリアス>のMutatingWebhookConfigurationを使用します。

今の状況は以下の通りです👇

istio_canary-upgrade_webhook_2-3

※ 実は、他にもインストールしているものがあるのですが、話をわかりやすくするために、今回は言及していません🙇🏻‍♂️


【3】 Webhookの宛先のServiceの変更

ここで実施すること

この手順では、エイリアスistio.io/tagラベルはそのままに、istio.io/revラベルの値を変更します。

さらに、Webhookの宛先のServiceを変更します。

↪️ 参考:

istioctl tag setコマンド

istioctl tag setコマンドを実行し、istio.io/revラベルの値と宛先のServiceを変更します。

$ istioctl tag set default --revision 1-15-4 --overwrite

実行後に、もう一度MutatingWebhookConfigurationを確認すると、istio.io/revラベルの値が変わっています。

$ kubectl get mutatingwebhookconfiguration istio-revision-tag-default -o yaml \
    | yq '.metadata.labels'

...

istio.io/rev: 1-15-4
istio.io/tag: default

...

また、Webhookの宛先のServiceも変わっています。

$ kubectl get mutatingwebhookconfiguration istio-revision-tag-default -o yaml \
    | yq '.webhooks[].clientConfig'

...

service:
  name: istiod-1-15-4

...

これらにより、Webhookの宛先が1-15-4のServiceとなるため、1-15-4istio-proxyコンテナをインジェクションできるようになります。

今の状況は以下の通りです👇

istio_canary-upgrade_webhook_3


【4】 IngressGatewayをインプレースアップグレード

ここで実施すること

Webhookの宛先が1-15-4のServiceに変わったところで、IngressGatewayをインプレースアップグレードします。

↪️ 参考:In place upgrade

kubectl rollout restartコマンド

kubectl rollout restartコマンドを実行し、IngressGatewayをインプレースアップグレードします。

$ kubectl rollout restart deployment istio-ingressgateway-n istio-ingress

再作成したPodのイメージを確認してみると、istio-proxyコンテナを1-15-4にアップグレードできています。

$ kubectl get pod bar -n bar -o yaml | yq '.spec.containers[].image'

docker.io/istio/proxyv2:1.15.4 # istio-proxyコンテナ

補足として、istioctl proxy-statusコマンドを使用して、アップグレードの完了を確認してもよいです。

今の状況は以下の通りです👇

istio_canary-upgrade_rollout-restart_2

なお、IngressGatewayのアップグレード時、マイクロサービスへのインバウンド通信が遮断されてしまうと思った方がいるかもしれません。

この点については、DeploymentがローリングアップグレードでIngressGatewayのPodを入れ替えるため、安心していただいて問題ありません🙆‍♂️


【5】 一部のNamespaceのistio-proxyコンテナをアップグレード

ここで実施すること

続けて、一部のNamespaceのistio-proxyコンテナをアップグレードします。

Podの再作成により、新Istiodのistio-proxyコンテナがインジェクションされるため。istio-proxyコンテナをアップグレードできます。

↪️ 参考:Data plane

kubectl rollout restartコマンド

前提にあるように、Namespaceには foo bar baz があります。

kubectl rollout restartコマンドを実行し、baristio-proxyコンテナからアップグレードします。

$ kubectl rollout restart deployment bar -n bar

再作成したPodのイメージを確認してみると、istio-proxyコンテナを1-15-4にアップグレードできています。

$ kubectl get pod bar -n bar -o yaml | yq '.spec.containers[].image'

bar-app:1.0 # マイクロサービス
docker.io/istio/proxyv2:1.15.4 # istio-proxyコンテナ

補足として、istioctl proxy-statusコマンドを使用して、アップグレードの完了を確認してもよいです。

今の状況は以下の通りです👇

istio_canary-upgrade_rollout-restart_3


【6】 ユーザの手を借りたテスト

ここで実施すること

Istioを部分的にアップグレードしたところで、アップグレードが完了したNamespaceをテストします。

ユーザーの手を借りて実地的にテストします (例:該当のエラーメトリクスが基準値を満たすか) 。

今の状況は以下の通りです👇

istio_canary-upgrade_rollout-restart_4

もし問題が起こった場合

もし問題が起こった場合、1-14-6にダウングレードしていきます。

istioctl tag setコマンドを実行し、istio.io/revラベルの値を元に戻します。

$ istioctl tag set default --revision 1-14-6 --overwrite

その後、kubectl rollout restartコマンドの手順を実行し、istio-proxyコンテナをダウングレードしてきます。


【7】 istio-proxyコンテナの段階的なアップグレード

ここで実施すること

先のNamespaceで問題が起こらなければ、残ったNamespace (foobaz、...) のistio-proxyコンテナも段階的にアップグレードしていきます。

kubectl rollout restartコマンド

同様にkubectl rollout restartコマンドを実行し、istio-proxyコンテナからアップグレードします。

$ kubectl rollout restart deployment foo -n foo

$ kubectl rollout restart deployment baz -n baz

...

最終的に、全てのNamespacemのistio-proxyコンテナが新しくなります。

今の状況は以下の通りです👇

istio_canary-upgrade_rollout-restart_5


【8】 旧Istiodのアンインストール

ここで実施すること

最後に、旧Istiodのアンインストールします。

↪️ 参考:Uninstall old control plane

istioctl uninstallコマンド

istioctl uninstallコマンドを実行し、旧Istiodをアンインストールします。

$ istioctl uninstall --revision 1-14-6

✅ Uninstall complete

今の状況は以下の通りです👇

istio_canary-upgrade_rollout-restart_6

kubectl getコマンド

▼ IstiodのDeployment

kubectl getコマンドを実行し、istioctl uninstallコマンドで何をアンインストールしたのかを確認します👀

まずはIstiodのDeploymentを確認すると、1-14-6というDeploymentが無くなっています。

$ kubectl get deployment -n istio-system -l app=istiod

NAME            READY   UP-TO-DATE   AVAILABLE   AGE
istiod-1-15-4   1/1     1            1           47s # 1-15-4

▼ Webhookの宛先のService

次に Webhookの宛先のServiceを確認すると、istiod-1-14-6というServiceが無くなっています。

$ kubectl get service -n istio-system -l app=istiod

NAME            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                                 AGE
istiod-1-15-4   ClusterIP   10.104.186.250   <none>        15010/TCP,15012/TCP,443/TCP,15014/TCP   87s  # 1-15-4

▼ 宛先のServiceを決めるMutatingWebhookConfiguration

最後にMutatingWebhookConfigurationを確認すると、istio-sidecar-injector-1-14-6というMutatingWebhookConfigurationが無くなっています。

$ kubectl get mutatingwebhookconfigurations

NAME                            WEBHOOKS   AGE
istio-revision-tag-default      2          114s  # 次のカナリアアップグレードでも使用する
istio-sidecar-injector-1-15-4   2          2m16s

これで、新Istiodに完全に入れ替わったため、アップグレードは完了です。

今の状況は以下の通りです👇

istio_canary-upgrade_webhook_4

※ 実は、他にもアンインストールしているものがあるのですが、話をわかりやすくするために、今回は言及していません🙇🏻‍♂️


05. おわりに

Istioの安全なアップグレード手法の仕組みをもりもり布教しました。

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

Istioのアップグレードの異常がシステムに与える影響力は非常に大きく、様々な問題 (体験談:istio-proxyコンテナのPodへのインジェクションがずっと完了せず、アプリコンテナを作成できない) が起こる可能性があります😇

これからIstioを採用予定の方は、Istioを安全にアップグレードするために十分に準備しておくことをお勧めします👍


【Istio⛵️】サイドカーインジェクションの仕組み



01. はじめに

どーも。

正月で激太りしましたが、ダイエットの予定はありません🙋🏻‍♂️

今回は、サービスメッシュを実装するIstioのサイドカーインジェクションに関する記事を投稿しました🚀

前回の記事に引き続き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

↪️ 参考:Istio / 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

↪️ 参考:Admission Controllers Reference | Kubernetes


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": {},
}

↪️ 参考:v1 package - k8s.io/api/admission/v1 - Go Packages

▼ 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",

    ...

  }
}

↪️ 参考:Dynamic Admission Control | Kubernetes

▼ 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) に追加します。

↪️ 参考:Dynamic Admission Control | Kubernetes


04. サイドカーインジェクションの仕組み

全体のフロー

前提知識を踏まえた上で、admission-controllersアドオンの仕組みの中で、サイドカーistio-proxyコンテナがどのようにPodにインジェクションされるのかを見ていきましょう。

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

画像の文字が小さくなってしまったため、拡大していただけると🙇🏻‍♂️

istio_container-injection_flow

↪️ 参考:https://www.amazon.co.jp/dp/B09XN9RDY1/


クライアント ➡︎ 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コンテナのインジェクションを有効化するようになりました。

エイリアスはどんな値でも問題なく、よくあるエイリアスとしてdefaultstableなどを使用します。

apiVersion: v1
kind: Namespace
metadata:
  name: foo-namespace
  labels:
    # istio-proxyコンテナのインジェクションを有効化する。
    # エイリアスは自由
    istio.io/rev: <エイリアス>

↪️ 参考:Istio / Announcing Support for 1.8 to 1.10 Direct Upgrades


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:
  namespace: istio-system
  name: istiod-<リビジョン番号>
  labels:
    app: istiod
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番ポートでリクエストを待ち受けます。

ここで、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

↪️ 参考:


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": {
                     ...
      }
    }
  }

  ...

]

↪️ 参考:

本題と話が逸れるため今回は詳しく言及しませんが、上記のpathc処理ではサイドカーコンテナのistio-proxyコンテナの他に、initコンテナのistio-initコンテナもインジェクションできるようにします。

このistio-initコンテナは、istio-proxyコンテナを持つPodでインバウンド/アウトバウンド通信の経路を制御できるように、Pod内にiptablesのルールを適用する責務を担っています💪🏻

↪️ 参考:Istio Sidecar's interception mechanism for traffic - SoByte

【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方式でエンコードした文字列>",
    },
}

↪️ 参考:istio/webhook.go at 1.14.3 · istio/istio · GitHub

【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と他コンポーネントの通信については、以下の方の記事と図が非常に参考になると思います🙇🏻‍♂️

kubernetes_kube-apiserver_communication

↪️ 参考:Kubernetes Master Components: Etcd, API Server, Controller Manager, and Scheduler | by Jorge Acetozi | jorgeacetozi | Medium


05. おわりに

Istioのサイドカーインジェクションの仕組みをもりもり布教しました。

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

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

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

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


【Istio⛵️】サービスディスカバリーの仕組み



01. はじめに

3-shake Advent Calender 2022 最終日の記事です🎅🎄

私は普段は 俺の技術ノート に知見を記録しており、はてなブログはデビュー戦となります。

さて今回は、サービスメッシュを実装するIstioのサービスディスカバリーに関する記事を投稿しました🚀

Istioの機能の一つである『サービスディスカバリー』の仕組みを、Envoyを交えながら、もりもり布教しようと思います (沼のまわりに餌をまく) 。

今回の記事では、先日の 3-shake SRE Tech Talk で発表した内容に加えて、スライドの余白と発表時間の制約で記載できなかったことも記載しました😗

↪️ 参考:⛵️ Istioによるサービスディスカバリーの仕組み - Speaker Deck


02. サービスディスカバリーについて

マイクロサービスアーキテクチャにおけるサービスディスカバリー

サービスディスカバリーとは

マイクロサービスアーキテクチャでは、マイクロサービスからマイクロサービスにリクエストを送信する場面があります。

サービスディスカバリーとは、宛先マイクロサービスの宛先情報 (例:IPアドレス、完全修飾ドメイン名、など) を検出し、送信元マイクロサービスが宛先マイクロサービスにリクエストを継続的に送信できるようにする仕組みのことです。

service-discovery.png

なぜサービスディスカバリーが必要なのか

そもそも、なぜサービスディスカバリーが必要なのでしょうか。

マイクロサービスアーキテクチャでは、システムの信頼性 (定められた条件下で定められた期間にわたり、障害を発生させることなく実行する程度) を担保するために、マイクロサービスのインスタンスの自動スケーリングを採用します。

この時、自動スケーリングのスケールアウトでマイクロサービスが増加するたびに、各インスタンスには新しい宛先情報が割り当てられてしまいます。

また、マイクロサービスが作り直された場合にも、宛先情報は更新されてしまいます。

このように、たとえインスタンスの宛先情報が更新されたとしても、インスタンスへのリクエストに失敗しない仕組みが必要です。

サービスディスカバリーの要素

サービスディスカバリーの仕組みは、次の要素からなります。

名前解決に関しては、DNSベースのサービスディスカバリー (例:CoreDNS + Service + kube-proxyによるサービスディスカバリー) で必要となり、Istioでは使いません。

そのため、本記事では言及しないこととします🙇🏻‍

service-discovery-pattern.png

要素 責務
送信元マイクロサービス リクエストを送信する。
宛先マイクロサービス リクエストを受信する。
サービスレジストリ 宛先マイクロサービスの宛先情報を保管する。
ロードバランサー 宛先マイクロサービスのインスタンスにロードバランシングする。
名前解決 宛先マイクロサービスへのリクエスト送信時に、名前解決できるようにする。


サービスディスカバリーのパターン

サービスディスカバリーのパターンとは

サービスディスカバリーの仕組みにはいくつか種類があります。

Istioのサービスディスカバリーは、このうちのサーバーサイドパターンを実装したものになります。

サーバーサイドパターン

service-discovery-pattern_client-side.png

送信元マイクロサービスから、問い合わせとロードバランシングの責務が切り離されています。

送信元マイクロサービスは、ロードバランサーにリクエストを送信します。

ロードバランサーは、宛先マイクロサービスの宛先をサービスレジストリに問い合わせ、またリクエストをロードバランシングする責務を担っています💪🏻

(例) Istio、Linkerd、など

↪️ 参考:

クライアントサイドパターン

service-discovery-pattern_server-side.png

通信の送信元マイクロサービスは、宛先マイクロサービスの宛先をサービスレジストリに問い合わせ、さらにロードバランシングする責務を担います。

(例) NeflixのEureka、など

↪️ 参考:


03. Istioのサービスディスカバリー

Istioのサービスディスカバリーの仕組み

Istioが実装するサービスメッシュには、サイドカープロキシメッシュとアンビエントメッシュがあり、今回はサイドカープロキシメッシュのサービスディスカバリーを取り上げます。

Istioのサービスディスカバリーは、discoveryコンテナとistio-proxyコンテナが軸となり、サーバーサイドパターンのサービスディスカバリーを実装します。

全体像

【1】 〜 【6】の全体像は、以下の通りです👇

istio-proxyコンテナは、サービスレジストリへの問い合わせと、ロードバランシングする責務を担っていることに注目してください。

service-discovery_istio.png

【1】

kube-apiserverは、Pod等の宛先情報をetcd等に保管します。

これは、Kubernetesの通常の仕組みです。

【2】

discoveryコンテナは、kube-apiserverからPod等の宛先情報を取得し、自身に保管します。

【3】

istio-proxyコンテナは、discoveryコンテナからPod等の宛先情報を双方向ストリーミングRPCで取得します。

【4】

送信元マイクロサービスがリクエストを送信します。

サーバーサイドパターンでの責務通り、送信元マイクロサービスはロードバランサー (ここではistio-proxyコンテナ) にリクエストを送信します。

この時、送信元マイクロサービスがistio-proxyコンテナに直接的にリクエストを送信しているというよりは、iptablesistio-proxyコンテナにリクエストをリダイレクトします。

istio-proxyコンテナこれを受信します。

【5】

istio-proxyコンテナは、リクエストをロードバランシングし、宛先Podにこれを送信します。

↪️ 参考:


discoveryコンテナの仕組み

discoveryコンテナを詳しく見てみましょう。

discoveryコンテナは、別名Istiodと呼ばれています。

XDS-APIというエンドポイントを公開しており、XDS-APIのうち、サービスディスカバリーに関係するAPIは以下の通りです。

APIの種類 説明
LDS-API Envoyのリスナー値を取得できる。
RDS-API Envoyのルート値を取得できる。
CDS-API Envoyのクラスター値を取得できる。
EDS-API Envoyのエンドポイント値できる。
ADS-API 各XDS-APIから取得できる宛先情報を整理して取得できる。

service-discovery_xds-api.png

discoveryコンテナは、kube-apiserverからPod等の宛先情報を取得して自身のメモリ上に保管し、各XDS-APIから提供します。

XDS-APIistio-proxyコンテナの間では、gRPCの双方向ストリーミングRPCの接続が確立されています。

そのため、istio-proxyコンテナからのリクエストに応じて宛先情報を返却するだけでなく、リクエストがなくとも、XDS-APIからもistio-proxyコンテナに対して宛先情報を送信します。

各種XDS-APIから個別に宛先情報を取得できますが、Envoy上で宛先情報のバージョンの不整合が起こる可能性があるため、Istioでは実際にはADS-APIを使用しています。

↪️ 参考:Amazon | Istio in Action | Posta, Christian E., Maloku, Rinor | Software Development


istio-proxyコンテナの仕組み

istio-proxyコンテナを詳しく見てみましょう。

istio-proxyコンテナでは、pilot-agentとEnvoyが稼働しています。

先ほどistio-proxyコンテナは、双方向ストリーミングRPCでADS-APIから宛先情報を取得すると説明しました。

厳密にはEnvoyが、pilot-agentを介して、ADS-APIから双方向ストリーミングRPCで宛先情報を取得します。

istio-proxyコンテナが送信元マイクロサービスからリクエストを受信すると、EnvoyはADS-APIから取得した宛先情報に基づいて、宛先マイクロサービスのインスタンスにロードバランシングします。

service-discovery_xds-api.png

↪️ 参考:


04. istio-proxyコンテナ内のEnvoyの仕組み

Envoyの処理の流れ

EnvoyがADS-APIから取得した宛先情報を見ていく前に、Envoyの処理の流れを解説します。

istio-proxyコンテナ内のEnvoyでは、以下の仕組みでリクエストを処理します。

全体像

【1】 〜 【6】の全体像は、以下の通りです👇

service-discovery_envoy.png

【1】

istio-proxyコンテナは、送信元マイクロサービスからリクエストを受信します。

【2】

Envoyは、リクエストの宛先情報 (例:宛先IPアドレス、ポート番号、パス、ホスト、など) に応じてリスナー値を選びます。

【3】

Envoyは、リスナーに紐づくルート値を選びます。

【4】

Envoyは、クラスターに紐づくクラスター値を選びます。

【5】

Envoyは、クラスターに紐づくエンドポイント値を選びます。

【6】

Envoyは、エンドポイント値に対応するインスタンスにリクエストを送信します。

Envoyで確認した宛先情報を👆に当てはめて見ていくことにしましょう。

↪️ 参考:


EnvoyがADS-APIから取得した宛先情報を見てみよう

config_dumpエンドポイント

実際にEnvoyに登録されている宛先情報は、istio-proxyコンテナ自体のlocalhost:15000/config_dumpからJSONで取得できます。

ただし、JSONだと見にくいので、yqコマンドでYAMLに変換すると見やすくなります。

もしお手元にIstioがある場合は、Envoyにどんな宛先情報が登録されているか、Envoyを冒険してみてください👍🏻

$ kubectl exec \
    -it foo-pod \
    -n foo-namespace \
    -c istio-proxy \
    -- bash -c "curl http://localhost:15000/config_dump" | yq -P

リスナー値

▼ 確認方法

istio-proxyコンテナがADS-APIから取得したリスナー値は、/config_dump?resource={dynamic_listeners}から確認できます。

ここでは、foo-pod内でbar-podのリスナー値を確認したと仮定します。

$ kubectl exec \
    -it foo-pod \
    -n foo-namespace \
    -c istio-proxy \
    -- bash -c "curl http://localhost:15000/config_dump?resource={dynamic_listeners}" | yq -P

▼ 結果

以下を確認できました。

  • 宛先IPアドレスや宛先ポート番号に応じてリスナー値を選べるようになっており、ここでは<任意のIPアドレス>:50002
  • リスナー値に紐づくルート値の名前
configs:
  - "@type": type.googleapis.com/envoy.admin.v3.ListenersConfigDump.DynamicListener
    # リスナー名
    name: 0.0.0.0_50002
    active_state:
      version_info: 2022-11-24T12:13:05Z/468
      listener:
        "@type": type.googleapis.com/envoy.config.listener.v3.Listener
        name: 0.0.0.0_50002
        address:
          socket_address:
            # 受信したパケットのうちで、宛先IPアドレスでフィルタリング
            address: 0.0.0.0
            # 受信したパケットのうちで、宛先ポート番号でフィルタリング
            port_value: 50002
        filter_chains:
          - filter_chain_match:
              transport_protocol: raw_buffer
              application_protocols:
                - http/1.1
                - h2c
            filters:
              - name: envoy.filters.network.http_connection_manager
                typed_config:
                  "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                  stat_prefix: outbound_0.0.0.0_50001
                  rds:
                    config_source:
                      ads: {}
                      initial_fetch_timeout: 0s
                      resource_api_version: V3
                    # 本リスナーに紐づくルート値の名前
                    route_config_name: 50002
  ...

  - "@type": type.googleapis.com/envoy.admin.v3.ListenersConfigDump.DynamicListener

  ...

↪️ 参考:

ルート値

▼ 確認方法

istio-proxyコンテナがADS-APIから取得したリスナー値は、/config_dump?resource={dynamic_route_configs}から確認できます。

ここでは、foo-pod内でbar-podのルート値を確認したと仮定します。

$ kubectl exec \
    -it foo-pod \
    -n foo-namespace \
    -c istio-proxy \
    -- bash -c "curl http://localhost:15000/config_dump?resource={dynamic_route_configs}" | yq -P

▼ 結果

コマンドを実行するとYAMLを取得でき、以下を確認できました。

  • リスナー値を取得した時に確認できたルート値の名前
  • リクエストのパスやHostヘッダーに応じてルート値を選べるようになっている
  • ルート値に紐づくクラスター値の名前
configs:
  - "@type": type.googleapis.com/envoy.admin.v3.RoutesConfigDump.DynamicRouteConfig
    version_info: 2022-11-24T12:13:05Z/468
    route_config:
      "@type": type.googleapis.com/envoy.config.route.v3.RouteConfiguration
      # ルート値の名前
      name: 50002
      virtual_hosts:
        - name: bar-service.bar-namespace.svc.cluster.local:50002
          # ホストベースルーティング
          domains:
            - bar-service.bar-namespace.svc.cluster.local
            - bar-service.bar-namespace.svc.cluster.local:50002
            - bar-service
            - bar-service:50002
            - bar-service.bar-namespace.svc
            - bar-service.bar-namespace.svc:50002
            - bar-service.bar-namespace
            - bar-service.bar-namespace:50002
            - 172.16.0.2
            - 172.16.0.2:50002
          routes:
            - match:
                # パスベースルーティング
                prefix: /
              route:
                # 本ルートに紐づくクラスター値の名前
                cluster: outbound|50002|v1|bar-service.bar-namespace.svc.cluster.local
                timeout: 0s
                retry_policy:
                  retry_on: connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes
                  num_retries: 2
                  retry_host_predicate:
                    - name: envoy.retry_host_predicates.previous_hosts
                  host_selection_retry_max_attempts: "5"
                  retriable_status_codes:
                    - 503
                max_stream_duration:
                  max_stream_duration: 0s
                  grpc_timeout_header_max: 0s
              decorator:
                operation: bar-service.bar-namespace.svc.cluster.local:50002/*

  ...

  - '@type': type.googleapis.com/envoy.admin.v3.RoutesConfigDump.DynamicRouteConfig

  ...

↪️ 参考:

クラスター値

▼ 確認方法

istio-proxyコンテナがADS-APIから取得したクラスター値は、/config_dump?resource={dynamic_active_clusters}から確認できます。

ここでは、foo-pod内でbar-podのクラスター値を確認したと仮定します。

$ kubectl exec \
    -it foo-pod \
    -n foo-namespace \
    -c istio-proxy \
    -- bash -c "curl http://localhost:15000/config_dump?resource={dynamic_active_clusters}" | yq -P

▼ 結果

コマンドを実行するとYAMLを取得でき、以下を確認できました。

  • ルート値を取得した時に確認できたクラスター値の名前
  • クラスター値に紐づくエンドポイント値の親名
configs:
  - "@type": type.googleapis.com/envoy.admin.v3.ClustersConfigDump.DynamicCluster
    version_info: 2022-11-24T12:13:05Z/468
    cluster:
      "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster
      # クラスター値の名前
      name: outbound|50002|v1|bar-service.bar-namespace.svc.cluster.local
      type: EDS
      eds_cluster_config:
        eds_config:
          ads: {}
          initial_fetch_timeout: 0s
          resource_api_version: V3
        # 本クラスターに紐づくエンドポイント値の親名
        service_name: outbound|50002|v1|bar-service.bar-namespace.svc.cluster.local
  ...

  - "@type": type.googleapis.com/envoy.admin.v3.ClustersConfigDump.DynamicCluster

  ...

↪️ 参考:

エンドポイント値

▼ 確認方法

istio-proxyコンテナがADS-APIから取得したクラスター値は、/config_dump?include_edsから確認できます。

ここでは、foo-pod内でbar-podのクラスター値を確認したと仮定します。

$ kubectl exec \
    -it foo-pod \
    -n foo-namespace \
    -c istio-proxy \
    -- bash -c "curl http://localhost:15000/config_dump?include_eds" | yq -P

▼ 結果

コマンドを実行するとYAMLを取得でき、以下を確認できました。

  • クラスター値を取得した時に確認できたエンドポイントの親名
  • bar-podのインスタンス3個あるため、3個のエンドポイントがあります

全てのエンドポイントのload_balancing_weightキー値が等しい場合、EnvoyはP2Cアルゴリズムに基づいてロードバランシングします。

configs:
  dynamic_endpoint_configs:
    - endpoint_config:
        "@type": type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment
        # エンドポイントの親名
        cluster_name: outbound|50002|v1|bar-service.bar-namespace.svc.cluster.local
        endpoints:
          - locality:
              region: ap-northeast-1
              zone: ap-northeast-1a
            lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      # 冗長化されたbar-podのIPアドレス
                      address: 11.0.0.1
                      # bar-pod内のコンテナが待ち受けているポート番号
                      port_value: 80
                  health_check_config: {}
                health_status: HEALTHY
                metadata:
                  filter_metadata:
                    istio:
                      workload: bar
                    envoy.transport_socket_match:
                      tlsMode: istio
                # ロードバランシングアルゴリズムを決める数値
                load_balancing_weight: 1
          - locality:
              region: ap-northeast-1
              zone: ap-northeast-1d
            lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      # 冗長化されたbar-podのIPアドレス
                      address: 11.0.0.2
                      # bar-pod内のコンテナが待ち受けているポート番号
                      port_value: 80
                  health_check_config: {}
                health_status: HEALTHY
                metadata:
                  filter_metadata:
                    istio:
                      workload: bar
                    envoy.transport_socket_match:
                      tlsMode: istio
                # ロードバランシングアルゴリズムを決める数値
                load_balancing_weight: 1
          - locality:
              region: ap-northeast-1
              zone: ap-northeast-1d
            lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      # 冗長化されたbar-podのIPアドレス
                      address: 11.0.0.3
                      # bar-pod内のコンテナが待ち受けているポート番号
                      port_value: 80
                  health_check_config: {}
                health_status: HEALTHY
                metadata:
                  filter_metadata:
                    istio:
                      workload: bar
                    envoy.transport_socket_match:
                      tlsMode: istio
                # ロードバランシングアルゴリズムを決める数値
                load_balancing_weight: 1
        policy:
          overprovisioning_factor: 140

    ...

    - endpoint_config:

    ...

↪️参考:

Envoyの処理の流れのまとめ

確認できた宛先情報を、Envoyの処理の流れに当てはめてみました。

service-discovery_envoy_detail.png

【1】

送信元マイクロサービスは、宛先マイクロサービス (<任意のIP>/:50002) にリクエストを送信し、サイドカーコンテナのistio-proxyコンテナはこれを受信します。

【2】

Envoyは、リクエストの宛先 (IPアドレス、ポート番号、パス) からPodのリスナー値 (0.0.0.0_50002) を選びます。

【3】

Envoyは、リスナーに紐づくPodのルート値 (50002) を選びます。

【4】

Envoyは、クラスターに紐づくPodのクラスター値 (outbound|50002|v1|bar-service.bar-namespace.svc.cluster.local) を選びます。

【5】

Envoyは、クラスターに紐づくPodのインスタンスのエンドポイント値 (11.0.0.X/:80) を選びます。

【6】

Envoyは、エンドポイント値の宛先にPodのリクエストを送信します。

サービスディスカバリーの冒険は以上です⛵


05. おわりに

Istioの機能の一つである『サービスディスカバリー』の仕組みを、Envoyを交えながらもりもり布教しました。

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

ここまで見ていただいたそこのあなた、片足が沼に浸かってます😏


謝辞

3-shake SRE Tech Talk での発表前後に、以下の方々に、発表内容について助言をいただきました。

(アルファベット順)

また、今回の 3-shake Advent Calender 2022 は、以下の方々に企画いただきました。

(アルファベット順)

皆様に感謝申し上げます🙇🏻‍♂️