Envoy Proxyを使ったファイルベースの動的コンフィグレーション

こんにちは、ed (edward) です。

この記事は、みらい翻訳のカレンダー | Advent Calendar 2022 - Qiita の17日目です。

さて、SaaS として提供している Mirai Translator のようなプロダクトの場合、原則として機能追加等のデプロイ、リリースをゼロダウンタイムで実施することが求められます。

また Strangler Fig Pattern を適用して追加や変更する機能を別サービスとしてデプロイし、最終的に全てのトラフィックが新しいサービスに流れるようにルーティングを切り替えて旧サービスを枯らせていく戦略で迅速かつ継続的なアーキテクチャ (Continuous Architecture) をドライブしたいと考えています。

Kubernetes 環境でこのような切り替えの方法はいくつかあり、AWS App MeshIstio のようなサービスメッシュを活用するケースがよく紹介されていますが、ここではサービスメッシュのサイドカーでよく使用されている Envoy Proxy をファイルベースの動的コンフィグレーションを使用して説明します。

Envoy Proxy とは

Envoy Proxy は、C++ で記述された軽量のプロキシコンポーネントです。API または構成ファイルを使ってゼロダウンタイムでルーティング等の構成変更が可能なため、さまざまなサービスメッシュのデータプレーンのコンポーネントとして活用されています。

ルーティングの変更がゼロダウンタイムで可能なため Strangler Fig Pattern、Feature Toggles (Feature Flags)、A/B テスト、Blue/Green Deployment、カナリアリリース等さまざまなユースケースの実現が可能です。

コントロールプレーン

サービスメッシュのコントロールプレーンでは、Envoy Proxy のようなデータプレーンのサイドカーAPI により制御します。

この記事では、Envoy Proxy の API にアクセスするコントロールプレーンを実装せず、構成ファイルを切り替えて制御する方法を説明します。

環境

プロダクション環境では Amazon EKS 等を利用することになりますが、検証では Apple Silicon 搭載の mac miniRancher Desktop をインストールした環境を使用します。

Amazon EKS と Persistent Volumes に EBS を利用する場合は「Envoy Proxyを使ったファイルベースの動的コンフィグレーション (EKS)」を参照してください。

そして、プロダクション環境で利用する場合は「Envoy Proxyを使ったファイルベースの動的コンフィグレーションとk8sのライフサイクル」を参照することをおすすめします。

ストーリー

次のような Strangler Fig Pattern を実現するストーリーをこれから説明します。

  1. 今後枯らせるサービスをイメージした spring-boot-demo サービスをデプロイします
  2. spring-boot-demo の動作確認を実施します
  3. 新サービスを想定した、jp-spring-boot-demo をデプロイします
  4. jp-spring-boot-demo 単体の動作確認を実施します
  5. spring-boot-demo API/jp パスを追加して jp-spring-boot-demo にルーティングするように設定します

準備

この記事のコードは、GitHub リポジトリ にあります。最初にこれを clone します。

git clone https://github.com/takesection-sandbox/envoyproxy-examples.git

以降は、clone した envoyproxy-examplesspring-boot ディレクトリで操作を行なっていきます。

cd envoyproxy-examples/spring-boot

コンテナイメージのビルド

このビルドの前に Java 17 以降の JDKMaven、Rancher Desktop をインストールして、Rancher Desktop では Container Engine に dockerd(moby) を使用するように設定しておきます。

次のコマンドでコンテナイメージをビルドします。

mvn spring-boot:build-image

1. spring-boot-demo をデプロイ

Envoy Proxy のクラスタとリスナーの構成ファイルをコピーします。

mkdir /tmp/rancher-desktop/spring-boot-demo
cp -r config /tmp/rancher-desktop/spring-boot-demo

Envoy Proxy の「Configuration: Dynamic from filesystem」には、次のような記述があります。

Envoy only updates when the configuration file is replaced by a file move, and not when the file is edited in place.

