Envoy Gatewayは、Multi-cluster Service APIsのServiceImport
に対応しています。本記事では、Envoy GatewayがServiceImport
をどのように扱っているか解説します。
1. ServiceImportの役割
ServiceImport
はMulti-cluster Service APIsで定義されたカスタムリソースで、Kubernetesのマルチクラスタ環境において、他のクラスタからエクスポートされたサービスを現在のクラスタ内でインポートし、利用可能にするためのリソースです。
ServiceImportの処理フロー
Envoy Gatewayのコントローラーは、以下の手順でServiceImport
を処理します:
-
リソースのウォッチと取得:
- コントローラーは
ServiceImport
リソースの作成、更新、削除イベントを監視します。 -
ServiceImport
が作成または更新されると、その詳細を取得し、関連するエンドポイント情報を収集します。
- コントローラーは
-
EndpointSliceからのIP取得:
-
ServiceImport
に関連付けられたEndpointSlice
リソースをリストし、各エンドポイントのIPアドレスとポート情報を取得します。 - これにより、EndpointSliceに列挙されているIPがEnvoyの設定に反映されます。
-
-
EnvoyへのIP登録:
- 取得したIPアドレスを基に、Envoyの設定に反映させ、トラフィックルーティングを実現します。
3. Envoy GatewayにおけるServiceImportからのIP登録の流れ
Envoy Gatewayは、ServiceImport
リソースを通じてインポートされたサービスのエンドポイントIPを管理し、これを利用してトラフィックを適切にルーティングします。具体的な流れは以下の通りです:
-
ServiceImportおよび関連リソースの取得:
- Envoy Gatewayコントローラーは、
ServiceImport
リソースを監視し、必要に応じて取得します。 - 取得された
ServiceImport
に関連するEndpointSlice
をリストし、各エンドポイントのIPアドレスを収集します。
- Envoy Gatewayコントローラーは、
-
EndpointSliceからのエンドポイント情報の抽出:
- 各
EndpointSlice
には、実際のエンドポイント(Podや外部サービス)のIPアドレスとポート情報が含まれています。 - Envoy GatewayはこれらのIPアドレスを基に、Envoyの設定を更新します。
- 各
コードスニペットの例
以下は、Envoy GatewayのコントローラーがServiceImport
およびEndpointSlice
からIPアドレスを取得し、Envoyに登録する際のコード部分です:
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で議論および実装が行われています:
6. まとめ
Envoy Gatewayは、Multi-cluster Service APIsのServiceImport
およびEndpointSlice
を活用してトラフィックルーティングを実現できます。しかし、Service
を経由せずにEndpointSlice
のIPを直接利用する設計には、kube-proxy
が既に提供するTopology Aware Routingの機能の利用に制限がかかる可能性があります。
現在、この課題に対する対応はGitHubのIssue #1909で進行中ですが、まだ未実装の段階です。今後のアップデートにより、Envoy Gatewayが地域やゾーンに基づいた効率的なトラフィックルーティングを実現するための機能が強化されることが期待されています。
参考リンク: