パッチを差し込み、istioctl と Envoy の判定を見分ける
目標
EnvoyFilterでfaultフィルターをrouterの前に差し込み、そのパッチがEnvoy設定のどこになるかを、手で再現します。routerの後ろに差し込んだものと、値が間違っているものを作って、istioctlとEnvoyの判定が分かれるのを見て、バージョンの絞り込みと適用範囲を確認します。
なぜ重要なのか
EnvoyFilterは、メッシュで最も力が強く、最も静かに壊れやすいリソースです。istioctlが通しても、Envoyが拒否すれば、本番では「設定が変わらない」としか見えません。パッチが届く場所と、検査の限界がわかれば、マージする前に見つけて弾けます。
ステップ
/root/ist2-efを作り、/root/ist2-ef/ef.yamlにEnvoyFilterを書いてください。apiVersion: networking.istio.io/v1alpha3、名前はreviews-fault、ネームスペースはdefault、workloadSelectorはapp: reviewsです。パッチは1つで、applyTo: HTTP_FILTER、match.context: SIDECAR_INBOUND、match.listener.filterChain.filter.nameはenvoy.filters.network.http_connection_manager、そのsubFilter.nameはenvoy.filters.http.router、patch.operation: INSERT_BEFOREです。差し込む値は、名前envoy.filters.http.faultに、typed_config(@typeはtype.googleapis.com/envoy.extensions.filters.http.fault.v3.HTTPFault)で、abort.http_status: 418、abort.percentageはnumerator: 100・denominator: HUNDREDです。istioctl validate -f /root/ist2-ef/ef.yamlの出力(標準エラー出力を含む)と終了コードを、/root/ist2-ef/01-validate.txtに入れてください(最後の行はrc=)。/root/ist2-ef/ef.yamlの最初のパッチから、5つの値をyqで取り出し、/root/ist2-ef/02-fields.txtに書いてください。applyTo=、context=(match.context)、operation=(patch.operation)、anchor=(matchが基準にするHTTPフィルター、つまりsubFilter.name)、filter=(差し込む値のname)です。/root/ist2-ef/envoy-before.yamlにEnvoyの設定を書いてください。管理ポートは9989、リスナーvirtualInboundが127.0.0.1:10089で待ち受け、HTTP接続マネージャーのstat_prefixはinbound_0.0.0.0_9080で、すべてのパスをクラスターinbound|9080||(127.0.0.1:8112)へ送ります。http_filtersは、ef.yamlのパッチの値とまったく同じfaultフィルターの項目、その次にrouterの順序です。アップストリームを8112でokとして起動し、Envoyを起動してから、curl localhost:10089/reviewsを3回送り、/root/ist2-ef/03-before.txtに3行を書いてください。codes=(3つのレスポンスコードをカンマ区切りで)、stat_name=(faultが中止させたリクエストの数を数える統計の完全な名前)、aborts_injected=(その統計の値)です。/root/ist2-ef/ef.yamlを/root/ist2-ef/ef-after.yamlにコピーして、patch.operationだけをINSERT_AFTERに変えてください。そして、/root/ist2-ef/envoy-before.yamlを/root/ist2-ef/envoy-after.yamlにコピーして、http_filtersの順序だけを、routerが先でfaultが後になるように入れ替えてください(そのパッチが適用された結果です)。2つのファイルを、それぞれistioctl validate -fとenvoy --mode validate -cで検査して、/root/ist2-ef/04-after.txtに書いてください。1行目はistioctl_rc=、2行目はenvoy_rc=、その下に、Envoyが拒否した理由を含む出力の行を、そのまま書きます。/root/ist2-ef/ef.yamlを/root/ist2-ef/ef-badfield.yamlに、/root/ist2-ef/envoy-before.yamlを/root/ist2-ef/envoy-badfield.yamlにコピーしてから、2つのファイルのfaultabort.percentageを、VirtualServiceで使っていた形{ value: 100 }に変えてください(残りはそのままです)。istioctl validate -fとenvoy --mode validate -cでそれぞれ検査して、/root/ist2-ef/05-gap.txtに書いてください。1行目はistioctl_rc=、2行目はenvoy_rc=、その下に、istioctlがこのフィールドを問題にした警告の行と、Envoyが拒否した理由の行を、そのまま書きます。/root/ist2-ef/ef.yamlを/root/ist2-ef/ef-pinned.yamlにコピーして、最初のパッチのmatch.proxy.proxyVersionに、正規表現^1\.24.*を加えてください(残りはそのままです)。istioctl validateで通ることを確認し、このラボのプロキシのバージョンを調べるために、/root/ist2-efでistioctl kube-injectを使って/opt/lab/fixtures/istio/inject-target.yamlに注入し、/root/ist2-ef/inject.yamlとして保存してください(注入設定の3つのファイル/opt/istio/inject-config.yaml・mesh-config.yaml・values-config.yamlをすべて渡します)。/root/ist2-ef/06-version.txtに4行を書いてください。regex=(ef-pinned.yamlに書いた正規表現そのまま)、proxy_version=(istio-proxyイメージのタグ)、matches_proxy=(そのバージョンが正規表現に合えばyes、合わなければno)、matches_1_25_0=(仮想のバージョン1.25.0が合えばyes、合わなければno)です。/root/ist2-ef/ef-ratelimit.yamlにEnvoyFilterを書いてください。名前はinbound-ratelimit、ネームスペースはdefault、workloadSelectorなしで、パッチの形はef.yamlと同じ(HTTP_FILTER・SIDECAR_INBOUND・routerの前へのINSERT_BEFORE)で、値は、名前envoy.filters.http.local_ratelimit、@typeはtype.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit、stat_prefix: http_local_rate_limiter、token_bucketはmax_tokens: 1・tokens_per_fill: 1・fill_interval: 300s、filter_enabledとfilter_enforcedは、どちらもdefault_valueがnumerator: 100・denominator: HUNDREDです。istioctl validateで値の警告がないことを確認した後、その値をrouterの前に置いたEnvoy設定を/root/ist2-ef/envoy-rl.yamlに書いてください(管理ポート・リスナー・クラスターはステップ3と同じです)。起動してcurl localhost:10089/reviewsを3回送った後、/root/ist2-ef/07-ratelimit.txtに4行を書いてください。codes=(3つのレスポンスコードをカンマ区切りで)、rate_limited=(統計http_local_rate_limit.rate_limitedの値)、scope=(このEnvoyFilterが届く範囲。workload・namespace・meshのどれか1つ)、mesh_wide_namespace=(同じファイルを移しておくとメッシュ全体に届くネームスペース。/opt/istio/mesh-config.yamlから)です。/root/ist2-ef/08-report.mdに、before_status=、after_envoy_rc=、badfield_istioctl_rc=、ratelimit_codes=の4行を書き(それぞれ、ステップ3でfaultが返したステータスコード、ステップ4でのEnvoyの終了コード、ステップ5でのistioctlの終了コード、ステップ7の3つのレスポンスコードです)、その下に、-で始まる説明を4行以上書いてください。
参考
- このPodには、本物のistiodも本物のサイドカーもありません。そのため、
istioctl proxy-configで実際の生成物を見られず、変換ルールを知って、手で同等のEnvoy設定を作って動作を確認します。同じルールが、本番クラスターのproxy-configの出力に、そのまま見えます。 istioctl validateは、正しいEnvoyFilterにも、「exposes internal implementation details」という警告を出します。この警告は正常です。値の中のフィールドを問題にする警告が別に出るかどうかを見てください。警告は標準エラー出力にも出るので、ファイルに入れるときは2>&1を付けます。- Envoyのエラー文は、単語の間の空白が、わざと揺れています(
no such field)。目で読むときに驚かないでください。 - ステップごとに、設定ファイルの名前が違います。前のステップのファイルはコピーして使い、直さないでください。前のステップの採点が、そのファイルを見ます。
- Envoyを起動するときは、
setsid --fork nohup envoy -c <파일> --log-level warn > <로그> 2>&1 </dev/null(プレースホルダーは設定ファイルとログファイルです)でシェルから完全に切り離してください。起動し直す前にはpkill -x envoyで片付けます(pkill -f 'envoy -c'は、その文字列を含むシェル自身まで終了させます)。 - アップストリームの代用サーバーがイメージにあります:
python3 /opt/lab/envoy/upstream.py <포트> ok|fail|slow(プレースホルダーはポート番号です)。応答本文は<모드>:<포트> <경로>(プレースホルダーはモード、ポート番号、パスです)です。 - 設定を直した後は、起動する前に
envoy --mode validate -c <파일>(プレースホルダーは設定ファイルです)で先に検査してください。クラスター名に|が入るので、YAMLでは必ず引用符で囲みます。
routerの前にfaultを差し込むEnvoyFilterを書く
/root/ist2-efを作り、/root/ist2-ef/ef.yamlにEnvoyFilterを書いてください。apiVersion: networking.istio.io/v1alpha3、名前はreviews-fault、ネームスペースはdefault、workloadSelectorはapp: reviewsです。パッチは1つで、applyTo: HTTP_FILTER、match.context: SIDECAR_INBOUND、match.listener.filterChain.filter.nameはenvoy.filters.network.http_connection_manager、そのsubFilter.nameはenvoy.filters.http.router、patch.operation: INSERT_BEFOREです。差し込む値は、名前envoy.filters.http.faultに、typed_config(@typeはtype.googleapis.com/envoy.extensions.filters.http.fault.v3.HTTPFault)で、abort.http_status: 418、abort.percentageはnumerator: 100・denominator: HUNDREDです。istioctl validate -f /root/ist2-ef/ef.yamlの出力(標準エラー出力を含む)と終了コードを、/root/ist2-ef/01-validate.txtに入れてください(最後の行はrc=)。
EnvoyFilterのパッチ1つは、「どこに」(applyTo・match)と「何を」(operation・value)の組です。HTTP_FILTERにsubFilterで基準のフィルターを選ぶと、そのフィルターが入っているHTTP接続マネージャーのフィルター一覧が舞台になります。valueはIstioの文法ではなくEnvoy設定そのままなので、パーセントも、EnvoyのFractionalPercent(分子・分母)で書く必要があります。正しいファイルでも、istioctlはEnvoyFilter自体についての警告を1行出します。その警告は正常で、値の中のフィールドを問題にする警告がさらに出たら、直す必要があります。
パッチの「どこに」と「何を」を4つの項目で読む
/root/ist2-ef/ef.yamlの最初のパッチから、5つの値をyqで取り出し、/root/ist2-ef/02-fields.txtに書いてください。applyTo=、context=(match.context)、operation=(patch.operation)、anchor=(matchが基準にするHTTPフィルター、つまりsubFilter.name)、filter=(差し込む値のname)です。
applyToは、パッチが届くEnvoyオブジェクトの種類(リスナー・フィルターチェーン・ネットワークフィルター・HTTPフィルター・クラスター・ルート…)、contextは、どの方向のプロキシか(サイドカーの受信側・送信側、ゲートウェイ)、matchは、その種類の中でどれか1つを選ぶ条件です。INSERT_BEFOREのような相対位置の操作は、この基準フィルターがあって初めて意味があります。filter.nameとsubFilter.nameを取り違えないでください。前者はネットワークフィルター、後者がHTTPフィルターです。
パッチが適用された姿をEnvoyで立ち上げて、418を受け取る
/root/ist2-ef/envoy-before.yamlにEnvoyの設定を書いてください。管理ポートは9989、リスナーvirtualInboundが127.0.0.1:10089で待ち受け、HTTP接続マネージャーのstat_prefixはinbound_0.0.0.0_9080で、すべてのパスをクラスターinbound|9080||(127.0.0.1:8112)へ送ります。http_filtersは、ef.yamlのパッチの値とまったく同じfaultフィルターの項目、その次にrouterの順序です。アップストリームを8112でokとして起動し、Envoyを起動してから、curl localhost:10089/reviewsを3回送り、/root/ist2-ef/03-before.txtに3行を書いてください。codes=(3つのレスポンスコードをカンマ区切りで)、stat_name=(faultが中止させたリクエストの数を数える統計の完全な名前)、aborts_injected=(その統計の値)です。
istiodがEnvoyFilterを適用すると、このサイドカーの受信側リスナーのHTTPフィルター一覧が、[..., fault, router]になります。このPodにはistiodがないので、その結果を手で作るわけです。パッチのvalueがそのまま一覧の項目1つになる点が核心です。書き写すときに、値を変えないでください。HTTPフィルターの統計は、http.<stat_prefix>.の下に溜まります。管理ポートの/stats?filter=で絞ってみてください。
routerの後ろに差し込むと、istioctlは通すが、Envoyは拒否する
/root/ist2-ef/ef.yamlを/root/ist2-ef/ef-after.yamlにコピーして、patch.operationだけをINSERT_AFTERに変えてください。そして、/root/ist2-ef/envoy-before.yamlを/root/ist2-ef/envoy-after.yamlにコピーして、http_filtersの順序だけを、routerが先でfaultが後になるように入れ替えてください(そのパッチが適用された結果です)。2つのファイルを、それぞれistioctl validate -fとenvoy --mode validate -cで検査して、/root/ist2-ef/04-after.txtに書いてください。1行目はistioctl_rc=、2行目はenvoy_rc=、その下に、Envoyが拒否した理由を含む出力の行を、そのまま書きます。
routerは、リクエストをアップストリームへ送って終わらせるフィルターなので、Envoyはその後ろにフィルターが来ることを、設定の段階で拒否します。istioctlは、EnvoyFilterの外側の構造しか知らず、Envoyのフィルター順序のルールは知らないので、このパッチを通します。本番では、istiodがそのまま送り込み、Envoyがそのリスナーの更新を拒否(NACK)します。Podは普通に見えるのに、設定が変わらない姿です。終了コードは、コマンドの直後の$?で受け取ってください。
VirtualService式のパーセントを書き写すと、警告しか出ない
/root/ist2-ef/ef.yamlを/root/ist2-ef/ef-badfield.yamlに、/root/ist2-ef/envoy-before.yamlを/root/ist2-ef/envoy-badfield.yamlにコピーしてから、2つのファイルのfaultabort.percentageを、VirtualServiceで使っていた形{ value: 100 }に変えてください(残りはそのままです)。istioctl validate -fとenvoy --mode validate -cでそれぞれ検査して、/root/ist2-ef/05-gap.txtに書いてください。1行目はistioctl_rc=、2行目はenvoy_rc=、その下に、istioctlがこのフィールドを問題にした警告の行と、Envoyが拒否した理由の行を、そのまま書きます。
VirtualServiceのfault.abort.percentage.valueはIstioのパーセント型で、Envoyのfaultフィルターは、分子・分母でできたFractionalPercentを使います。人がよくやる書き写しのミスです。istioctlは、値をEnvoyの型として読み解いてみますが、知らないフィールドは警告で知らせるだけで、終了コードは0です。CIが終了コードだけを見ていれば、そのままデプロイされます。一方、@typeの名前自体が間違っていれば、istioctlもエラーで止めます。どちらを信じるかの基準が、ここで分かれます。yqで1つのフィールドだけを変えれば、残りには触れません。
proxyVersionでパッチを1つのバージョンに絞る
/root/ist2-ef/ef.yamlを/root/ist2-ef/ef-pinned.yamlにコピーして、最初のパッチのmatch.proxy.proxyVersionに、正規表現^1\.24.*を加えてください(残りはそのままです)。istioctl validateで通ることを確認し、このラボのプロキシのバージョンを調べるために、/root/ist2-efでistioctl kube-injectを使って/opt/lab/fixtures/istio/inject-target.yamlに注入し、/root/ist2-ef/inject.yamlとして保存してください(注入設定の3つのファイル/opt/istio/inject-config.yaml・mesh-config.yaml・values-config.yamlをすべて渡します)。/root/ist2-ef/06-version.txtに4行を書いてください。regex=(ef-pinned.yamlに書いた正規表現そのまま)、proxy_version=(istio-proxyイメージのタグ)、matches_proxy=(そのバージョンが正規表現に合えばyes、合わなければno)、matches_1_25_0=(仮想のバージョン1.25.0が合えばyes、合わなければno)です。
istiodは、プロキシが接続するときに知らせてきた自分のバージョン(ISTIO_VERSIONメタデータ)を、この正規表現に当てて、合うときだけパッチを付けます。アップグレードでEnvoyのフィルター名や設定の形が変わると、古いパッチが新しいプロキシを壊すことがありますが、バージョンを絞っておけば、新しいプロキシにはパッチがそもそも付きません。ドットは正規表現では任意の1文字なので、\.で止め、先頭を^で固定してください。判別はgrep -Eで試せます。注入の出力の末尾に空のドキュメントがあるので、yqはselect(.kind=="Deployment")で絞ってください。
セレクターのないEnvoyFilterで、ネームスペース全体にリクエスト制限をかける
/root/ist2-ef/ef-ratelimit.yamlにEnvoyFilterを書いてください。名前はinbound-ratelimit、ネームスペースはdefault、workloadSelectorなしで、パッチの形はef.yamlと同じ(HTTP_FILTER・SIDECAR_INBOUND・routerの前へのINSERT_BEFORE)で、値は、名前envoy.filters.http.local_ratelimit、@typeはtype.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit、stat_prefix: http_local_rate_limiter、token_bucketはmax_tokens: 1・tokens_per_fill: 1・fill_interval: 300s、filter_enabledとfilter_enforcedは、どちらもdefault_valueがnumerator: 100・denominator: HUNDREDです。istioctl validateで値の警告がないことを確認した後、その値をrouterの前に置いたEnvoy設定を/root/ist2-ef/envoy-rl.yamlに書いてください(管理ポート・リスナー・クラスターはステップ3と同じです)。起動してcurl localhost:10089/reviewsを3回送った後、/root/ist2-ef/07-ratelimit.txtに4行を書いてください。codes=(3つのレスポンスコードをカンマ区切りで)、rate_limited=(統計http_local_rate_limit.rate_limitedの値)、scope=(このEnvoyFilterが届く範囲。workload・namespace・meshのどれか1つ)、mesh_wide_namespace=(同じファイルを移しておくとメッシュ全体に届くネームスペース。/opt/istio/mesh-config.yamlから)です。
workloadSelectorがあればラベルが合うワークロードだけに、なければそのネームスペースのすべてのワークロードに、そしてメッシュ設定のルートネームスペースに置けばメッシュ全体に付きます。ルートネームスペースのものが先に、ワークロードのネームスペースのものが後に適用されます。リクエスト制限の値の中にruntime_keyを入れないでください。このバージョンのEnvoyが拒否します。トークンが1つで、補充の間隔が長いと、最初のリクエストだけが通ります。制限されたレスポンスのコードが何なのかは、自分で見てください。
EnvoyFilterを書く前のチェックリストとしてまとめる
/root/ist2-ef/08-report.mdに、before_status=、after_envoy_rc=、badfield_istioctl_rc=、ratelimit_codes=の4行を書き(それぞれ、ステップ3でfaultが返したステータスコード、ステップ4でのEnvoyの終了コード、ステップ5でのistioctlの終了コード、ステップ7の3つのレスポンスコードです)、その下に、- で始まる説明を4行以上書いてください。
値は、前のステップのファイルから移してください。説明の行には、「EnvoyFilterをマージする前に何を確認するか」を書くと、この表がレビューのチェックリストになります。パッチが届く場所、routerの位置、istioctlが見つけられないもの、バージョンの絞り込み、範囲です。