見出し画像

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 / Architecture

この 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!