IstioとEnvoy Filterを試してみた｜ローカル環境でアクセスログ＆Luaを試す

# はじめに
Envoy Proxyは、サービスメッシュの中核となるプロキシで、トラフィック管理や監視、セキュリティ機能を提供する強力なツールです。特に、Istioと組み合わせることで、トラフィックの制御やカスタマイズを柔軟に行えます。

https://www.envoyproxy.io/

Istioでは、Envoy Proxyの動作をカスタマイズするために「Envoy Filter」と呼ばれるカスタムリソースを使用できます。このリソースを利用することで、Envoy Proxyのトラフィック処理をさらに細かく制御できるようになります。

https://istio.io/latest/docs/reference/config/networking/envoy-filter/

本記事では、ローカル環境にKubernetesクラスターを構築し、Istioを使って以下の2つを試してみます。

1. **アクセスログの有効化**
Envoy Proxyにリクエスト・レスポンスの詳細を記録する設定を追加します。

2. **LuaフィルターでHTTPヘッダーを動的に付与**
リクエストのパラメータを基にカスタムHTTPヘッダーを追加する方法を解説します。

## 今回のテーマ
1. **ローカル環境でKindクラスタを構築する**
2. **Istioのインストール**
3. **Envoy Filterでアクセスログの有効化**
4. **LuaフィルターでHTTPヘッダーを付与**

# 1. ローカル環境でKindクラスタを構築する

