Contents
Envoy Lua フィルタの概要と活用ポイント
Envoy の HTTP リクエスト/レスポンス処理に対して、軽量かつ高速にスクリプトを差し込める仕組みが Lua フィルタ です。LuaJIT が各 Worker Thread に直接組み込まれているため、ミリ秒単位のレイテンシでヘッダ操作や短時間の外部呼び出しが可能になります。本節では、フィルタがどのように実装されているかと、代表的なユースケースを簡潔に整理します。
Worker Thread と LuaJIT の関係
Lua フィルタは envoy.extensions.filters.http.lua.v3.Lua で定義され、各 Worker Thread が独自の LuaJIT インスタンスを保持します。この構造がもたらす主な利点は次の通りです。
-
高速実行
JIT コンパイル済みコードがスレッドローカルに残るため、同時リクエストでも競合が少なく、数十マイクロ秒程度のオーバーヘッドで処理できます。 -
メモリ分離
スレッドごとに確保された Lua VM が他スレッドの影響を受けないため、1 つのフィルタが原因で全体のパフォーマンスが低下するリスクが抑えられます。 -
安全なサンドボックス
Envoy の組み込み API(request_handle:*系)だけが利用可能で、外部プロセスやファイルシステムへの直接アクセスは禁止されています。
この特性により、ヘッダの付加・書き換え や 数百ミリ秒以内に完了する外部 API 呼び出し といった軽微処理が最適な利用シーンとなります。実装例は公式ドキュメント(Lua filter — Envoy 1.39)でも紹介されています。
Istio の EnvoyFilter CRD 基本構造と適用対象
Istio が提供する EnvoyFilter カスタムリソースは、上記 Lua フィルタをデータプレーンの任意のポイントに差し込むための宣言的設定です。本節では必須フィールドと代表的な適用パターン(Inbound / Outbound・ポート単位)を解説します。
基本構造と主要フィールド
EnvoyFilter は spec.configPatches 配列で patch を記述し、対象リソース (listener, http_connection_manager など) と適用条件 (applyTo, match) を組み合わせます。以下は最小構成の例です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
apiVersion: networking.istio.io/v1alpha3 kind: EnvoyFilter metadata: name: lua-header-modifier namespace: bookinfo spec: configPatches: - applyTo: HTTP_FILTER # フィルタレベルでパッチを適用 match: context: SIDECAR_INBOUND # サイドカーのインバウンドトラフィックに限定 listener: portNumber: 8080 # ポート 8080 のみ対象 filterChain: filter: name: envoy.filters.network.http_connection_manager patch: operation: INSERT_BEFORE value: name: lua.custom_header typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua inline_code: | function envoy_on_request(request_handle) request_handle:headers():add("x-custom-header", "istio-lua") end |
applyToはパッチ対象(例:HTTP_FILTER、CLUSTERなど)を指定します。contextにより SIDECAR_INBOUND / SIDECAR_OUTBOUND を選択し、サイドカー側のトラフィックだけに絞れます。listener.portNumberでポート単位の限定が可能です。
Outbound(SIDECAR_OUTBOUND)や複数ポートへの適用は、上記 match.context と portNumber を変更するだけで実現できます。公式リファレンスは正しいリンク Istio EnvoyFilter reference にあります。
サンプル実装:YAML 設定と Lua スクリプト
本節では、ヘッダの書き換え と 外部 HTTP 呼び出し の 2 パターンを具体的に示します。なお、外部呼び出しで使用している resty.http は Envoy の組み込み Lua 環境には含まれていません。そのため代替手段として Envoy が提供する HTTP Call API(request_handle:httpCall)を用いた実装例も併せて説明します。
1. ヘッダ追加・書き換えのスクリプト
以下はリクエストヘッダに x-demo: true を付与し、受信した user-agent を x-ua-modified にコピーする Lua スニペットです。inline_code 部分に貼り付けて利用します。
|
1 2 3 4 5 6 7 8 9 10 11 |
function envoy_on_request(request_handle) -- ヘッダ追加 request_handle:headers():add("x-demo", "true") -- user-agent を別名でコピー(上書き防止) local ua = request_handle:headers():get("user-agent") if ua then request_handle:headers():replace("x-ua-modified", ua) end end |
このロジックは ヘッダ操作だけ を行うため、外部ネットワークへの接続は発生せず、実質ゼロレイテンシで処理できます。
2. 外部 HTTP 呼び出し(代替実装)
(a) 必要な Cluster 定義
外部サービス internal.org.net:8888 に対して Envoy が直接リクエストを送れるよう、EnvoyFilter 内で CLUSTER を追加します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
- applyTo: CLUSTER match: context: SIDECAR_OUTBOUND patch: operation: ADD value: name: external_service_cluster type: STRICT_DNS connect_timeout: 0.5s load_assignment: cluster_name: external_service_cluster endpoints: - lb_endpoints: - endpoint: address: socket_address: address: internal.org.net port_value: 8888 |
(b) Lua スクリプト(request_handle:httpCall を使用)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
function envoy_on_request(request_handle) -- HTTP Call 用のリクエスト情報を作成 local headers = { [":method"] = "GET", [":path"] = "/health", [":authority"] = "internal.org.net:8888", ["x-forwarded-for"] = request_handle:headers():get(":authority") } -- タイムアウトは 200 ms(実運用環境での妥当性は SLA に合わせて調整してください) local timeout_ms = 200 -- Envoy のビルトイン HTTP Call API local success, resp_headers, resp_body = request_handle:httpCall( "external_service_cluster", -- 上記で定義した Cluster 名 headers, "", -- GET なのでボディは空文字列 timeout_ms ) if not success then request_handle:logInfo("外部呼び出し失敗: " .. (resp_body or "unknown")) request_handle:respond( {[":status"] = "502"}, "Bad Gateway" ) return end -- 外部サービスのステータスコード・ボディをそのままクライアントへ返す request_handle:respond( { [":status"] = tostring(resp_headers[":status"]) }, resp_body ) end |
ポイント解説
| 項目 | 内容 |
|---|---|
resty.http の代替 |
Envoy が組み込みで提供する request_handle:httpCall を利用すれば、外部ライブラリを追加せずに非同期 HTTP リクエストが可能です。 |
| タイムアウト根拠 | 200 ms は一般的なマイクロサービス間通信の SLA(95 パーセンタイル ≤ 250 ms)を満たす目安ですが、実際にはネットワーク遅延やバックエンド処理時間を測定し、負荷テストで最適値を決定してください。 |
| エラーハンドリング | httpCall が失敗した場合は 502 を返してトラフィック全体への影響を最小化します。 |
この実装は Qiita の「Istio と Envoy Filter を試してみた」や IBM のチュートリアルで紹介された構成をベースに、Envoy 標準 API に置き換えたものです。
ローカル環境でのデプロイと動作確認手順
本節では、サンプルリポジトリからマニフェストを取得し、Kubernetes クラスタ上で実際に適用するまでの流れを示します。各ステップは 「kubectl apply → 動作検証 → ログ確認」 のシンプルなフローです。
1. リポジトリクローンとマニフェスト適用
|
1 2 3 4 5 6 7 8 9 |
# GitHub からサンプルコードを取得 git clone https://github.com/example/envoy-lua-sample.git cd envoy-lua-sample # 必要な Namespace とモック Service、EnvoyFilter を順にデプロイ kubectl apply -f manifests/namespace.yaml kubectl apply -f manifests/external-service.yaml # internal.org.net のモック kubectl apply -f manifests/envoyfilter.yaml |
適用後、Istio Pilot が各サイドカーへ EnvoyFilter を配布するまで 数秒 待ちます。
2. リクエスト送信と結果検証
|
1 2 3 4 5 6 |
# reviews サービスの ClusterIP を取得 REVIEWS_IP=$(kubectl get svc reviews -n bookinfo -o jsonpath='{.spec.clusterIP}') # ヘッダ書き換えが正しく行われているか確認(-i でヘッダ表示) curl -s -D - http://$REVIEWS_IP:8080/api/reviews/1 | head -n 10 |
期待結果
- レスポンスヘッダに
x-demo: trueとx-ua-modified: <元の User-Agent>が含まれる。 /healthエンドポイントが呼び出された場合は、外部モックサービスから返されたステータスコードとボディがそのままクライアントに届く。
3. Envoy のデバッグログ取得
|
1 2 3 4 5 6 |
# reviews ポッド名を取得 POD=$(kubectl get pods -l app=reviews -n bookinfo -o jsonpath='{.items[0].metadata.name}') # istio-proxy コンテナのログから Lua 関連メッセージを抽出 kubectl logs $POD -c istio-proxy -n bookinfo | grep lua |
request_handle:logInfo() で出力した文字列が表示されれば、Lua スクリプトは期待どおりに実行されています。
トラブルシューティングとベストプラクティス
本節では、開発・運用時によく遭遇する問題と、その回避策・最適化ポイントをまとめます。各項目は 「何が起きたか」→「原因の切り分け方」→「推奨対策」 の順に記載しています。
1. Lua スクリプトのシンタックスエラー対策
- ローカルで事前チェック
luac -p script.luaを実行し、構文エラーを検出します。CI パイプラインに組み込むと PR 時点でミスが防げます。 - Envoy 起動時のエラーログ取得
スタートアップログにlua.filterエラーが出力されるので、kubectl logs <pod> -c istio-proxy | grep luaで確認してください。
2. フィルタ適用順序とサイドカー再起動
- INSERT_BEFORE / INSERT_AFTER の明示
認証やトレースフィルタの前に自作 Lua を置く場合はINSERT_BEFORE、後にしたい場合はINSERT_AFTERを指定し、意図しない上書きを防ぎます。 - CRD 更新後の再デプロイ
Istio 1.12 以降は Pilot が自動リロードしますが、古いバージョンやキャッシュが残るケースではkubectl rollout restart deployment <app>でポッドを再起動すると確実です。
3. パフォーマンスと安全性のベストプラクティス
| 項目 | 推奨設定 | 補足 |
|---|---|---|
| Lua メモリ上限 | max_memory_bytes: 10485760 (10 MiB)※フィールド名は Envoy バージョンにより maxMemoryBytes と表記されることがあります。公式スキーマで確認してください。 |
無制限だとメモリリークの危険があるため、必ず上限を設定します。 |
| 外部呼び出しタイムアウト | 200 ms – 500 ms の範囲で調整 | SLA に合わせて負荷テストし、バックエンドの平均応答時間 + ネットワーク遅延分を余裕として設定します。 |
| スクリプトサイズ | 1 KB 未満を目安 | 大規模ロジックはサイドカー再デプロイコストが増えるため、可能な限りシンプルに保ちます。 |
| 入出力バリデーション | ヘッダ長・文字種チェックを必ず実装 | 不正入力が LuaJIT のクラッシュ要因になるケースがあります(例:極端に長いヘッダ)。 |
これらは 2025 年 4 月に公開された shigemk2 さんの実践メモ とも合致しており、実運用での安定性向上が期待できます。
まとめ
- Envoy の Lua フィルタ は Worker Thread ごとに JIT コンパイルされた軽量 VM を提供し、ヘッダ操作や数百ミリ秒以内に完了する外部 API 呼び出しに最適です。
- Istio
EnvoyFilterCRD により、インバウンド/アウトバウンド・ポート単位でフィルタの適用範囲を細かく制御できます(正しいドキュメントは https://istio.io/latest/docs/reference/config/networking/envoy-filter/)。 - 外部 HTTP 呼び出し は
resty.httpが利用できないため、Envoy が標準で提供するrequest_handle:httpCallを使うのが安全かつ推奨されます。タイムアウトは実環境で測定した SLA に合わせて調整してください。 - デプロイ手順 は
kubectl apply→ 動作検証(curl)→ Envoy ログ確認 の流れで完了し、問題があればシンタックスチェックやフィルタ適用順序を見直します。 - ベストプラクティス としてはメモリ上限設定、タイムアウト調整、スクリプトサイズの抑制、入力バリデーションの徹底が重要です。
以上の手順と注意点を踏めば、Istio 環境下で安全かつ高性能な Lua カスタムロジックを確実に組み込むことができます。ぜひ本稿のサンプルをベースに、各サービス固有の要件に合わせて拡張してみてください。