Enjoy Architecting

Twitter: @taisho6339

【ArgoCD】最小ではじめるプロジェクトごとのユーザ権限管理

本記事の概要

ArgoCDをクラスタにインストールするとまずadminユーザが作られます。 しかし、adminクラスタは権限が強く(いわゆるsuperuser)、自由にアプリケーションを更新したり削除したりできてしまいます。

マイクロサービスなどによってサービスごとに分割されたチームでは、チームごとに異なる権限を与えることが多いはずで(他チームのリソースへの権限をつけないなど)、adminユーザを共有するわけにはいきません。

とはいえ、個別にユーザを発行して権限を管理していくのも運用負荷がそれなりにかかります。 そこで本記事ではsmall teamのための最小ではじめるユーザの権限を管理を提案してみます。

ArgoCDのユーザと権限管理

ArgoCDでは、バージョン1.5以降からユーザのRBAC機能が実装されています。 これによって、ユーザに対して細かいリソースアクセスの権限設定ができます。

ドキュメントに記載している通り、ArgoCDでは直接ユーザを定義していく方法とSSOにより、GoogleGitHubのアカウント認証を通してユーザを定義していく2通りの方法を提供しています。 SSOを使えばユーザのグルーピングなど、複雑な機能を扱えたりするのですが、小規模な運用チームでは少々過剰なので、直接ユーザを定義していくLocal usersを作成していく方針でいくことにします。

想定する環境

今回は、クラスタ内にいろんなチームのサービスが混在する環境ということで、 マルチテナントなクラスタを想定します。 当然TeamAの人々はTeamBのサービス群に対して権限を持ちたくないハズです。 f:id:taisho6339:20200730183026p:plain

提案する構成

f:id:taisho6339:20200730183051p:plain ArgoCDのProjectリソースを論理的なサービスの区分けごとに定義し、 このProjectに対してのみ権限を持つユーザを定義します。

Project & Role定義

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: projectA
  namespace: argocd
