Envoy GatewayにおけるMulti-cluster Service APIsの対応状況

Envoy Gatewayは、Multi-cluster Service APIsの`ServiceImport` に対応しています。本記事では、Envoy Gatewayが`ServiceImport`をどのように扱っているか解説します。

## 1. ServiceImportの役割

`ServiceImport`はMulti-cluster Service APIsで定義されたカスタムリソースで、Kubernetesのマルチクラスタ環境において、他のクラスタからエクスポートされたサービスを現在のクラスタ内でインポートし、利用可能にするためのリソースです。

### ServiceImportの処理フロー

Envoy Gatewayのコントローラーは、以下の手順で`ServiceImport`を処理します：

1. **リソースのウォッチと取得**:
   - コントローラーは`ServiceImport`リソースの作成、更新、削除イベントを監視します。
   - `ServiceImport`が作成または更新されると、その詳細を取得し、関連するエンドポイント情報を収集します。

2. **EndpointSliceからのIP取得**:
   - `ServiceImport`に関連付けられた`EndpointSlice`リソースをリストし、各エンドポイントのIPアドレスとポート情報を取得します。
   - これにより、EndpointSliceに列挙されているIPがEnvoyの設定に反映されます。

3. **EnvoyへのIP登録**:
   - 取得したIPアドレスを基に、Envoyの設定に反映させ、トラフィックルーティングを実現します。

## 3. Envoy GatewayにおけるServiceImportからのIP登録の流れ

Envoy Gatewayは、`ServiceImport`リソースを通じてインポートされたサービスのエンドポイントIPを管理し、これを利用してトラフィックを適切にルーティングします。具体的な流れは以下の通りです：

1. **ServiceImportおよび関連リソースの取得**:
   - Envoy Gatewayコントローラーは、`ServiceImport`リソースを監視し、必要に応じて取得します。
   - 取得された`ServiceImport`に関連する`EndpointSlice`をリストし、各エンドポイントのIPアドレスを収集します。

2. **EndpointSliceからのエンドポイント情報の抽出**:
   - 各`EndpointSlice`には、実際のエンドポイント（Podや外部サービス）のIPアドレスとポート情報が含まれています。
   - Envoy GatewayはこれらのIPアドレスを基に、Envoyの設定を更新します。

### コードスニペットの例

以下は、Envoy Gatewayのコントローラーが`ServiceImport`および`EndpointSlice`からIPアドレスを取得し、Envoyに登録する際のコード部分です：

