- Published on
ambient に移行すると EnvoyFilter は静かに無視される — Istio 1.30 の TrafficExtension が埋めた穴、埋まらなかった穴
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- はじめに — サイドカーを外したときについてこないもの
- EnvoyFilter は waypoint で静かに無視される
- Istio 1.30 が出した答え — TrafficExtension
- waypoint では selector も静かに無視される
- Lua vs Wasm — 公式ドキュメントのメモリ表をそのまま信じてはいけない理由
- Lua には fail-open しかない — プロト定義が証明する
- 既存の WasmPlugin はどうなるのか
- では ambient はプロダクション対応済みなのか — 成熟度表をそのまま読む
- いつ移行を見送るべきか
- おわりに
- 参考資料
はじめに — サイドカーを外したときについてこないもの
ambient モードの売り文句は、いつもリソースの話です。ポッドごとに付いていたEnvoyサイドカーを外し、ノードごとに1つ動くztunnelと、必要な場所にだけ置くwaypointに分ける — この構造自体とサイドカーとの比較は、Istio Ambient vs Sidecar 比較ですでに扱っているので、ここでは繰り返しません。
ここで扱いたいのは、そうした記事があまり触れない側です。実際に移行を試みたチームがつまずく点は、たいていCPUやメモリではなく拡張性です。サイドカー時代なら誰もが1つは持っていたあのEnvoyFilter — ヘッダーを1つ余分に付けたり、癖のある認証を差し込んだり、あまり知られていないEnvoyのフィルタを有効にしていたりする、あのリソース — は、ambientに移行してもついてきません。
そしてこれは私の推測ではなく、Istio公式ドキュメントが直接書いていることです。
EnvoyFilter は waypoint で静かに無視される
Istio 1.30で新しく入ったサイドカーからambientへのポリシー移行ドキュメントは、こう述べています。
EnvoyFilterresources are not supported on waypoints. If you haveEnvoyFilterresources that configure sidecar proxy behavior, they will be silently ignored after migration and must be handled before proceeding.
核心は「silently ignored」— 静かに無視される、という部分です。エラーは出ませんし、ポッドが落ちることもありません。ただ、あなたが仕掛けたフィルタが動かなくなるだけです。それが認証バイパス防止のフィルタだった場合にどうなるかは、あえて説明しません。
同じドキュメントは対処法を3つ挙げていて、最後の1つが正直です。
- フィルタがEnvoyのカスタム機能を足しているものなら、
WasmPluginで同じ動作を出せないか検討する。 - もう不要なら削除する。
- ambient互換の代替がなければ、これはマイグレーションのブロッカーだ。 依存関係が解消されるまで先に進むな。
CNCFの発表資料にはあまり出てこない一文です。「ambientはGAだ」と「あなたのEnvoyFilterはマイグレーションのブロッカーだ」が、同時に真でありうるということですから。
なぜこうなったのかは、EnvoyFilterの設計を見れば納得できます。EnvoyFilter APIの定義のコメントは、自分自身をこう紹介しています — この機能は「must be used with care, as incorrect configurations could potentially destabilize the entire mesh」であり、「some aspects of this API are deeply tied to the internal implementation in Istio's networking subsystem as well as Envoy's XDS API」だと。istiodが生成したEnvoy設定を直接いじるAPIなので、データプレーンがEnvoyでない場所(ztunnelはRustで書かれた別のプロキシです)や、生成経路がまったく違う場所(waypoint)にそのまま移植できるはずがありません。EnvoyFilterは最初から移植可能な抽象ではなく、特定の実装に対するパッチでした。
Istio 1.30 が出した答え — TrafficExtension
Istio 1.30.0は2026年5月18日に出て、この穴を狙ったAPIが一緒に入りました。DocusignのLiam Whiteが書いたTrafficExtension API紹介記事が背景を説明しています。
それまで、Istioが公式サポートする拡張APIはWasmPluginひとつだけでした。Luaを使いたければEnvoyFilterを経由するしかなく — 見てきた通り、それはwaypointで無視されます。つまりambientではLuaはそもそも選択肢になかったのです。
TrafficExtensionはWasmとLuaを1つのAPIにまとめ、サイドカー・ゲートウェイ・waypointのすべてに同じ文法で付けられます。実際のプロト定義を見ると、構造がはっきりします。
message TrafficExtension {
enum ExecutionPhase {
UNSPECIFIED = 0;
AUTHN = 1;
AUTHZ = 2;
STATS = 3;
}
istio.type.v1beta1.WorkloadSelector selector = 1;
repeated istio.type.v1beta1.PolicyTargetReference targetRefs = 2;
ExecutionPhase phase = 3;
google.protobuf.Int32Value priority = 4;
repeated TrafficSelector match = 5;
oneof filter_config {
WasmConfig wasm = 6;
LuaConfig lua = 7;
}
}
oneofである点が重要です — 1つのリソースにはWasmかLuaのどちらか一方しか入りません。
順序制御はphaseとpriorityが担います。phaseはフィルタチェーンの既知の位置(AUTHN/AUTHZ/STATS、未設定ならルーター付近)に差し込み、同じphase内ではpriorityが降順 — 値が大きいほど先に — 実行されます。プロトのコメントに書かれたタイブレーカーは特に実務で役立ちます。priorityが未設定または同値なら、順序はcreationTimestamp、続いて名前とネームスペースで決定論的に決まります。つまり「指定しなければランダム」ではなく「指定しなければ作成順」ということで、これは再適用すると順序が変わりうるという意味でもあります。
waypoint では selector も静かに無視される
ここで2つ目の罠が出てきます。しかも1つ目とまったく同じ痛み方をします。
TrafficExtensionが対象を選ぶ方法は2つあります。サイドカーはラベルベースのselectorで、waypointはtargetRefsで捕まえます。waypointはワークロードにラベルで対応する類のものではないからです。プロトのコメントはこの点を大文字で釘を刺しています。
NOTE: Waypoint proxies are required to use this field for policies to apply;
selectorpolicies will be ignored.
また「ignored」です。サイドカーでは問題なく動いていたselectorベースの拡張をそのままwaypointに持っていくと、エラーもなくただ適用されなくなります。このパターンはambient移行全体で繰り返されます — 同じ移行ドキュメントは、L7ルールを使う、あるいはaction: CUSTOM/AUDITを使うAuthorizationPolicy、それにRequestAuthenticationとWasmPluginも、すべてtargetRefsに切り替える必要があると書いています。
targetRefsが受け付ける種類もプロトに明記されています — 同じネームスペースのGateway(グループgateway.networking.k8s.io)、ルートネームスペースのGatewayClass、同じネームスペースのService(waypoint専用)、同じネームスペースのServiceEntryです。最大16個までで、selectorとtargetRefsは同時に使えません。
ambientでwaypointにWasmを付ける最小構成はこうなります。
apiVersion: extensions.istio.io/v1alpha1
kind: TrafficExtension
metadata:
name: basic-auth-gateway
spec:
targetRefs:
- kind: Gateway
group: gateway.networking.k8s.io
name: bookinfo-gateway
phase: AUTHN
wasm:
url: oci://ghcr.io/istio-ecosystem/wasm-extensions/basic_auth:1.12.0
pluginConfig:
basic_auth_rules:
- prefix: "/productpage"
request_methods: ["GET", "POST"]
credentials: ["ok:test"]
Lua vs Wasm — 公式ドキュメントのメモリ表をそのまま信じてはいけない理由
Luaが第一級市民になった今、当然「じゃあどっちを使うのか」という話が続きます。Istioの拡張性コンセプトドキュメントは、この問いにメモリ表で答えています。Luaは並行度によらずおよそ20〜26 MiBを使い、Wasmは低い並行度で約110 MiB、高い並行度では約290 MiBまで達するというものです。
並行度(--concurrency) | Lua (MiB) | Wasm (MiB) |
|---|---|---|
| 1 | 19.79 | 117.7 |
| 2 | 23.07 | 132.5 |
| 4 | 22.63 | 152.0 |
| 8 | 23.97 | 190.9 |
| 16 | 25.66 | 291.8 |
数字だけ見れば結論は明白に見えます。ところが、ドキュメントがリンクしている元のベンチマークリポジトリを開くと測定条件が出てきて、その条件が結論をかなり揺さぶります。ちなみにこのベンチマークの著者は、TrafficExtension API紹介記事を書いた人物と同一人物です — つまり著者自身による測定ということです。
リポジトリのREADMEとdocker-compose.ymlが明かす条件はこうです。
- Wasmモジュールは Go でコンパイルされています。 Go 1.24.4をWASIターゲット、
c-sharedビルドモードでビルドしています。これが決定的です。Goで作ったWasmは、Goランタイムとガベージコレクタをまるごとサンドボックスごとに抱え込みます。同じロジックをRustやC++で書けば数字は大きく変わります。ドキュメントの表は「Wasm」とだけ書いていますが、実際に測っているのは「Goでコンパイルしたwasm」です。 - 負荷は5 RPSです。 ロードジェネレータは
curlループにsleep 0.2を挟んだコンテナで、READMEも5 RPSと明記しています。つまりこの表は、トラフィックを受けている最中のメモリではなく実質的にアイドル状態の常駐メモリです。 - 測定ツールは
docker statsです。 Envoy内部のメモリ統計ではなく、コンテナ全体のRSSです。コンテナのメモリ上限は15.6 GiBで、圧迫はまったくかかっていません。 - ロジックはコイントスです。 リクエストヘッダー全体の決定論的なハッシュで200/500を返すレベルの作業で、実際の拡張がやっていることとはかけ離れています。
- Envoyはv1.33に固定されています。
数字そのものを検算してみると、もう1点引っかかります。ドキュメントの要約項目はLuaのメモリがWasmより「~10x lower」だと書いていますが、表の値をそのまま割ると並行度1で5.9倍、16で11.4倍です。「10倍」という表現が成り立つのは並行度が高いときだけです。
そして、ドキュメント本文にある「約110 MiBから約290 MiB」という範囲は、実は2つの異なるランタイムを混ぜた値です。元のリポジトリはV8とWAMRという2つのWasmランタイムを並べて測定していて、約110はWAMRの並行度1の値(110.6)、約290はV8の並行度16の値(291.8)です。ドキュメントの表自体はV8だけを載せているので、表の並行度1は117.7になっています。大きな誤りではありませんが、表と本文が同じデータを語ってはいません。
これらの数字が無意味だという話ではありません。むしろ方向性は明らかに正しいのです — Wasmのメモリが並行度に応じてほぼ線形に増えるのは、Envoyがワーカースレッドごとにwasm VMを1つ持つからで、Luaが平坦なのも同じ理由の裏返しです。ワーカーを16個使うプロダクションゲートウェイなら、この差は実際に現れます。ただし「Wasmはポッドあたり290 MiB食う」をそのまま自分の容量計画に転記してはいけません。その数字は、Goで書いたコイントスを5 RPSで回したコンテナのRSSです。あなたの言語、あなたの負荷、あなたのモジュールで、あらためて測り直すべき値なのです。
Lua には fail-open しかない — プロト定義が証明する
ドキュメントの比較表の中で、メモリよりもはるかに重要な行が別にあります。失敗ポリシーです。
ドキュメントは、Wasmの失敗ポリシーが「configurable — fail-closed by default」である一方、Luaは「fail-open only — no configuration option」だと書いています。これはドキュメントの主張にすぎないのでしょうか。プロト定義を見ると、構造的に裏付けられます。
message WasmConfig {
string url = 1;
string sha256 = 2;
PullPolicy image_pull_policy = 3;
string image_pull_secret = 4;
string verification_key = 5;
google.protobuf.Struct plugin_config = 6;
string plugin_name = 7;
FailStrategy fail_strategy = 8;
VmConfig vm_config = 9;
PluginType type = 10;
}
message LuaConfig {
string inline_code = 1;
}
LuaConfigにはフィールドがinline_codeひとつしかありません。fail_strategyはそもそも存在しません。ドキュメントが「設定オプションがない」と言っているのは、レトリックではなくAPI表面そのものです。
Wasm側のFailStrategyはデフォルトがFAIL_CLOSE = 0で、プロトのコメントはFAIL_OPENについて「this flag is not recommended for the authentication or the authorization plugins」と警告しています。この警告をそのまま裏返せば結論が出ます — 認証・認可ロジックをLuaで書いてはいけません。 Luaはfail-openしかできず、fail-openな認証はスクリプトが落ちた瞬間に認証が通ってしまうことを意味するからです。新しいAPIがLuaを第一級に引き上げたという事実は、「これでLuaで認証を書いていい」という意味ではありません。
分離特性も同じ方向を指しています。ドキュメントによれば、Wasmはプラグインごとにサンドボックスを持つVMなのでクラッシュがプラグイン内に閉じ込められますが、Luaはインプロセスで動くため、クラッシュがワーカースレッドを道連れにしうるのです。メモリ20 MiBの代償です。
もう1点 — Wasmのサプライチェーンの話です。WasmConfigにはverification_keyフィールドがありますが、wasm.protoのコメントには「this field will not be implemented until the detailed design is established. For the future use, just keep this field in proto and hide from documentation」と書かれています。実際、istioのソースコード内にこのフィールドを参照するコードは存在しません。つまりOCIレジストリからWasmを取得する際、あなたが実際に持っている整合性の手段はsha256ダイジェスト固定であって、署名検証ではありません。sha256のほうはきちんと実装されていて、タグでイメージを参照しつつこのフィールドを埋めれば、取得した内容のチェックサムと照合されます。プロダクションで外部Wasmを使うなら、タグではなくダイジェストで固定してください。
既存の WasmPlugin はどうなるのか
TrafficExtensionがWasmPluginを置き換えると聞いて、まず気になるのがこれです。答えは穏やかです。1.30には強制マイグレーションがなく、既存のWasmPluginリソースはそのまま動作します。
仕組みが面白いところです。istiodが内部ですべてのWasmPluginをTrafficExtensionに変換したうえで、Envoy設定を生成します。変換コードを開くと、そのまま見えます。
const syntheticMarker = "~istio-translated-wasmplugin"
func translateWasmPlugin(cfg config.Config) *config.Config {
wp, ok := cfg.Spec.(*extensions.WasmPlugin)
if !ok {
return nil
}
targetRefs := wp.TargetRefs
if wp.TargetRef != nil && len(targetRefs) == 0 {
targetRefs = []*typeapi.PolicyTargetReference{wp.TargetRef}
}
te := &extensions.TrafficExtension{
Selector: wp.Selector,
TargetRefs: targetRefs,
Phase: extensions.TrafficExtension_ExecutionPhase(wp.Phase),
Priority: wp.Priority,
Match: convertTrafficSelectors(wp.Match),
FilterConfig: &extensions.TrafficExtension_Wasm{
Wasm: &extensions.WasmConfig{ /* ...フィールドをコピー... */ },
},
}
// 名前に syntheticMarker を付けて合成リソースであることを示す
return &config.Config{ /* ... */ }
}
デバッグの際に知っておくと便利な事実が1つあります。合成されたリソースの名前は、元の名前の後ろに~istio-translated-wasmpluginが付いた形です。istiod内部の設定をダンプしたときにこの名前が見えたら、あなたが作った覚えのない幽霊ではなく、あなたのWasmPluginが変換された結果です。
ちなみにWasmPluginのphase列挙型とネストされたTrafficSelector型は、後方互換性のために意図的にプロトに残されています。まとめると、WasmPluginは今や事実上、TrafficExtensionの薄いレガシー表面です。
では ambient はプロダクション対応済みなのか — 成熟度表をそのまま読む
「ambientはGA」というひと言はあまりに大雑把です。Istioは機能ごとの成熟度をfeatures.yamlで管理していて、これをそのまま読むほうがずっと正直です。2026年7月時点で、ambient領域はこうなっています。
| 機能 | 成熟度 |
|---|---|
| ztunnel: Core | Stable |
| Waypoints: Core | Stable |
| Waypoints: Gateway API Stable Channel (HTTPRoute, GRPCRoute) | Stable |
| Waypoints: DestinationRule | Stable |
| AuthorizationPolicy | Stable |
| PeerAuthentication | Stable |
| Waypoints: Cross-namespace usage | Beta |
| RequestAuthentication | Beta |
| DNS Proxying | Beta |
| Dual Stack, IPv6 | Beta |
| Multi-network multicluster | Beta |
| Waypoints: Gateway API Experimental Channel (TLSRoute, TCPRoute) | Alpha |
| Waypoints: VirtualService | Alpha |
| Waypoints: WebAssembly extensibility (WasmPlugin) | Alpha |
| Baggage based telemetry | Alpha |
読み方はこうです。コアは本当に安定しています — ztunnelとwaypoint本体、mTLS、認可ポリシーはStableです。ここまでが「ambientはGAだ」の正直な範囲であり、L4のmTLSとL4の認可だけが必要な大多数のサービスなら、この言葉はそのまま信じてよいものです。
問題はその上の層です。waypointのWasm拡張性はまだAlphaで、VirtualServiceもAlphaです。本稿の主役であるTrafficExtension自体も、紹介記事が明示する通りalphaであり、waypointにWasm/Luaを付けるドキュメントページにもalphaバナーが付いています。言い換えると、EnvoyFilterの穴を埋めに来たそのツール自身が、まだ安定化していないツールなのです。
そしてVirtualServiceがAlphaであることは、別の意味でも痛手です。移行ドキュメントはHTTPRouteへの移行を強く推奨しつつ、同じワークロードにVirtualServiceとHTTPRouteを混在させることはサポートされておらず、未定義の動作につながると警告しています。DestinationRuleのトラフィックポリシー(コネクションプール、外れ値検出、TLS)はwaypointでもそのまま使えますが、HTTPRouteはsubsetではなくServiceをbackendRefsとして使うため、バージョン別のトラフィック分割をするにはバージョンごとにServiceを分ける必要があります。サイドカー時代にsubsetベースのカナリアを回していたチームにとって、これは軽くない作り直しです。
いつ移行を見送るべきか
整理すると、判断基準はこうです。
ambient移行が無難なケース
- L4のmTLSとL4の認可だけで必要が満たせる。成熟度表のStableだけに依存する形になる。
- L7が必要なサービスが少数で、waypointを選択的にしか置かない。
- EnvoyFilterがない、あるいはあっても削除してよいものだけである。
まだ待つのが正しいケース
- Wasm/Lua拡張がリクエスト経路の中核にある。その表面はまだAlphaです。
- subsetベースの
VirtualServiceカナリアに深く依存している。HTTPRouteへの作り直しとServiceの分割コストを先に計算すべきです。 - 認証・認可をプロキシ拡張で処理していて、Luaへの移行を考えている。fail-openしか選べないという事実が、この選択肢を消します。
- 代替のないEnvoyFilterが1つでもある。Istioドキュメントの表現そのまま、それはブロッカーです。
移行するなら順序ははっきりしています。まずkubectl get envoyfilter -Aで棚卸しをしてください。静かに無視されるものは、移行後に数えるのではなく、移行前に数えるべきです。
おわりに
ambientの本当のコストは、リソース表には出てきません。サイドカーを外すということはEnvoyを外すということであり、Envoyを外すということはEnvoyに直接手を入れていたものすべてを外すということです。EnvoyFilterは移植可能な抽象ではなく、特定の実装に対するパッチでした。だからついてこないのです。Istioのドキュメントが「silently ignored」と「migration blocker」という言葉を直接使っているのは、むしろ正直なほうだと言えます。
Istio 1.30のTrafficExtensionは、その穴を狙った本気の試みです。WasmとLuaを1つのAPIにまとめ、waypointにも同じ文法で付き、既存のWasmPluginを内部変換で吸収します。ただし、それはEnvoyFilterの代替ではありません — EnvoyFilterが与えていた「Envoy設定を何でも触れる」という力は戻ってきていませんし、戻す計画もありません。あなたが手に入れたのはWasmとLuaという2つの明確に定義された拡張点であって、任意のフィルタチェーン操作ではないのです。あなたのEnvoyFilterがやっていたことがその2つで表現できるなら移行できますし、できないならできません。それがすべてです。
だからベンチマーク表ではなく、あなた自身の棚卸しから始めてください。kubectl get envoyfilter -Aの出力が、あなたのambient移行の難易度をほぼすべて物語っています。
参考資料
- Announcing Istio 1.30 (2026年5月18日)
- Introducing the TrafficExtension API — Liam White, Docusign
- Istio ドキュメント — サイドカーからambientへのポリシー移行(EnvoyFilterは「silently ignored」)
- Istio ドキュメント — 拡張性コンセプト(Lua対Wasm比較表)
- traffic_extension.proto — TrafficExtension API定義
- wasm.proto — WasmPluginとFailStrategyの定義
- envoy_filter.proto — EnvoyFilter自身の警告
- translate.go — WasmPluginからTrafficExtensionへの内部変換
- features.yaml — Istio機能別成熟度の原本
- liamawhite/lua-vs-wasm-envoy — ドキュメントが引用するメモリベンチマークの原本
- Istio Ambient vs Sidecar 比較(関連記事)