spec:
  description: projectA project
  sourceRepos:
    - '*'
  destinations:
    - namespace: '*'
      server: '*'
  clusterResourceWhitelist:
    - group: '*'
      kind: '*'
  roles:
    - name: viewer
      description: Read-only privileges to projectA
      policies:
        - p, proj:projectA:viewer, applications, get, projectA/*, allow
    - name: editor
      description: Edit privileges to projectA applications
      policies:
        - p, proj:projectA:editor, applications, create, projectA/*, allow
        - p, proj:projectA:editor, applications, update, projectA/*, allow
        - p, proj:projectA:editor, applications, delete, projectA/*, allow
        - p, proj:projectA:editor, applications, sync, projectA/*, allow
        - g, proj:projectA:editor, proj:projectA:viewer
  • ここでは参照用ロールと編集用ロールを定義しています。
  • pがpolicy、gがおそらくgrantです。pでどのロール、ユーザに対して、どのような操作を拒否/許可するかというルールを定義し、gで実際にロール、ユーザに対してロールをアサインできます。(ロールに対してロールをアサインできるので、上記ではeditorにviewerのポリシーも付与しています)

Account

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
data:
  accounts.projectA-viewer: login,apiKey
  accounts.projectA-editor: login
  • argocd-cmに定義することでLocal Userの追加を行うことができます。
  • loginはGUIへログインして使う用途、apiKeyはCLI用にトークンを発行して使う場合に設定します。

Roleのアサイ

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-rbac-cm
data:
  policy.csv: |-
    g, projectA-viewer, proj:projectA:viewer
    g, projectA-editor, proj:projectA:editor

実際にロールをアサインするにはargocd-rbac-cmに記載します。 projectファイルに記載しても動作しますが、こちらに集約したほうが一見してユーザと権限の割当がわかるのでおすすめです。

ユーザのパスワード更新

仕上げに作成したユーザがログインできるようにパスワードを更新しておきます。

argocd account update-password \
  --account <name> \
  --current-password <current-admin> \
  --new-password <new-user-password>

また、匿名ユーザや、デフォルトのロールアサインを無効にしておくとより安全だと思います。

結果

  • 管理画面にログインしてみると、read権限がないほかのプロジェクトのサービスは見えないようになっています
  • また、viewerでsyncしようとした結果 f:id:taisho6339:20200730183113p:plain

分散システムにおけるScalableな名前付けアルゴリズム「Chord Protocol」を実装してみた

概要

分散システムを学術的に学びたくて、 Chord Protocolというアルゴリズムが面白かったので実際に論文を読んで実装してみました。 この記事では、分散システムにおける名前付けの概念と、Chord Protocolの紹介、簡単な検証について言及していこうと思います。

実装したシステム
実際に作ったサーバ

分散システムにおける名前付けとは?

分散システムの分野には「名前」、「名前付け」、「アドレス」と呼ばれる概念があります。 それぞれどのような意味を持っているのでしょうか?

名前付けと名前

分散システムはネットワークを通じてそれぞれのサーバ、プロセスが協調して動作しています。 この中で、各サーバ、プロセスはやり取りをする相手の「名前」を知らなければやり取りを行うことができません。 この名前の解決を行うことを「名前付け」と呼んでいます。 そして、あるリソース(特定のプロセス、サーバなど)を一意に特定するための文字列を「名前」呼んでいます。

アドレス

名前が分かっていても実際にはそれだけではやり取りできず、実際には「アドレス」と呼ばれるリソースの住所に対してアクセスすることでやり取りが成立します。 つまり、リソースに対する実質的なアクセスポイントをアドレスと呼んでいます。 名前との違いは、名前は永続的にリソースを一意に特定するもの、 アドレスはアクセスするために必要な実際のリソースの住所、つまり位置情報を持っているということです。 名前は永続的に同じリソースを指し続けるのに対し、アドレスはそのときリソースが存在する位置によって変化します。

具体例

ドメイン名(example.com)が「名前」、example.comに紐づくAレコードのIPが「アドレス」に当たります。 ドメイン名はずっと変わりませんが、IPアドレスはサーバを移管したりすることで変化することがありえます。 永久に同じリソースへのポインタになるのでドメインは「名前」として成立するわけです。

Naming Serverとは?

名前からアドレスを解決したり、特定のリソースに名前をつけてくれるサービスのことをNaming Serviceと呼びます。 一番わかり易い例としては、DNSが挙げられます。 DNSは、ドメインという名前からIPというアドレスを解決し、またIPに対してドメイン名をつける名前付けを行うことで成立しています。

代表的なアルゴリズム ~Consistent Hash~

名前付け、名前解決のためのアルゴリズムとしてメジャーなものにConsistent Hashがあります。 Consistent Hashは、 サーバの集合(以降各要素をノードと呼ぶ)に対してハッシュ関数で一意のID(たいていはホスト名などのハッシュ)を振り、 各ノードをID順に円状に並べ、 あるIDがどのノードに配置されるべきかを決定するアルゴリズムです。 与えられたID以上であり、値が一番近いノードに解決され、 円状になっているので、 仮にそのノードがいなくなったとしても次のノードに回されるといったように、ノード障害に強いアルゴリズムになっています。

詳しくは、こちらがわかりやすいです。 コンシステントハッシュ法

ハッシュによって名前付けを行い、 ハッシュによるIDから対応するサーバを解決して名前解決を行います。

f:id:taisho6339:20200722162239p:plain (引用: https://vitalflux.com/wtf-consistent-hashing-databases/)

Consistent Hashの拡張、Chord Protocol

前述のConsistent Hashは完璧ではなく、欠点があります。 それは、ノードの離脱、ノードの参画がいつ起こったとしても、 ただしい結果を返し続けるためには一つのノードは、参加している他のすべてのノードの状態を把握している必要があることです。 (そうでないとノードが増えたり減ったりした時点で正しく結果を返すことができなくなる) これは大規模な分散システムになればなるほど、大きな課題になりえます。 大規模なノードの集合では各ノードは他のノードの情報を保持するために大規模なリソースを要求し、また、ノードの参加、離脱時の情報更新などの計算コストも高くつくことになります。 これを解決するべく生まれたのがChord Protocolになります。

基本的なアルゴリズムは下記の資料がとてもわかり易くまとめられています。

Chord Protocolについて

Chord Protocolは何が優れているのか?

基本的なことは上の資料がとてもわかり易いので、ここではかいつまんで何が優れているポイントなのかを説明しておきます。 乱暴にいってしまえば、

  • 経路表による高速な検索
  • ノード参画、離脱時に各ノードが独立して経路表を更新する仕組み
  • 経路表が最新化されていない場合のためのFallback手段としてのSuccessorList

が主なポイントになります。 これらのポイントにより、耐障害性を担保しつつスケーラビリティを向上させることに成功しています。 順に追っていきましょう。

経路表による高速な検索

各ノードは、全てのノードの状態を維持する代わりに、一定サイズの経路表をメンテナンスします。 例えば、32ビット長のハッシュをIDとする場合、32個の行数を持つ経路表を持ち、 この表を元にして、他ノードにルーティングして検索します。 計算量はO(logN)になり(Nはノード数)、これによって大幅にスケーラビリティを担保できるようになります。

ノード参画、離脱時に各ノードが独立して経路表を更新する仕組み

Chord Protocolでは、ノードの参画、離脱時に各ノードがそれぞれ安定化させるためのルールを規定しています。 このルールに従った非同期プロセス、ないしスレッドが一定間隔で動作していて、経路表のメンテナンスを行います。 このメンテナンスは、ノードの離脱、参画に対して、メッセージ数がO(log2N)程度で、そこそこ高速に実行されます。

経路表が最新化されていない場合のためのFallback手段としてのSuccessorList

ノードが参加した直後などは当然経路表は最新化されていません。 よって一時的に正しい結果を返せない可能性もあります。 そこで、この資料のスライド131ページあたりに記載されているように、 Successor Listというリストも保持しておき、経路表による検索に失敗した場合はこのリストを使ってFallbackすることができます。

実際に実装してみた

ここまでがNaming Service及び、Chord Protocolの説明でした。 今回は、これをもとに実際にNaming ServiceをGoで実装してみました。 Goで書いたChordの参照実装なので名前はgordです。(安直)

https://github.com/taisho6339/gord

gordの概要

これはどういう実装かというと、 GordがChord Protocolのノード郡を管理するためのプロセスとして常駐し、 別途ストレージを管理するソフトウェアがgRPCでGordに対してアクセスし、 特定のキーがどのノードに存在するかを検索したり、 自分たちが管理するべきキーを判別したりする、といった機能を提供します。 k8sであればサイドカーとして各Podにくっついているイメージになります。 この構成は実際に論文でもサンプルとして提案されている構成になります。

論文で提案されている使用例
論文で提案されている構成
実装したシステム
実際に落とし込んだ構成

実装に対する検証

実際に実装に落とし込んだところで、ちゃんと実装できているのかを検証したいと思います。 機能的な観点は、単体テストにて実装しているので今回はパフォーマンス、耐障害性について検証してみます。 ただし、ローカルマシンでの検証につき、プロセッサ数に限度があるため、 お遊び程度の検証しかできないのはあしからず...

パフォーマンスに関する検証

探索のパフォーマンスを測定してみます。

ノード数を1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024と増やしていったときに特定キーの検索にかかる時間を計算してみましょう。

[ベンチマークテストの実装は雑ですがこの辺です。] (https://github.com/taisho6339/gord/blob/master/chord/bench_search_test.go)

すでにStabilizationを完了したノードを指定数用意し、検索のパフォーマンスの違いを見ていきます。

実験にした私のマシンは下記のスペックになります。

OS: MacOS Catalina 10.15.5
プロセッサ: 2.4 GHz 8コア/16スレッド Intel Core i9
メモリ: 64 GB 2667 MHz DDR4

結果はこんな感じになりました。

ノード数 検索にかかった時間
1 20.1 ns/op
2 4440 ns/op
4 4450 ns/op
8 4644 ns/op
16 4644 ns/op
32 6473 ns/op
64 19778 ns/op
128 25808 ns/op
256 35940 ns/op
512 35825 ns/op
1024 36940 ns/op

ローカルマシンでの検証なので32ノード以上になってくると、 ノードが実行している大量のゴルーチンを限られたプロセッサで共有せざるを得なくなってくるため、アルゴリズム以外の要因で遅くなっていそうです。 少なくともノード数によって線形に増加している傾向は見られませんでした。

耐障害性に関する検証

Dockerを使って試しに3ノードたててみます。

docker-compose build && docker-compose up

gord1_1  | time="2020-07-22T06:59:08Z" level=info msg="Running Gord server..."
gord1_1  | time="2020-07-22T06:59:08Z" level=info msg="Gord is listening on gord1:26041"
gord1_1  | time="2020-07-22T06:59:08Z" level=info msg="Running Chord server..."
gord1_1  | time="2020-07-22T06:59:08Z" level=info msg="Chord listening on gord1:26040"
gord1_1  | time="2020-07-22T06:59:09Z" level=info msg="Host[gord1] updated its successor."
gord2_1  | time="2020-07-22T06:59:09Z" level=info msg="Running Gord server..."
gord2_1  | time="2020-07-22T06:59:09Z" level=info msg="Gord is listening on gord2:26041"
gord3_1  | time="2020-07-22T06:59:09Z" level=info msg="Running Gord server..."
gord3_1  | time="2020-07-22T06:59:09Z" level=info msg="Gord is listening on gord3:26041"
gord2_1  | time="2020-07-22T06:59:09Z" level=info msg="Running Chord server..."
gord2_1  | time="2020-07-22T06:59:09Z" level=info msg="Chord listening on gord2:26040"
gord3_1  | time="2020-07-22T06:59:09Z" level=info msg="Running Chord server..."
gord3_1  | time="2020-07-22T06:59:09Z" level=info msg="Chord listening on gord3:26040"
gord2_1  | time="2020-07-22T06:59:09Z" level=info msg="Host[gord2] updated its successor."

すぐにStabilizerがノード間連携をし、経路表を更新することでどのノードも一貫した結果を返しています。

grpcurl -plaintext -d '{"key": "gord1"}' localhost:26041 server.ExternalService/FindHostForKey \
&& grpcurl -plaintext -d '{"key": "gord1"}' localhost:36041 server.ExternalService/FindHostForKey \
&& grpcurl -plaintext -d '{"key": "gord1"}' localhost:46041 server.ExternalService/FindHostForKey
{
  "host": "gord1"
}
{
  "host": "gord1"
}
{
  "host": "gord1"
}

つぎに試しに1ノード退場させてみます。

docker stop 1a3bf85b693b

gord3_1  | time="2020-07-22T07:04:43Z" level=error msg="successor stabilizer failed. err = &errors.errorString{s:\"NodeUnavailable\"}"
gord2_1  | time="2020-07-22T07:04:43Z" level=warning msg="Host:[gord1] is dead."
gord3_1  | time="2020-07-22T07:04:43Z" level=warning msg="Host:[gord1] is dead."
gord_gord1_1 exited with code 0

すぐにノードのダウンを検知し、更新に入っています。

grpcurl -plaintext -d '{"key": "gord1"}' localhost:36041 server.ExternalService/FindHostForKey \                    ✘ 1
&& grpcurl -plaintext -d '{"key": "gord1"}' localhost:46041 server.ExternalService/FindHostForKey

{
  "host": "gord2"
}
{
  "host": "gord2"
}

ノード1がダウンしてもすぐに経路表が更新され、さらにノード1に本来属していた"gord1"というキーが別のノードに移っていることがわかります。

まとめ

実際にChord Protocolを実装することで、 実装としては、未熟な点やバグなど残っていると思いますが、何が優れていて、既存の手法とは何が違うのかを実際に理解することができました。 また付加価値として256ビットにもなる大きな数値をGolangでどう計算するのか、 Golangでの並行処理についても勉強することができました。 こういったことが直接お仕事の役に立つケースは多くはないかもしれませんが、実装力のレベルを一段引き上げてくれる良い機会になり、楽しむことができました。 今度はもっと身近なOSSなどで利用されているアルゴリズムなども実装してみたいと思います。

これだけは知っておこう負荷試験 ~その1 基本とお試し試験~

記事の概要

負荷試験はシステム運用において避けて通れないタスクであるが難易度が高いタスクでもある。

本記事ではまず、実際にWordpressに対して簡単な負荷試験を行いながら、 負荷試験における基本的な観点を整理していく。

負荷試験の主な目的

負荷試験は何のために行うのだろうか? 主な観点としては下記のような項目が挙げられる。

  • 負荷に耐えうる構成か?
    • システムが想定した負荷に耐えられることを確認する
    • 長時間負荷をかけ続けた場合にメモリリークなどの潜在的なバグがないかを確認する
  • 要件に対して最適な構成になっているか?
    • 突然スパイクしたときにも自動でスケールし耐えられる構成になっているか?
    • 通常時に必要以上にインフラリソースを確保していないか?
  • スケール特性を把握する
    • まずどこにボトルネックが来て、どこをチューニングすればシステムがスケールするかを確認する

負荷試験の大事なルール

  • 必ずどこかがボトルネックになっている状態を作ること

  • スコープを絞ってステップごとに区切って実施すること

    • 問題の原因特定を効率的に行うことができるため、少しずつ負荷のかけ方を変えながら実施する
  • ネットワーク的に近いところから攻撃する

    • クラウドであれば同じVPC内から攻撃するのが望ましい
    • ネットワーク環境は千差万別であり、コントロールできないため、純粋にシステムの負荷を可視化したいのであればネットワーク起因の問題は排除した上で試験するべきである
  • 最初は最小の構成 & 自動スケールしないようにして実施する

    • ボトルネックがどこにあるかがわからない状態でオートスケールする構成で実施してしまうと、ボトルネックの特定が困難になる。よって、スケール特性を把握するフェーズで初めてオートスケールするようにする。

負荷試験をする上で参考にする性能指標

負荷試験実施の際は、下記指標を確認していき、問題があった場合はCPU使用率やメモリ使用率、コネクション数などの細かい指標を追っていくことになる。

  • スループット

    • 単位時間あたりどのくらいの処理をさばくことができるのか
  • レイテンシ

    • 処理をリクエストして、結果がクライアントに返ってくるまでどのくらいの時間がかかったか

実際の負荷試験のステップ例

  • 攻撃サーバのセットアップ

    • 最大攻撃性能を把握することで攻撃のパフォーマンスが十分に発揮できているかを確認する。
  • フレームワークの素の性能の把握

  • DBへの参照を含めた性能の把握

    • DBとの接続方法、クエリの実行方法などが適切に設定、実装されているかを確認する。
    • 前工程と比較してDB参照に大幅な劣化がある場合は、下記観点で確認する。
      • Web, DB, 攻撃サーバで負荷が正しくかかっているかを確認する
      • バッファプールにデータが乗り切っているか、キャッシュヒット率はどのくらいか
      • コネクションを永続化できているか確認する(コネクションプーリングの設定)
      • アプリケーション側でN+1やスロークエリなどが出ているか
  • DBへの更新を含めた性能の把握

    • DBとの接続方法、クエリの実行方法などが適切に設定、実装されているかを確認する。
    • 前工程と比較してDB参照に大幅な劣化がある場合は、下記観点で確認する。
      • Web, DB, 攻撃サーバで負荷が正しくかかっているかを確認する
      • 必要以上に偏ったユーザデータのシナリオになっていないか? ※例えばテストユーザを一人だけにして更新テストを行う場合、ロックの傾向が偏ってしまい、本番のシナリオと剥離してしまう。
      • 不必要なロック、トランザクション分離レベルなどを見直しつつ調整
  • 外部サービスとの通信を含めた性能の把握

    • 外部サービスとネットワークを経由して通信しているケースの負荷シナリオ
    • 自サービスの管轄外の場合、ここがボトルネックになりやすい
    • 負荷をかける場合は、意図せず外部サービスに過負荷をかけてしまわないよう気をつける
  • 実際の使用を想定したシナリオでの性能の把握

    • 実際のユーザのシナリオを想定し、負荷をかける
    • このシナリオでの負荷試験でしっかり想定どおりのパフォーマンスがでるかどうかを検証する
  • スケール特性試験

    • リソースが想定より余裕がある場合はスケールイン、スケールダウンして試してみることで必要以上にコストがかかっていないかを検証する

    • 負荷をあげていったときにどこがボトルネックになり、実際にそのボトルネックをスケールアウト、スケールアップ、パラメータチューニングにより解消したときに、システムが想定どおりスケールすることを確認する

実践してみよう!

環境

この環境で出せる攻撃時スループットの上限値を知る

現時点で、MAXでどのくらいのスループットがでるのかを検証するため、 一番処理の軽い静的ファイルを返すエンドポイントに対し、 Apache Benchを使って負荷をかけてみる。 ネットワーク的な要因を排除した純粋なスループットを計測したいのでホストから実行する。 ネットワークはスループットをかなり落としてしまう要因になるので、実際の負荷試験においてもなるべくネットワーク的に近いところから試験することはかなり重要になってくる。

ab -n 20000 -c 200 -k http://10.108.252.89/readme.html

大体この数値がこの環境における攻撃時に出せる最大のスループットということになる。

Requests per second:    7711.03 [#/sec] (mean)

検証の妥当性

この結果が妥当であることをどのように判別すればよいだろうか? まず最初に見るべき観点は以下になる。

  • 対象システムのCPU使用率が100%近くまで上がっているか?

    • ※DBサーバへの負荷の場合はもう少し下がることが多い
  • 攻撃サーバ側のCPU使用率は100%近くまで上がっているか?

    • 余裕がある場合は、同時接続クライアント数が足りていない

極端に同時接続クライアントが多すぎる場合スループットには出ないが、レイテンシが極端に落ちる。(リクエストが実行されるまでの待ち時間も含まれてしまうため)

こうなると問題の切り分けが大変になってしまうので、同時接続クライアント数は多すぎず、少なすぎない状態で実施するのがベスト。

(十分に負荷がかかっているのに必要以上にクライアント数をあげない)

今回は静的ファイルのため、Webサーバ単体への負荷となり、WebサーバはCPU使用率が90%以上で張り付いていたため、 十分に負荷をかけられていると判断した。

Wordpressの実際のスループット

今度は静的ページではなく、MySQLへのアクセスを伴う実際のページにアクセスしてみる。

ab -n 20000 -c 200 -k http://10.108.252.89

スループットは下記まで落ち込んだ。

Requests per second:    30.93 [#/sec] (mean)

また、DBもWebもCPU使用率90%以上で張り付いているので負荷はしっかりかかっていると見て良さそうである。

DB側がボトルネックになっていそうなので、DBのPodをスケールアップしてみる。

DBをスケールアップしてみた後

もともとが1コアを割り当てていたので、2コア割り当てるようにしてみた。

kubectl describe node

  Namespace                  Name                                CPU Requests  CPU Limits  Memory Requests  Memory Limits  AGE
  ---------                  ----                                ------------  ----------  ---------------  -------------  ---
  default                    wordpress-5bbd7fd785-gswtm          0 (0%)        0 (0%)      0 (0%)           0 (0%)         22h
  default                    wordpress-mysql-88898b8ff-jhj9g     2 (50%)       2 (50%)     0 (0%)           0 (0%)         22h
ab -n 20000 -c 200 -k 10.108.109.63

両者ともにCPU使用率は90%以上で、スループットが純粋に向上した。

Requests per second:    70.14 [#/sec] (mean)

ただ、またもやDBもWeb側もCPUが90%以上になっているため、 まずDB側をスケールアップしないとスループットは上がらなさそうである。 今回はアプローチを変えてDBを一定時間キャッシュさせ、DBアクセスをしないようにしてみる。

DBキャッシュ適用後

DBキャッシュを有効にした結果、スループットが劇的に改善している。

Requests per second:    3120.29 [#/sec] (mean)

DB側のCPU使用率も一瞬はねたが、一気に下がった。 一方Web側は変わらずCPU使用率が90%超えであり、 ボトルネックがDBからWebに移ったのが分かる。

まとめ

ボトルネックがWeb側に移動したのでWeb側をスケールしたいところだが、ローカルの貧弱な環境だと限界なのでローカルでの試験は一旦ここまでとする。

基本的な流れをこのさきのステップも同じで、

を繰り返していくことになる。

次回はKubernetesにアプリケーションをデプロイし、複数ノード & 攻撃サーバも負荷対象システムもスケールできる環境で実際にやってみる。

~ TO BE CONTINUED ~

Istioで割合でTraffic Managementするときにユーザごとにセッションを固定する

解決したい課題

Istioでweightによってサービスのバージョンを切り替えているとき、 リクエストの割合ベースで切り替えているだけなので、 同一ユーザでも異なるバージョンが表示されてしまう。 ユーザには少なくとも一定期間は同じバージョンを見せたいケースが多いと思うので今回はその方法を検証してみた。

Istioに用意されているSession Affinity機能

IstioではDestinationRuleで、ユーザごとに一定期間バージョンを固定化してルーティングするよう宣言できる。 https://istio.io/docs/reference/config/networking/destination-rule/#LoadBalancerSettings-ConsistentHashLB だがこれはConsistent Hashアルゴリズムによって実装されているため、PodがHPAでスケールアウト、スケールインしたときにルーティングされる向き先が変わってしまう。 また、この機能はweightの指定によってルーティングを設定しているときには併用してつかうことができない。 この機能はIstioというよりは実際にはEnvoyが担っていて、Envoy側のIssueを見る限りまだ解決されている様子はない。 https://github.com/envoyproxy/envoy/issues/8167

ではどうすればよいのか?

今回はWorkAround的な手法として、Cookieを用い、下記の方法を考えた。

  • Cookieがないリクエストに関しては、設定した割合でそれぞれのバージョンに割り振る
  • Cookieを持っているリクエストに関しては、そのCookieが保持しているバージョン情報に基づいて割り振る

あるバージョンに割り振られたトラフィックに対して、レスポンスにSetCookieヘッダーを付与し、 そこにサービスのバージョン情報を付与する、という方針を考えた。

具体的な実装

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: sample-service-gateway
spec:
  selector:
    istio: ingressgateway # use istio default controller
  servers:
    - port:
        number: 80
        name: http
        protocol: HTTP
      hosts:
        - "*"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: sample-service-gateway-vs
spec:
  hosts:
    - "*"
  gateways:
    - sample-service-gateway
  http:
    - match:
        - headers:
            Cookie:
              exact: sample-service-version=v1
      route:
        - destination:
            host: sample-service
            subset: v1
            port:
              number: 8080
    - match:
        - headers:
            Cookie:
              exact: sample-service-version=v2
      route:
        - destination:
            host: sample-service
            subset: v2
            port:
              number: 8080
    - route:
        - destination:
            host: sample-service
            subset: v1
            port:
              number: 8080
          weight: 50
          headers:
            response:
              add:
                "Set-Cookie": sample-service-version=v1; Max-Age=2592000
        - destination:
            host: sample-service
            subset: v2
            port:
              number: 8080
          weight: 50
          headers:
            response:
              add:
                "Set-Cookie": sample-service-version=v2; Max-Age=2592000
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: sample-service
spec:
  host: sample-service
  subsets:
    - name: v1
      labels:
        version: v1
    - name: v2
      labels:
        version: v2

これで実際にアクセスしてみると、最初は50%の確率でどちらかに割り振られ、それ以降のリクエストは同じバージョンへルーティングされる。

まとめ

今回はIstioでCookieを用いることで、バージョンを固定したルーティングを行った。 A/Bテストなど、ユーザには一貫した結果を出したい場合に活用したい。

Istio + Argo Rolloutとカナリアリリース

本記事について

Argo Rolloutを導入すると、様々なデプロイ戦略を実践することができる。 ただ少し実際の運用イメージが見えづらかったので整理してみる。

Argo Rolloutについての概要

Argo Rolloutとは通常のk8sのDeploymentに加え、カナリアリリースなどの様々なアップデート戦略をサポートするためのコンポーネント。 Rolloutという、Deploymentに対し、様々なPodのアップデート戦略をつけ加えたCRDを用いる。 Deployment同様に内部ではReplicaSetを管理していて、 アップデートのたびに新しいReplicaSetを作り、設定の通りにPodを移行させる。

カナリアリリース

一部のリクエストを新しいバージョンに流し、なにかあればロールバック、何も無ければ徐々にトラフィックを増やし、最終的に100%に切り替える。 ArgoRolloutでは、Rolloutリソースにどのように移行していくかをステップごとに宣言する。 設定できるのは下記のような項目。

  • setWeightでカナリアバージョンの比重を設定する
  • pauseでその状態でどのくらい待機するかを定義する
  • analysisで同期的な新バージョンのヘルスチェックを挟む

ヘルスチェックについて

B/Gやカナリアの場合、定期的にヘルスチェックを行い、失敗した場合にロールバックすることができる。 実際にはAnalysisTemplateに記述し、Rolloutで指定することで、実行時にAnalysisRunリソースが作られ、 指定したヘルスチェックを実行する。 AnalysisTemplateでは、下記の実行形式をサポートしている。

  • Prometheusサーバからのメトリクス取得

    • 一定時間内の200エラーが95%以上など
    • 一定時間内の5xx系エラーが1%以下など
  • Job Metrics

  • WebHook

    • クラスタ内、クラスタ外に対して指定タイミングでリクエストを送り、その結果でもって判別する
    • 結果はJSONである必要があるが、形式は問わない(パースの設定をすることができる)

ちなみにAnalysisを定義しない場合はロールバックは自動で行わない。

結局Argo Rolloutを使って担保されるものはなに?

カナリアリリースの想定実行フロー

  • 新バージョンデプロイ => 数%のトラフィックを新バージョンへ流す => 一定時間経って問題なければ自動的に新バージョンをStableバージョンを切り替え
  • 新バージョンデプロイ => 数%のトラフィックを新バージョンへ流す => manual approveで新バージョンをStableバージョンへ切り替え

やらないといけないこと

  • DeploymentはRolloutに置き換える
  • カナリアリリース用のServiceを作成する
  • VirtualServiceでカナリアリリースサービスとStableリリース用サービスで比重を0%, 100%に設定しておく
  • ヘルスチェック用のリソース定義(AnalysisTemplate)
  • Rolloutでstable用Service, canary用Service, virtualService, AnalysisTemplateを指定しておく

挙動FAQ

  • Podが存在しないときはどうなる?

    • Deploymentと同様で、UpdatePolicyによらず、新規のReplicaSetおよびPodが作られる
  • どのようにWeightをコントロールしている?

    • Traffic Routingの設定を組み合わせることでリクエストベースのWeigh制御ができるが、何も指定しないときはReplicaSetのPodの割合で調整する
    • IstioのようなTraffic Routing機能を持つコンポーネントと連携すると分散ポリシーを移譲することができる
  • Istioと連携するとどうなる?

    • ルーティングがリクエストの割合ベースで分散できる
    • WeightはArgoRolloutのPodがVirtualServiceの設定を随時書き換えている
  • カナリア用のServiceの向き先はStableと同じでいいの?

    • 良い。Argo Rolloutが随時Serviceのセレクタに条件を付け足している

How to Use

設定例

  • stable = v1, canary = v2のケース
  • AnalysisTemplateを使って、500エラーが一定期間内に3回でたらNGという定義を行っている
    • istioが持つprometheusに対してクエリを投げている
  • この設定では、比重を10%にし10分待ち、以降徐々に10分毎に20%ずつ増やす。50%になったら、そのまま待ち手動によるResumeを待つ。
apiVersion: v1
kind: Service
metadata:
  name: reviews
  labels:
    app: reviews
    service: reviews
spec:
  ports:
    - port: 9080
      name: http
  selector:
    app: reviews
---
apiVersion: v1
kind: Service
metadata:
  name: reviews-canary
  labels:
    app: reviews
    service: reviews
spec:
  ports:
    - port: 9080
      name: http
  selector:
    app: reviews
---
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: reviews
  labels:
    app: reviews
spec:
  replicas: 1
  selector:
    matchLabels:
      app: reviews
  template:
    metadata:
      labels:
        app: reviews
        version: v2
    spec:
      serviceAccountName: bookinfo-reviews
      containers:
        - name: reviews
          image: docker.io/istio/examples-bookinfo-reviews-v2:1.15.0
      volumes:
        - name: wlp-output
          emptyDir: {}
        - name: tmp
          emptyDir: {}
  strategy:
    canary:
      analysis:
        templates:
          - templateName: failure-count
        args:
          - name: service-name
            value: reviews-canary.default.svc.cluster.local
      stableService: reviews
      canaryService: reviews-canary
      trafficRouting:
        istio:
          virtualService:
            name: reviews
            routes:
              - primary
      steps:
        - setWeight: 10
        - pause: { 10m }
        - setWeight: 30
        - pause: { 10m }
        - setWeight: 50
        - pause: { }
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: failure-count
spec:
  args:
    - name: service-name
  metrics:
    - name: failure-count
      interval: 30s
      failureCondition: result >= 3
      failureLimit: 3
      provider:
        prometheus:
          address: "http://prometheus.istio-system.svc.cluster.local:9090"
          query: |
            sum(irate(
              istio_requests_total{reporter="source",destination_service=~"{{args.service-name}}",response_code!~"5.*"}[30s]
            ))

GitOpsとの兼ね合い

  • カナリアリリースが完了し、100%新バージョンに移行したあとに同期されたらどうなる?

    • 結論: 何も変わらない
      • ArgoRolloutがいじった結果とリソースが一緒になる
      • StableとCanaryのServiceの向き先が両方共新Podになっていて、VirtualServiceもStableに100%向ける設定になっている
  • カナリアリリース実行中に同期されたらどうなる?

    • 結論: 一度ルーティングの割合がStable 100%, Canary 0%にリセットされた後、Rolloutによって同期される直前の状態に戻される

カナリアリリース時と通常リリース時でのフローの違い

デバッグ方法

  • RolloutとAnalysisRunリソースを見るとデプロイの挙動が分かる
 kd rollouts.argoproj.io
 kd analysisruns.argoproj.io

Analysisの設定は実運用上どうすれば良い?

  • IstioでPrometheusを有効にしてインストールし、Istioのメトリクスによって判別する

    • --set values.prometheus.enabled=trueをつけてistioctl manifest applyすればprometheusが有効になるので、これをつかってメトリクスを取得することはできる
    • ただ、素直に入れても可用性は担保されていないので、別途冗長化するなどの可用性対応が必要になってくる
  • Stackdriverなどにメトリクスを集約している場合、Goなどでデータを引っ張ってきた上で判定する

  • カナリアリリースした結果失敗してロールバックしたよね、というのはどのように検知すればよいか?

    • Argo Rolloutの機能では通知できないので、自前の検証プログラムを使ってAnalysisRunを動かす場合は、プログラムの中にエラーケースでアラートするように記載しておく
    • 例えばStackdriverならCloud Monitoringを活用して、ロールバック条件と同じ条件でアラートが飛ぶようにポリシー設定しておく

【GCP Anthos】 Fail Overの挙動から最適なマルチクラスタ構成を考える

記事の目的

前回の記事でIngress For Anthosを用いてRegionにまたがったKubernetsのマルチクラスタを構築した。 今回は1段深く掘り下げて、クラスタ内に複数のフロントサービスを持つケースを考える。 このケースでAnthosで構築したマルチクラスタのFail Overの挙動を詳しく検証し、 どんなときに、どんな条件でFail Overするのかを整理する。 また、それによって最適なマルチクラスタ構成を考える。 今回はIstioはマルチコントロールプレーン構成にしないので、クラスタ内のサービス間通信のFail Overについてはとりあげない。 【GCP Anthos】 Regionに跨って冗長化したKubernetsのマルチクラスタをロードバランシングする

題材となるサービス

今回は少し実運用を考慮し、クラスタ内に2つのフロントサービス(外部から直接ルーティングされてくるサービス)を作り、それぞれへはIngressGatewayでルーティングすることとする。

f:id:taisho6339:20200514120059p:plain

GCLB + NEGsのFailOverの検証

Anthosはクラスタ間のロードバランシングにGCPのCloud Loadbalancer(以下GCLB)とクラスタ内の任意のPodに紐付いたNEGを使う。 よってまずはGCLB + NEGの構成のFailOverの挙動をまずは整理する。

事前準備

  • 東京クラスタ
    • GKE 1.15
    • Anthosへのメンバー登録済み
    • istioインストール済み(v1.5.1)
  • 台湾クラスタ
    • GKE 1.15
    • Anthosへのメンバー登録済み
    • istioインストール済み(v1.5.1)

アプリケーションのデプロイ

ホストヘッダーによってbookinfoとzoneprinterへのルーティングの向き先を変更するようにしている。

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: zoneprinter
spec:
  hosts:
#    - "*"
    - "www.zoneprinter.com"
  gateways:
    - single-gateway
  http:
    - match:
        - uri:
            exact: /
        - uri:
            exact: /ping
        - uri:
            exact: /location
      route:
        - destination:
            host: zoneprinter
            port:
              number: 80
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: bookinfo
spec:
  hosts:
    - "*"
#    - "www.bookinfo.com"
  gateways:
    - single-gateway
  http:
    - match:
        - uri:
            exact: /
        - uri:
            exact: /productpage
        - uri:
            prefix: /static
        - uri:
            exact: /login
        - uri:
            exact: /logout
        - uri:
            prefix: /api/v1/products
      fault:
          abort:
            httpStatus: 503
            percentage:
              value: 100.0
      route:
        - destination:
            host: productpage
            port:
              number: 9080

MCI, MCSリソースのデプロイ

一つのMCSを一つのIngress GatewayのPodに割り当てている。

FailOverの検証方法

MultiClusterServiceが一つなのでIngress GatewayのPodに紐付いた単一のNEGが作られる。 よってこのままだと、ヘルスチェックに使用できるパスが一つのフロントサービスへのルーティングのみになってしまうが、一旦このまま検証する。

  • ヘルスチェック対象のサービス = zoneprinter, ヘルスチェック非対象のサービス = bookinfo
    • ホストヘッダーがwww.bookinfo.comのものはbookinfoへ、それ以外はzoneprinterへルーティング
    • ヘルスチェックは、/ のパスへ
    • ダウンさせるサービスは、IstioのFault Injectionを利用し、エラーが常に返却されるように
    • サンプルコードはこちら

GCLB + NEGのFail Overの挙動結果まとめ

  • ヘルスチェック対象でないサービスがダウン

    • リクエスト時に5xxエラー => Fail Overしない
    • リクエスト時に503(Service Unavailable)エラー => Fail Overしない
    • 一定時間内のほとんどのリクエストが5xxエラー => Fail Overしない
    • 一定時間内のほとんどのリクエストが503エラー => Fail Overしない
  • ヘルスチェック対象のサービスがダウン

    • リクエスト時に5xxエラー => Fail Overしない
    • リクエスト時に503(Service Unavailable)エラー => Fail Overしない
    • 一定時間内のほとんどのリクエストが5xxエラー => Fail Overしない
    • 一定時間内のほとんどのリクエストが503エラー => Fail Overしない
    • ヘルスチェックがエラー => Fail Overする
  • Fail Over条件のカスタマイズは不可能

つまり、ヘルスチェックによってのみFail Overするかどうかが決まる。 よって、今の構成だと、ヘルスチェック対象のサービスがUnhealthyの場合、 引きずられてHealthyなサービスまでも、冗長化された別クラスタにFail Overされてしまう。

理想的な挙動はなにか?

フロントサービスに対して個別にヘルスチェックを実施し、 一つのフロントサービスのヘルスチェックステータスが他のサービスのルーティングに影響しないようにしたい。 つまり、今回ならzoneprinterがUnhealthyだからといって、bookinfoまで別クラスタにルーティングしないでほしい。

どういう構成にするべきか

ようは各フロントサービスごとにヘルスチェックしてほしい => 各サービスごとにNEGが作られて、それらがGCLBのバックエンドとして設定されてほしいということになる、 よってフロントサービスの数だけMultiClusterServiceを作成し、単一のIngressGatewayに紐付けることにした。

f:id:taisho6339:20200514115840p:plain

補足

ちなみに、各フロントサービスに対して直接NEGを紐付ければいいのでは?と思うかもしれないが、 そうなるとIngressGatewayのEnvoyを通過せずに直接リクエスト先サービスのPodにルーティングされてしまうので、 VirtualServiceやDestinationRuleの設定が無視されてしまう。 この辺の挙動のイメージは、こちらにまとめた。 また、IngressGatewayのPodへのNEGを作るので、IngressGatewayにアタッチされているL4のGCLBは不要になる。 よってIstio Operatorなどを使ってNodePortに変更する。

実際のコード

apiVersion: cloud.google.com/v1beta1
kind: BackendConfig
metadata:
  name: multi-gateway-bookinfo-cfg
  namespace: istio-system
spec:
  healthCheck:
    checkIntervalSec: 15
    timeoutSec: 10
    healthyThreshold: 1
    unhealthyThreshold: 2
    type: HTTP
    port: 80
    requestPath: /
    hostHeader: "www.bookinfo.com"
---
apiVersion: networking.gke.io/v1
kind: MultiClusterService
metadata:
  name: multi-gateway-bookinfo
  namespace: istio-system
  annotations:
    beta.cloud.google.com/backend-config:
      '{"ports": {"80":"multi-gateway-bookinfo-cfg"}}'
spec:
  template:
    spec:
      selector:
        app: istio-ingressgateway
      ports:
        - name: multi-gateway-bookinfo
          protocol: TCP
          port: 80
          targetPort: 80
---
apiVersion: cloud.google.com/v1beta1
kind: BackendConfig
metadata:
  name: multi-gateway-zoneprinter-cfg
  namespace: istio-system
spec:
  healthCheck:
    checkIntervalSec: 15
    timeoutSec: 10
    healthyThreshold: 1
    unhealthyThreshold: 2
    type: HTTP
    port: 80
    requestPath: /
    hostHeader: "www.zoneprinter.com"
---
apiVersion: networking.gke.io/v1
kind: MultiClusterService
metadata:
  name: multi-gateway-zoneprinter
  namespace: istio-system
  annotations:
    beta.cloud.google.com/backend-config:
      '{"ports": {"80":"multi-gateway-zoneprinter-cfg"}}'
spec:
  template:
    spec:
      selector:
        app: istio-ingressgateway
      ports:
        - name: multi-gateway-zoneprinter
          protocol: TCP
          port: 80
          targetPort: 80
apiVersion: networking.gke.io/v1
kind: MultiClusterIngress
metadata:
  name: multi-gateway
  namespace: istio-system
spec:
  template:
    spec:
      rules:
        - host: "www.bookinfo.com"
          http:
            paths:
              - path: /
                backend:
                  serviceName: multi-gateway-bookinfo
                  servicePort: 80
        - host: "www.zoneprinter.com"
          http:
            paths:
              - path: /
                backend:
                  serviceName: multi-gateway-zoneprinter
                  servicePort: 80
      backend:
        serviceName: multi-gateway-zoneprinter
        servicePort: 80

確認

この状態でbookinfoのヘルスチェックエンドポイントに対してFaultInjectionし、 zoneprinterにcurlすると、

❯❯❯ curl -H "Host:www.zoneprinter.com" http://35.241.18.9/location
<!DOCTYPE html>
<h4>Welcome from Google Cloud datacenters at:</h4>
<h1>Tokyo, Japan</h1>
<h3>You are now connected to &quot;asia-northeast1-a&quot;</h3>
<img src="https://upload.wikimedia.org/wikipedia/en/9/9e/Flag_of_Japan.svg" style="width: 640px; height: auto; border: 1px solid black"/>

となり、引きずられてFailOverしていないことが分かる。

また、逆にzoneprinterにFaultInjectionし、 bookinfoにアクセスすると、 台湾クラスタの方のログにアクセスが来たことが分かる。

まとめ

  • GCLBはヘルスチェックによってのみFailOverする
  • MCSに対して一つずつNEGが作られるので、複数のNEGを作ることで複数のフロントサービスに対してヘルスチェックを行うことができ、サービスごとにFailOverできる
  • 今回のコード

IstioのTraffic Managementの動作イメージを掴もう

本記事について

Istioを使う上で、Traffic Managementを司るVirtual Service, DestinationRuleについて、 どう作用しているのかという点がわかりにくかったため、本記事にて整理し実際に動かしながら検証する。 前提として、サービスメッシュやEnvoyの基本的な設定についてはざっくりと理解している読者を想定している。

Istioとは?

Istioは、サービスメッシュの機能を統括的に導入、管理を行うためのOSSである。 内部ではEnvoyを使用していて、IstioとしてはとしてはこのEnvoyのコントロールプレーンとしての機能も担っている。 What is Istio?

主に、

  • Traffic Management

    • 割合やヘッダーによるルーティングのコントロールやロードバランシングなどを行うための機能
    • Envoyの機能であるような、サーキットブレーカー、Fault Injectionなどの機能も基本的に使える
  • Observability

    • Promheusなどメトリクスを集計するためのソフトウェアにデータを転送する機能
  • Security

    • mTLS通信による認証された通信や、JWTを活用した認可処理をプロキシとして挟んだりすることができる
    • mTLSに用いられる証明書はIstioが独自証明書を発行してくれ、特に意識せずともmTLSをサービス間通信に導入することができる

といった機能を提供しており、Kubernetsなどのプラットフォーム連携が充実している。

Traffic Managementについて

Istioは、k8sにデプロイされたPodに自動的にサイドカーとしてEnvoyベースのProxyをInjectする機能が備わっている。 基本的には、リクエスト元サービス -> Envoy -> Envoy -> リクエスト先サービスという流れで通信することになる。 そして、このEnvoyが実際にルーティングの制御を行っている。 このEnvoyに設定情報を提供するのがk8sのCRDである下記3種類のリソースである。

  • Virtual Service
  • DestinationRule

各リソースについての概念説明などはなどは、 公式ドキュメント に記載されている通りである。 このさきは、minikubeにIstioと、Istioのデモアプリである、Bookinfo をデプロイして挙動を確認していく。

事前準備

  • minikube v1.9.2
  • Istio v1.5.2
  • Kubernets v1.18.0

Ingress Gatewayの実態を見てみよう

初期状態

Ingress Gatewayを有効にしてIstioをk8sクラスタにインストールすると、

  • Service(Load Balancer Type)
  • Deployment

のリソースがそれぞれ作られていることが分かる。

❯❯❯ kubectl get service -n istio-system
istio-ingressgateway        LoadBalancer   10.103.255.236   10.103.255.236   15020:31097/TCP,80:31587/TCP,443:32153/TCP,15029:30056/TCP,15030:32475/TCP,15031:32379/TCP,15032:32322/TCP,31400:32143/TCP,15443:32376/TCP   3d15h
❯❯❯ kubectl get deployments.apps -n istio-system
NAME                   READY   UP-TO-DATE   AVAILABLE   AGE
istio-ingressgateway   1/1     1            1           3d15h

そして、このDeploymentによって管理されているPodの定義を見てみると、

❯❯❯ kubectl get pod -n istio-system istio-ingressgateway-6489d9556d-sp8f2 -o yaml | grep image:
    image: docker.io/istio/proxyv2:1.5.2

のようになっているのが分かる。 このproxyvというのが中でEnvoyが動いてる。 この状態でRoutingの設定を覗いてみると、まだなにも定義していないためすべて404になるようになっている。

❯❯❯ istioctl -n istio-system proxy-config route istio-ingressgateway-6489d9556d-sp8f2 -o json | yq -y '.' -
- name: http.80
  virtualHosts:
    - name: blackhole:80
      domains:
        - '*'
      routes:
        - name: default
          match:
            prefix: /
          directResponse:
            status: 404
  validateClusters: false

GatewayリソースとGatewayリソースに紐付けたVirtualServiceの作成

では次に下記リソースをapplyし、IngressGatewayにAttachしたGatewayリソースと、VirtualServiceを作ってみる。

❯❯❯ kubectl apply -f samples/bookinfo/networking/bookinfo-gateway.yaml

そうすると、下記のようなルーティングルールが作成されている。

❯❯❯ istioctl -n istio-system proxy-config route istio-ingressgateway-6489d9556d-sp8f2 -o json | yq -y '.' -
- name: http.80
  virtualHosts:
    - name: '*:80'
      domains:
        - '*'
        - '*:80'
      routes:
        ...
        - match:
            path: /productpage
            caseSensitive: true
          route:
            cluster: outbound|9080||productpage.default.svc.cluster.local
            timeout: 0s
            retryPolicy:
              retryOn: connect-failure,refused-stream,unavailable,cancelled,resource-exhausted,retriable-status-codes
              numRetries: 2
              retryHostPredicate:
                - name: envoy.retry_host_predicates.previous_hosts
              hostSelectionRetryMaxAttempts: '5'
              retriableStatusCodes:
                - 503
            maxGrpcTimeout: 0s
          metadata:
            filterMetadata:
              istio:
                config: /apis/networking.istio.io/v1alpha3/namespaces/default/virtual-service/bookinfo
          decorator:
            operation: productpage.default.svc.cluster.local:9080/productpage
          ...

このように、IngressGatewayにAttachしたGatewayを作成し、さらにそのGateway指定したVirtual Serviceを作ると、 L4/LBから直接ルーティングされてくるEnvoyにルーティングルールが追加される。 また、向き先としてはPodのIPが解決されている。

❯❯❯ istioctl -n istio-system proxy-config endpoint istio-ingressgateway-6489d9556d-sp8f2 -o json | yq -y '.' - | grep "outbound|9080||productpage.default.svc.cluster.local" -A 30
- name: outbound|9080||productpage.default.svc.cluster.local
  addedViaApi: true
  hostStatuses:
    - address:
        socketAddress:
          address: 172.17.0.12
          portValue: 9080
      weight: 1

Gatewayに紐付けないVirtualService

次に下記リソースをapplyし、reviewsサービスにはv2に50%、v3に50%の割合でルーティングされるようにしてみる。

❯❯❯ kubectl apply -f samples/bookinfo/networking/destination-rule-reviews.yaml
❯❯❯ kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-v2-v3.yaml

まずIngress Gateway側のEnvoyの設定を確認する。

❯❯❯ istioctl -n istio-system proxy-config route istio-ingressgateway-6489d9556d-sp8f2 -o json | yq -y '.' -

今回はGatewayにAttachしていないVirtualServiceを作成したので、特にルーティング設定に変化がないことが分かる。

ではアプリケーション側のPodに配置されたEnvoyの設定はどうだろうか?

❯❯❯ istioctl proxy-config route productpage-v1-7f44c4d57c-nfnjd -o json | yq -y '.' -
- name: reviews.default.svc.cluster.local:80
      domains:
        - reviews.default.svc.cluster.local
        - reviews.default.svc.cluster.local:80
      routes:
        - match:
            prefix: /
          route:
            weightedClusters:
              clusters:
                - name: outbound|80|v2|reviews.default.svc.cluster.local
                  weight: 50
                - name: outbound|80|v3|reviews.default.svc.cluster.local
                  weight: 50
            timeout: 0s
            retryPolicy:
              retryOn: connect-failure,refused-stream,unavailable,cancelled,resource-exhausted,retriable-status-codes
              numRetries: 2
              retryHostPredicate:
                - name: envoy.retry_host_predicates.previous_hosts
              hostSelectionRetryMaxAttempts: '5'
              retriableStatusCodes:
                - 503
            maxGrpcTimeout: 0s
          metadata:
            filterMetadata:
              istio:
                config: /apis/networking.istio.io/v1alpha3/namespaces/default/virtual-service/reviews
          decorator:
            operation: reviews:80/*

長いので省略しているが、reviewsへのトラフィックのときに下記2つの向き先へ50%の割合でルーティングしているのが分かる。

  • outbound|80|v2|reviews.default.svc.cluster.local
  • outbound|80|v3|reviews.default.svc.cluster.local

endpointの設定を見てみると、それぞれv3のPodのIP、v2のPodのIPを指していることが読み取れる。

❯❯❯ istioctl proxy-config endpoint productpage-v1-7f44c4d57c-nfnjd -o json | yq -y '.' - | grep reviews -A 30
- name: outbound|9080|v3|reviews.default.svc.cluster.local
  addedViaApi: true
  hostStatuses:
    - address:
        socketAddress:
          address: 172.17.0.10
          portValue: 9080
- name: outbound|9080|v2|reviews.default.svc.cluster.local
  addedViaApi: true
  hostStatuses:
    - address:
        socketAddress:
          address: 172.17.0.11
          portValue: 9080

ちなみにこのルーティング設定は、Bookinfoアプリケーション内のそれぞれのサービスのサイドカーEnvoyの設定に組み込まれている。 (正確にはIstio Pilotの仕組みを使って設定が動的にInjectされている) つまり、リクエストを送る側、つまりクライアント側のプロキシ(Envoy)でロードバランシング、ルーティング制御を行い、Podにトラフィックを送っているのである。

DestinationRuleを削除してみる

最後に上記までの状態からDestinationRuleだけ削除してみる。

❯❯❯ kubectl apply -f samples/bookinfo/networking/destination-rule-reviews.yaml

ルーティングルールとしては変化していないように見える。

❯❯❯ istioctl proxy-config route productpage-v1-7f44c4d57c-nfnjd -o json | yq -y '.' -
clusters:
                - name: outbound|80|v2|reviews.default.svc.cluster.local
                  weight: 50
                - name: outbound|80|v3|reviews.default.svc.cluster.local
                  weight: 50

だが、エンドポイント定義としてはv2, v3が消えてしまっているのが分かる。

❯❯❯ istioctl proxy-config endpoint productpage-v1-7f44c4d57c-nfnjd -o json | yq -y '.' - | grep reviews

- name: outbound|9080||reviews.default.svc.cluster.local

まとめ

  • VirtualService, DestinationRuleはすべてIngress GatewayのEnvoyもしくは、サイドカーEnvoyの設定に作用する
    • Gatewayに紐付けたVirtualServiceは、Ingress GatewayのEnvoyのルーティング設定に作用する
    • Gatewayに紐づけていないVirtualServiceは各サービスのサイドカーEnvoyに、アウトバウンドのルーティング設定として作用する
      • これによってクライアント側でロードバランシング、ルーティングの制御を行っている
    • DestinationRuleはEnvoyのEndpoint設定に作用する