```go
func (r *gatewayAPIReconciler) processBackendRefs(ctx context.Context, gwcResource *resource.Resources, resourceMappings *resourceMappings) {
    for backendRef := range resourceMappings.allAssociatedBackendRefs {
        backendRefKind := gatewayapi.KindDerefOr(backendRef.Kind, resource.KindService)
        r.log.Info("processing Backend", "kind", backendRefKind, "namespace", string(*backendRef.Namespace), "name", string(backendRef.Name))

        var endpointSliceLabelKey string
        switch backendRefKind {
        case resource.KindService:
            // Serviceの場合の処理
            service := new(corev1.Service)
            err := r.client.Get(ctx, types.NamespacedName{Namespace: string(*backendRef.Namespace), Name: string(backendRef.Name)}, service)
            if err != nil {
                r.log.Error(err, "failed to get Service", "namespace", string(*backendRef.Namespace), "name", string(backendRef.Name))
            } else {
                resourceMappings.allAssociatedNamespaces.Insert(service.Namespace)
                gwcResource.Services = append(gwcResource.Services, service)
                r.log.Info("added Service to resource tree", "namespace", string(*backendRef.Namespace), "name", string(backendRef.Name))
            }
            endpointSliceLabelKey = discoveryv1.LabelServiceName

        case resource.KindServiceImport:
            // ServiceImportの場合の処理
            serviceImport := new(mcsapiv1a1.ServiceImport)
            err := r.client.Get(ctx, types.NamespacedName{Namespace: string(*backendRef.Namespace), Name: string(backendRef.Name)}, serviceImport)
            if err != nil {
                r.log.Error(err, "failed to get ServiceImport", "namespace", string(*backendRef.Namespace), "name", string(backendRef.Name))
            } else {
                resourceMappings.allAssociatedNamespaces.Insert(serviceImport.Namespace)
                key := utils.NamespacedName(serviceImport).String()
                if !resourceMappings.allAssociatedServiceImports.Has(key) {
                    resourceMappings.allAssociatedServiceImports.Insert(key)
                    gwcResource.ServiceImports = append(gwcResource.ServiceImports, serviceImport)
                    r.log.Info("added ServiceImport to resource tree", "namespace", string(*backendRef.Namespace), "name", string(*backendRef.Name))
                }
            }
            endpointSliceLabelKey = mcsapiv1a1.LabelServiceName

        case egv1a1.KindBackend:
            // Backendの場合の処理
            // 省略
        }

        // EndpointSliceの取得
        if endpointSliceLabelKey != "" {
            endpointSliceList := new(discoveryv1.EndpointSliceList)
            opts := []client.ListOption{
                client.MatchingLabels(map[string]string{
                    endpointSliceLabelKey: string(backendRef.Name),
                }),
                client.InNamespace(*backendRef.Namespace),
            }
            if err := r.client.List(ctx, endpointSliceList, opts...); err != nil {
                r.log.Error(err, "failed to get EndpointSlices", "namespace", string(*backendRef.Namespace), backendRefKind, string(backendRef.Name))
            } else {
                for _, endpointSlice := range endpointSliceList.Items {
                    key := utils.NamespacedName(&endpointSlice).String()
                    if !resourceMappings.allAssociatedEndpointSlices.Has(key) {
                        resourceMappings.allAssociatedEndpointSlices.Insert(key)
                        r.log.Info("added EndpointSlice to resource tree", "namespace", endpointSlice.Namespace, "name", endpointSlice.Name)
                        gwcResource.EndpointSlices = append(gwcResource.EndpointSlices, &endpointSlice)
                    }
                }
            }
        }
    }
}
```

このコードでは、`ServiceImport`によって参照されるIPアドレスを`EndpointSlice`から取得し、Envoyの設定に反映させています。

## 4. Topology Aware Routingの課題

Envoyが`Service`を経由せずに`EndpointSlice`のIPをEnvoyが直接参照する設計には、以下のような課題が存在します：

### 1. kube-proxyによるTopology Aware Routingとの整合性

Kubernetesの`kube-proxy`は、クラスター内のトラフィックを効率的にルーティングするためにTopology Aware Routingを実装しています。これは、トラフィックをできるだけ地理的に近いエンドポイントに送ることで、レイテンシの低減や帯域幅の節約を図るものです。しかし、Envoy Gatewayが`EndpointSlice`のIPを直接参照する場合、`kube-proxy`が提供するTopology Aware Routingの機能、又は類似する機能をEnvoyが独自に再実装してほしくなります。

### 2. 現在の対応状況

このTopology Aware Routingの類似課題に対する対応は、現在Envoy GatewayのGitHubリポジトリで進行中です。具体的には、以下のIssueで議論および実装が行われています：

- [Locality Based Routing Support #1909](https://github.com/envoyproxy/gateway/issues/1909)

## 6. まとめ

Envoy Gatewayは、Multi-cluster Service APIsの`ServiceImport`および`EndpointSlice`を活用してトラフィックルーティングを実現できます。しかし、`Service`を経由せずに`EndpointSlice`のIPを直接利用する設計には、`kube-proxy`が既に提供するTopology Aware Routingの機能の利用に制限がかかる可能性があります。

現在、この課題に対する対応はGitHubのIssue [#1909](https://github.com/envoyproxy/gateway/issues/1909)で進行中ですが、まだ未実装の段階です。今後のアップデートにより、Envoy Gatewayが地域やゾーンに基づいた効率的なトラフィックルーティングを実現するための機能が強化されることが期待されています。

参考リンク:
- [Locality Based Routing Support #1909](https://github.com/envoyproxy/gateway/issues/1909)
- [Multicluster Service Routing | Envoy Gateway](https://gateway.envoyproxy.io/docs/tasks/traffic/multicluster-service/)