It is implemented this way to ensure configuration consistency.

意訳すると、Envoy の構成の一貫性を確保するため、構成ファイルをその場で編集するのではなく、移動によって置換された場合にのみ更新されるとあります。

ConfigMap や Secret は読み込みのみのボリュームマウントしかサポートしていないこともあって、ファイルベースの動的コンフィグレーションには不適です。

そのため、この記事では読み書きが可能なボリュームマウントを使用します。

次にサービスを Helm チャートでデプロイします。Helm を利用すると、デプロイする構成それぞれのマニフェストファイル (deployment、service、configmap など) をテンプレート化して、必要な部分の変更がしやすくなります。

helm install spring-boot-demo ./spring-boot-demo

2. spring-boot-demo の動作確認を実施

ここでは、Ingress を使用せず、port-forward を使って確認します。まず、次のコマンドを実行します。

kubectl port-forward service/spring-boot-demo 8080:80

構成は次のとおりです。

別のターミナルで、次のコマンドを実行します。

curl http://localhost:8080

レスポンスとして、"Hello World" が表示されます。

3. 新機能を想定した、jp-spring-boot-demo をデプロイ

Envoy Proxy のクラスタとリスナーの構成ファイルをコピーします。

mkdir /tmp/rancher-desktop/jp
cp -r config /tmp/rancher-desktop/jp

Helm チャートでデプロイします。

helm install jp --values=jp-values.yaml ./spring-boot-demo

4. jp-spring-boot-demo 単体の動作確認を実施

ここでも、Ingress を使用せず、port-forward を使って確認します。まず、次のコマンドを実行します。

kubectl port-forward service/jp-spring-boot-demo 8081:80

構成は次のとおりです。

別のターミナルで、次のコマンドを実行します。

curl http://localhost:8081

レスポンスとして、"Hello Japan" が表示されます。

5. ルーティングの変更

では、ここから本題の spring-boot-demo API/jp パスを追加して jp-spring-boot-demo にルーティングするように設定していきます。

ファイルによる動的コンフィグレーション

Envoy Proxy のクラスターとリスナーの構成は cds_configlds_config で設定します。

Helm チャートのテンプレートファイル (spring-boot/spring-boot-demo/templates/configmap.yaml) では次のように設定しています。

    dynamic_resources:
      cds_config:
        path_config_source:
          path: /var/lib/envoy/cds/cds.yaml
      lds_config:
        path_config_source:
          path: /var/lib/envoy/lds/lds.yaml

このサンプルでは、構成ファイルのパスに hostPath を使ったローカルのファイルシステムをマウントするように設定しています。プロダクション環境の場合は、Amazon EFS 等をマウントして利用するとよいでしょう (「Amazon EKS で永続的ストレージを使用するにはどうすればよいですか?」)。

ルーティングを切り替える前に、まず spring-boot-demo のポッド名を取得して、環境変数POD_NAME に設定しておきます。

kubectl get pod

次のようにポッド一覧が表示されます。

NAME                                   READY   STATUS    RESTARTS   AGE
jp-spring-boot-demo-7b5454f8b7-c9vc5   2/2     Running   0          55m
spring-boot-demo-686f67b754-r7g4s      2/2     Running   0          51m

spring-boot-demo で始まるポッド名を環境変数に設定します。

export POD_NAME=spring-boot-demo-686f67b754-r7g4s

kubectl describe pod ポッド名 でポッドの情報を取得できます。そこから、このポッドに spring-boot-demoenvoy という名前の2つのコンテナが含まれていることがわかります。

まず、現在の cds.yaml を表示してみましょう。

kubectl exec $POD_NAME -c envoy -- cat /var/lib/envoy/cds/cds.yaml

resources:
  - "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster
    name: us
    load_assignment:
      cluster_name: us
      endpoints:
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: 127.0.0.1
                    port_value: 8081

この設定に、jp-spring-boot-demo を追加します。