まずはローカル環境にKubernetesクラスターを構築します。
今回使用するのは、Dockerコンテナ上で簡単にクラスターを作成できるツール「[Kind](https://kind.sigs.k8s.io/)」です。

[Kind](https://kind.sigs.k8s.io/)を使ってローカル環境にKubernetesクラスターを構築する記事はネットで調べると沢山出てきますので、既にご存知の方も多いと思います。
そのためテーマ1、2については、必要に応じて読み飛ばしてください:pray:


## 必要なツール
以下のツールがインストールされていることを確認してください。
- [Docker](https://www.docker.com/) 
- [Kind](https://kind.sigs.k8s.io/)
- [kubectl](https://kubernetes.io/docs/tasks/tools/)

## Kindクラスタの構築
### 専用のディレクトリを作成（任意）
```bash
mkdir -p ~/kind-envoy
cd ~/kind-envoy
```

### Kindクラスタ設定ファイルを作成
`kind-config.yaml` という名前で以下の内容を保存します。
   ```yaml
   kind: Cluster
   apiVersion: kind.x-k8s.io/v1alpha4
   nodes:
     - role: control-plane
       extraPortMappings:
         - containerPort: 30000
           hostPort: 30000
           protocol: TCP
   ```
:::note info
今回は、単純な動作確認なので、コントロールプレーンノードのみを作成します。本番環境に近い構成を試したい場合や、スケジューリングや分散処理をテストしたい場合は、ワーカーノードを追加する設定に変更してください。
:::


### クラスタを作成
   ```bash
   kind create cluster --name envoy-testing --config kind-config.yaml
```
 
#### 動作確認
正常に作成されたか確認します。

```bash
kubectl cluster-info --context kind-envoy-testing
```

# 2. Istioのインストール 
Envoy Filterを適用するには、Istioのサイドカー（Envoy）を利用します。
以下の手順でIstioをインストールします。

## Istioctlのインストール
```bash
curl -L https://istio.io/downloadIstio | sh -
cd istio-*
export PATH=$PWD/bin:$PATH
```
 
## Istioのデフォルトプロファイルをインストール
```bash
istioctl install --set profile=demo -y
```
 
### 動作確認
Istioコントロールプレーンが正常に動作しているか確認します。
```bash
kubectl get pods -n istio-system
```
 
### サイドカー自動注入の有効化
デフォルトのネームスペースにIstioサイドカー（Envoy）を自動注入します。
```bash
kubectl label namespace default istio-injection=enabled
```

### サンプルアプリケーションをデプロイ
```bash
kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.24/samples/sleep/sleep.yaml
kubectl get pods
```

# 3. Envoy Filterでアクセスログを有効化 
お待たせしました！ここからは実際にEnvoy Filterを使って設定を進めていきます。
まずは、Envoy Proxyにリクエストやレスポンスの詳細を記録するアクセスログを有効化する設定を作成してみましょう。

## Envoy Filterの作成 
以下は、アクセスログを標準出力に記録するためのEnvoy Filter設定例です。

```yaml
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: enable-access-logs
  namespace: default
spec:
  # Workload Selectorを使用して、このフィルターを適用する対象のPodを指定
  workloadSelector:
    labels:
      # このフィルターは、ラベル "app: sleep" を持つPodにのみ適用される
      app: sleep
  configPatches:
    - applyTo: HTTP_FILTER
      # フィルターの適用対象をHTTP_FILTER（リクエスト/レスポンス処理）に指定
      match:
        context: ANY
        listener:
          filterChain:
            filter:
              # EnvoyのHTTP Connection Managerフィルターを対象とする
              name: "envoy.filters.network.http_connection_manager"
      patch:
        # "MERGE"は既存の設定を変更または追加する操作
        operation: MERGE
        value:
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
            access_log:
              # アクセスログの設定を追加
              - name: envoy.file_access_log
                typed_config:
                  # アクセスログの形式としてファイルログを選択
                  "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
                  path: /dev/stdout
```

この設定により、`app: sleep`ラベルを持つPodのトラフィックに対して、アクセスログが標準出力に記録されます。

### 詳細な設定ポイント解説
- `workloadSelector`: このセクションでは、このEnvoy FilterがどのPodに適用されるかを指定します。
`labels`に指定したキーと値（例: `app: sleep`）が対象Podのラベルと一致する場合に適用されます。
この設定がないと、デフォルトでネームスペース内のすべてのPodに適用されます。

- `configPatches`: Envoyの設定を変更または追加するセクションです。
- `applyTo`フィールドで、どの設定部分にパッチを適用するかを指定します。ここでは、HTTPのリクエストやレスポンスを処理する`HTTP_FILTER`を対象としています。

- `match.context`: Envoy Filterが適用されるトラフィックの方向やコンテキストを指定します。
ANY を指定することで、リクエスト（INBOUND）とレスポンス（OUTBOUND）の両方を対象にします。

- `listener.filterChain.filter.name`: Envoyの内部で動作するフィルターを指定します。
ここでは envoy.filters.network.http_connection_manager を選択し、HTTPトラフィックを処理するフィルターを対象としています。
`patch.operation`: `MERGE`は既存の設定を変更または追加する操作です。
`ADD`で新しい設定を追加したり、`REMOVE`で既存の設定を削除することも可能です。

- `access_log`: このセクションでアクセスログの出力先やフォーマットを指定します。
ここでは`envoy.file_access_log`を利用して、ログを標準出力（/dev/stdout）に出力する設定を行っています。詳細は、以下をご参照ください。

https://istio.io/latest/docs/reference/config/networking/envoy-filter/#EnvoyFilter-PatchContext

https://github.com/istio/istio/wiki/EnvoyFilter-Samples#tracing-and-access-logging

## フィルターの適用 
作成したマニフェストを適用します。
```bash
kubectl apply -f enable-access-logs.yaml
```
### 動作確認 
#### 1. 対象Podにリクエストを送信
```bash
kubectl exec -it <sleep-pod-name> -- curl http://httpbin.org/get
```

`<sleep-pod-name>`は、kubectl get podsコマンドで確認したsleepPodの名前に置き換えてください。
ここでは`curl`を使用して、`httpbin.org/get`エンドポイントにHTTPリクエストを送信します。

#### 2. Envoy Proxyのログを確認
サイドカーコンテナ（istio-proxy）が出力するログにアクセスします。

```bash
kubectl logs <sleep-pod-name> -c istio-proxy
```

##### 出力例
```csharp
[2024-11-30T12:00:00.123Z] "GET /get HTTP/1.1" 200 - "-" "-" 0 123 45 42 "-" "curl/7.68.0" "abcd1234-5678-90ef-ghij-klmnopqrstuv" "localhost" "127.0.0.1:8080"
```
アクセスログが出力されました！
このようにして、アクセスログが記録されていることで、リクエストのトラフィックの流れを把握しやすくなり、デバッグや監視が容易になります。

:::note info
このログはEnvoyのアクセスログフォーマットに基づいています。必要に応じて、Envoy Filterの設定を変更することで、ログフォーマットをカスタマイズできます。
:::

# 4. LuaフィルターでHTTPヘッダーを付与 
### Luaフィルターとは？
Envoyには、リクエストやレスポンスの処理を行うためのさまざまな組み込みHTTPフィルターが用意されています。
しかし、一部の特殊な操作や要件については、これらの組み込みフィルターだけでは対応できない場合があります。
たとえば、動的なリクエスト処理や、特定の条件に基づいてトラフィックを操作するカスタムロジックが必要な場合です。

こうしたカスタムロジックを実現するために、Envoyは汎用的なLuaフィルターを提供しています。Luaフィルターを使用すると、以下のような操作が可能です。

- 特定の条件に基づいてHTTPヘッダーを追加または変更
- リクエストやレスポンスの内容を操作
- トラフィックの動的なリダイレクトやフィルタリング

Luaは軽量で柔軟性が高いため、Envoyの動作を動的に拡張する際に適しています。具体的には、Luaスクリプトを利用してEnvoyのトラフィック処理をカスタマイズすることができます。

詳細は以下をご参照ください。

https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/lua_filter#config-http-filters-lua

以下のようにHTTPヘッダーを追加する簡単なスクリプトを試してみます。

### Envoy Filterの修正（Luaフィルターを付与）
以下は、前述のアクセスログを有効にした上で、Luaフィルターを適用する設定です。

```yaml
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: enable-access-logs
  namespace: default
spec:
  workloadSelector:
    labels:
      app: sleep
  configPatches:
    # アクセスログの有効化
    - applyTo: HTTP_FILTER
      match:
        context: ANY
        listener:
          filterChain:
            filter:
              name: "envoy.filters.network.http_connection_manager"
      patch:
        operation: MERGE
        value:
          typed_config:
            "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
            access_log:
              - name: envoy.file_access_log
                typed_config:
                  "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
                  path: /dev/stdout

    # Luaフィルターの追加 ----------------------------------------------------------
    - applyTo: HTTP_FILTER
      match:
        # OUTBOUND（サイドカーから外部への通信）時に適用
        context: SIDECAR_OUTBOUND
        listener:
          filterChain:
            filter:
              name: envoy.filters.network.http_connection_manager
              # HTTP Routerフィルターの直前にLuaフィルターを挿入
              subFilter:
                name: envoy.filters.http.router
      patch:
        # INSERT_BEFOREは指定したフィルターの前に新しいフィルターを追加する操作
        operation: INSERT_BEFORE
        value:
          name: envoy.filters.http.lua
          typed_config:
            # Luaスクリプトの設定
            "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
            # default_source_code を使用
            default_source_code:
              inline_string: |
                -- リクエストのパスから "user_id" クエリパラメータを抽出
                function envoy_on_request(request_handle)
                  local path = request_handle:headers():get(":path")  -- リクエストパスを取得
                  local user_id = path:match("user_id=([^&]+)")  -- "user_id" の値を抽出
                  if user_id then
                    -- 抽出した "user_id" をカスタムHTTPヘッダー "X-Custom-UserID" に追加
                    request_handle:headers():add("X-Custom-UserID", user_id)
                  end
                end
```
### 詳細な設定ポイント解説
- `subFilter`: envoy.filters.http.router
Luaフィルターを挿入する位置を指定します。ここでは `envoy.filters.http.router`（リクエストをルーティングするフィルター）の直前に挿入しています。

- `patch.operation`: INSERT_BEFORE
INSERT_BEFORE を指定することで、指定したフィルターの前に新しいフィルターを追加します。
`typed_config`: Luaフィルターの設定を行います。
`default_source_code`の使用: inline_codeは非推奨となっているので、`default_source_code`を利用し、`inline_string`フィールドでLuaコードを定義します。詳細は以下をご参照ください。

https://github.com/envoyproxy/examples/blob/main/lua/envoy.yaml

### フィルターの適用 
修正したマニフェストを適用してみます。
```bash
kubectl apply -f enable-access-logs.yaml
```

### 動作確認 
Luaフィルターを適用したことで、HTTPリクエストのURLからクエリパラメータ user_id を抽出し、それを新たなカスタムHTTPヘッダー X-Custom-UserID に追加する動作を確認します。
#### 1. 対象Podにリクエストを送信
Luaフィルターを適用したPodに対して、以下のようなリクエストを送信します。
```bash
kubectl exec -it <sleep-pod-name> -- curl "http://httpbin.org/headers?user_id=12345"
```
- URLにはクエリパラメータ `user_id=12345` を含めています。この `user_id` の値をLuaフィルターが抽出し、HTTPヘッダーに追加します。

#### 2. レスポンスにカスタムHTTPヘッダーが追加されていることを確認
応答として返されるJSONに、新たに追加されたカスタムHTTPヘッダー X-Custom-UserID が含まれることを確認します。
##### 出力例
```csharp
{
  "headers": {
    "X-Custom-UserID": "12345",
    ...
  }
}
```
このレスポンスは、`httpbin.org/headers` エンドポイントが現在のHTTPリクエストヘッダーを返すことで得られます。
Luaフィルターによって追加された `X-Custom-UserID` ヘッダーには、リクエストURLのクエリパラメータ `user_id` の値が反映されます（この場合は 12345）。

# まとめ
今回の記事では、Envoy ProxyとIstioを使用して、トラフィックの観測や制御を行う方法を解説しました。具体的には、以下の2つの機能を試しました。

#### 1. アクセスログの有効化
Envoy Filterを利用して、トラフィックの詳細を記録するアクセスログの設定方法をご紹介しました。

#### 2. LuaフィルターでHTTPヘッダーを付与
Luaスクリプトを使い、リクエストのURLから情報を抽出し、新たなカスタムHTTPヘッダーを動的に追加する方法を実践しました。
また、今回は特定のPod全体に対してフィルターを適用しましたが、特定のルートに対して適用することも可能です。詳細はこちらを参照ください。

https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/lua_filter#per-route-configuration

これらの設定を通じて、Envoy ProxyとIstioの柔軟性を体験できることが分かります。Envoy Filterはカスタマイズの幅が広く、さまざまなシナリオで活用可能です。

例）
  * トラフィックのフィルタリングやリダイレクト
  * 動的なデータ操作や監視の強化
  * セキュリティポリシーの細分化

Envoy ProxyとIstioは、本番環境でのトラフィック管理やセキュリティ強化に役立つツールです。ぜひ、この記事を参考に、皆さんも新たな機能やシナリオを試してみてください。

この記事が、皆さんのシステム構築やトラフィック管理の参考になれば幸いです。