Istio の EnvoyFilter を用いてヘッダベースでのルーティングを追加してみる REALITY Advent Calendar 2024 #23
はじめに
本エントリーはREALITY Advent Calendar 2024の23日目の記事です。
その他のエントリーについては、以下のページからご覧ください。
こんにちは、DevOpsチームエンジニアマネージャーの kuyama です。
さて、本日は Istio の VirtualService を利用しているサービスにおいて「VirtualService にルーティングを追加せずに、ヘッダベースでのルーティングを追加する」にはどうするかについて解説、実践していきます。
今回は、前に REALITY の技術ブログである Now In REALITY Tech で書いた「PRごとに動作確認用の環境を自動生成する Now In REALITY Tech #123」を下敷きにしていきます。こちらの記事を読んでいなくても大丈夫なように解説していきますが、読んでおけばさらに理解が深まるかと思いますので、ぜひご一読ください!
さて、まずはなぜ「VirtualService にルーティングを追加せずに、HTTPヘッダベースでのルーティングを追加する」などという曲芸をしようとしているのでしょうか。
前述の記事で言及したように、動作確認用の環境をPRごとに生成するには、「各PR用のリソースへのルーティングの設定を Github Actions 経由で VirtualService リソースに追加していく」というステップが必要になります。これは、PRの削除後に該当のルーティング設定を削除する必要も出てきて、あまり好ましくありません。
そこで、「Istio の VirtualService を利用している状態でも、ルーティングに関するリソースも個別にデプロイ」できないか、模索することにしました。
結論として、「EnvoyFilter を利用し、各 Pod にサイドカーとして挿入されている Envoy に対して VirtualHost の route 情報を追加していく」という手法をとることで解決することにしました。
Istio でクラスタ内のルーティングが実現されている仕組み
前提として、REALITYではサービスの提供のためにGKEクラスタ上にアプリケーションサーバをPodとして立てており、そのGKEクラスタ上でサービスメッシュを実現するために、Anthos Service Mesh (以下 ASM)を導入しています。
この ASM は、Istio をベースにしており、クラスタ内の Pod から Pod への通信は下記の図のように、各 Pod に サイドカーインジェクション された proxy コンテナを経由して行われます。
この Istio で利用される Proxy コンテナは、Envoy をベースにして作られています。そして、Istio の Control Plane (istiod) が現在のクラスタ内のリソースの状態を検知して、各 Pod にインジェクションしている proxy に情報を伝達し、Envoy の宛先情報として設定します。
例として、以下のような Pod, Service, VirtualService を deploy します。
apiVersion: v1
kind: Pod
metadata:
name: hello
labels:
app: hello
spec:
containers:
- name: hello
image: gcr.io/google-samples/hello-app:2.0
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: hello
spec:
type: NodePort
selector:
app: hello
ports:
- port: 8080
targetPort: 8080
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: hello
spec:
hosts:
- hello.default.svc.cluster.local
http:
- route:
- destination:
host: hello
port:
number: 8080
この時、下記のコマンドで同じクラスタ内に 踏み台Pod を立てて、curl でクラスタ内ドメインを叩くと、以下のようにレスポンスが返ってきます。
$ kubectl run -it --rm=true busybox --image=yauritux/busybox-curl
If you don't see a command prompt, try pressing enter.
/home # curl hello.default.svc.cluster.local.
Hello, world!
Version: 2.0.0
Hostname: hello
この busybox Pod にサイドカーインジェクションされた Proxy に設定された宛先情報を確認してみましょう。(設定により、default ネームスペース上の全ての Pod には自動的に Proxy コンテナがサイドカーインジェクションされるようになっています)
宛先情報は、以下のコマンドを実行して API を叩くことで取得できます。
この API は json で情報が返ってくるのですが、見にくいので yaml 形式に変換します。
$ kubectl exec -it busybox -n default -c istio-proxy \
-- bash -c "curl http://localhost:15000/config_dump?resource={dynamic_route_configs}" | yq -P
下記のように、VirtualService で設定している route 情報は、DynamicRouteConfig 以下の routes に設定されます。
configs:
- '@type': type.googleapis.com/envoy.admin.v3.RoutesConfigDump.DynamicRouteConfig
route_config:
'@type': type.googleapis.com/envoy.config.route.v3.RouteConfiguration
name: hello.default.svc.cluster.local:80
virtual_hosts:
- name: hello.default.svc.cluster.local:80
domains:
- '*'
routes:
# パス、ヘッダベースでのルーティング設定はここにlistされる
- match:
prefix: /
route:
cluster: outbound|8080||hello.default.svc.cluster.local
timeout: 0s
retry_policy:
retry_on: connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes
num_retries: 2
retry_host_predicate:
- name: envoy.retry_host_predicates.previous_hosts
typed_config:
'@type': type.googleapis.com/envoy.extensions.retry.host.previous_hosts.v3.PreviousHostsPredicate
host_selection_retry_max_attempts: "5"
retriable_status_codes:
- 503
max_grpc_timeout: 0s
metadata:
filter_metadata:
istio:
config: /apis/networking.istio.io/v1alpha3/namespaces/default/virtual-service/hello
decorator:
operation: hello.default.svc.cluster.local:8080/*
EnvoyFilter で route config に設定を追加する
ここで、Istio の EnvoyFilter というリソースについて紹介します。
この EnvoyFilter というリソースは、istiod から各 Envoy プロキシに伝達される宛先情報の設定を変更することができます。つまり、このリソースを使えば、 Virtual Service を利用しなくても envoy に設定される route config に別のルーティングを追加することができます。
では、今度は以下のように Pod, Service, EnvoyFilter を追加してみます。
apiVersion: v1
kind: Pod
metadata:
name: hello-another
labels:
app: hello-another
spec:
containers:
- name: hello-another
image: gcr.io/google-samples/hello-app:2.0
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: hello-another
spec:
type: NodePort
selector:
app: hello-another
ports:
- port: 8080
targetPort: 8080
---
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: hello-another-envoy-filter
spec:
priority: 10
configPatches:
# HTTP_ROUTE の設定
- applyTo: HTTP_ROUTE
match:
context: SIDECAR_OUTBOUND
routeConfiguration:
vhost:
name: "hello.default.svc.cluster.local:80" # httpリクエストはデフォルトで80番ポートに送信されるので、vhostのnameはこのように指定する
patch:
operation: INSERT_FIRST
value:
# https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-msg-config-route-v3-routematch 参照
match:
prefix: "/"
case_sensitive: true
headers:
- name: REQUEST_ROUTING
string_match:
prefix: "another"
route:
cluster: "outbound|8080||hello-another.default.svc.cluster.local"
timeout: 0s
retry_policy:
retry_on: connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes
num_retries: 2
retry_host_predicate:
- name: envoy.retry_host_predicates.previous_hosts
typed_config:
'@type': type.googleapis.com/envoy.extensions.retry.host.previous_hosts.v3.PreviousHostsPredicate
host_selection_retry_max_attempts: "5"
retriable_status_codes:
- 503
max_grpc_timeout: 0s
decorator:
operation: hello-another.default.svc.cluster.local:8080/*
すると、以下のように追加した Service へのルーティングが追加されているのが読み取れます。
configs:
- '@type': type.googleapis.com/envoy.admin.v3.RoutesConfigDump.DynamicRouteConfig
route_config:
'@type': type.googleapis.com/envoy.config.route.v3.RouteConfiguration
name: "80"
virtual_hosts:
- name: hello.default.svc.cluster.local:80
domains:
- hello.default.svc.cluster.local
routes:
# 追加されたルーティング設定
- match:
prefix: /
case_sensitive: true
headers:
- name: REQUEST_ROUTING
string_match:
prefix: another
route:
cluster: outbound|8080||hello-another.default.svc.cluster.local
timeout: 0s
retry_policy:
retry_on: connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes
num_retries: 2
retry_host_predicate:
- name: envoy.retry_host_predicates.previous_hosts
typed_config:
'@type': type.googleapis.com/envoy.extensions.retry.host.previous_hosts.v3.PreviousHostsPredicate
host_selection_retry_max_attempts: "5"
retriable_status_codes:
- 503
max_grpc_timeout: 0s
decorator:
operation: hello-another.default.svc.cluster.local:8080/*
# 元々のルーティング設定 */
- match:
prefix: /
route:
cluster: outbound|8080||hello.default.svc.cluster.local
timeout: 0s
retry_policy:
retry_on: connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes
num_retries: 2
retry_host_predicate:
- name: envoy.retry_host_predicates.previous_hosts
typed_config:
'@type': type.googleapis.com/envoy.extensions.retry.host.previous_hosts.v3.PreviousHostsPredicate
host_selection_retry_max_attempts: "5"
retriable_status_codes:
- 503
max_grpc_timeout: 0s
metadata:
filter_metadata:
istio:
config: /apis/networking.istio.io/v1alpha3/namespaces/default/virtual-service/hello
decorator:
operation: hello.default.svc.cluster.local:8080/*
ここで、先ほど立てた踏み台 Pod から HTTP リクエストを行い、実際にヘッダベースでのルーティングが行われているか確認します。
/home # curl hello.default.svc.cluster.local.
Hello, world!
Version: 2.0.0
Hostname: hello
/home # curl -H "REQUEST_ROUTING:another" hello.default.svc.cluster.local.
Hello, world!
Version: 2.0.0
Hostname: hello-another # 新しく立てたPod名
……ヘッダを付与したリクエストが、新しく立てた Pod にルーティングされているのが見て取れます!
まとめ
上記のように、EnvoyFilter を利用してルーティングを追加することができました!
これを利用して、個別の開発環境をさらにアップデートすることができます。
ところで、現在 REALITY では ASM を利用していますが、Google Cloud から新しく提供されている Cloud Service Mesh(以下CSM) に移行する際には EnvoyFilter が利用できないらしいです。
つまり、CSM に移行するタイミングで今回の頑張りは無駄になるのですが……。まぁ、CSM が EnvoyFilter に対応するまで移行を待つか、素直に Gateway API を使うことにしましょう!HAHAHA!