その前に Envoy の管理画面を表示して確認してみましょう。以下のコマンドを実行して、ブラウザで http://localhost:8082 にアクセスしてください。

kubectl port-forward service/spring-boot-demo-admin 8082:80

clusters リンクをクリックすると現在のクラスタの情報が表示されます。

クラスターの変更

cds.yaml と同じ場所に、jp-spring-boot-demo を追加する cds2.yaml ファイルがあります。これを、cds.yaml に上書きしましょう。

kubectl exec $POD_NAME -c envoy -- mv -f /var/lib/envoy/cds/cds2.yaml /var/lib/envoy/cds/cds.yaml

jp クラスターが増えていることを確認できます。

上書きにより、次の cds.yaml に置換されています。

resources:
  - "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster
    name: us
    load_assignment:
      cluster_name: us
      endpoints:
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: 127.0.0.1
                    port_value: 8081
  - "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster
    name: jp
    type: LOGICAL_DNS
    dns_lookup_family: V4_ONLY
    load_assignment:
      cluster_name: jp
      endpoints:
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: jp-spring-boot-demo.default.svc.cluster.local
                    port_value: 80

Kubernetes のサービスにはそれぞれ IP アドレスが割り当てられ、また DNS 名も割り当てられます。jp-spring-boot-demo サービスの DNS 名は jp-spring-boot-demo.default.svc.cluster.local になります。

リスナーの変更

上と同様に、現在の lds.yaml を表示してみましょう。

kubectl exec $POD_NAME -c envoy -- cat /var/lib/envoy/lds/lds.yaml

resources:
  - "@type": type.googleapis.com/envoy.config.listener.v3.Listener
    name: listener_0
    address:
      socket_address:
        address: 0.0.0.0
        port_value: 8080
    filter_chains:
      - filters:
          - name: envoy.filters.network.http_connection_manager
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
              stat_prefix: ingress_http
              route_config:
                name: local_route
                virtual_hosts:
                  - name: local_service
                    domains:
                      - '*'
                    routes:
                      - match:
                          prefix: "/"
                        route:
                          cluster: us
              http_filters:
                - name: envoy.router
                  typed_config:
                    "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

そして、cds.yaml の場合と同様に lds2.yaml ファイルを lds.yaml に上書きします。

kubectl exec $POD_NAME -c envoy -- mv -f /var/lib/envoy/lds/lds2.yaml /var/lib/envoy/lds/lds.yaml

この変更により、下の図のように、spring-boot-demo の URL パスが /jp で始まる場合に jp-spring-boot-demo にルーティングされます。

次のコマンドでルーティングの変更を確認できます。

curl http://localhost:8080/jp

レスポンスとして、"Hello Japan" が表示されます。

上書きにより、次の lds.yaml に置換されています。

resources:
  - "@type": type.googleapis.com/envoy.config.listener.v3.Listener
    name: listener_0
    address:
      socket_address:
        address: 0.0.0.0
        port_value: 8080
    filter_chains:
      - filters:
          - name: envoy.filters.network.http_connection_manager
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
              stat_prefix: ingress_http
              route_config:
                name: local_route
                virtual_hosts:
                  - name: local_service
                    domains:
                      - '*'
                    routes:
                      - match:
                          prefix: "/jp"
                        route:
                          prefix_rewrite: "/"
                          cluster: jp
                      - match:
                          prefix: "/"
                        route:
                          cluster: us
              http_filters:
                - name: envoy.router
                  typed_config:
                    "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

まとめ

ここで確認したように、既存 API の一部を新しいサービスにルーティングするというアプローチで Strangler Fig Pattern を実現できます。

同様なアプローチで、Feature Toggles (Feature Flags) や A/B テスト に応用することもできます。

We're hiring!

みらい翻訳では、アーキテクチャに興味のある方や技術ブログを盛り上げていただけるエンジニアを募集しています! ご興味のある方は、ぜひ下記リンクよりご応募・お問い合わせをお待ちしております。

参考