Envoy Proxyを使ったファイルベースの動的コンフィグレーション
こんにちは、ed (edward) です。
この記事は、みらい翻訳のカレンダー | Advent Calendar 2022 - Qiita の17日目です。
さて、SaaS として提供している Mirai Translator のようなプロダクトの場合、原則として機能追加等のデプロイ、リリースをゼロダウンタイムで実施することが求められます。
また Strangler Fig Pattern を適用して追加や変更する機能を別サービスとしてデプロイし、最終的に全てのトラフィックが新しいサービスに流れるようにルーティングを切り替えて旧サービスを枯らせていく戦略で迅速かつ継続的なアーキテクチャ (Continuous Architecture) をドライブしたいと考えています。
Kubernetes 環境でこのような切り替えの方法はいくつかあり、AWS App Mesh や Istio のようなサービスメッシュを活用するケースがよく紹介されていますが、ここではサービスメッシュのサイドカーでよく使用されている 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 mini に Rancher Desktop をインストールした環境を使用します。
Amazon EKS と Persistent Volumes に EBS を利用する場合は「Envoy Proxyを使ったファイルベースの動的コンフィグレーション (EKS)」を参照してください。
そして、プロダクション環境で利用する場合は「Envoy Proxyを使ったファイルベースの動的コンフィグレーションとk8sのライフサイクル」を参照することをおすすめします。
ストーリー
次のような Strangler Fig Pattern を実現するストーリーをこれから説明します。
- 今後枯らせるサービスをイメージした
spring-boot-demo
サービスをデプロイします spring-boot-demo
の動作確認を実施します- 新サービスを想定した、
jp-spring-boot-demo
をデプロイします jp-spring-boot-demo
単体の動作確認を実施しますspring-boot-demo
API に/jp
パスを追加してjp-spring-boot-demo
にルーティングするように設定します
準備
この記事のコードは、GitHub リポジトリ にあります。最初にこれを clone します。
git clone https://github.com/takesection-sandbox/envoyproxy-examples.git
以降は、clone した envoyproxy-examples
の spring-boot
ディレクトリで操作を行なっていきます。
cd envoyproxy-examples/spring-boot
コンテナイメージのビルド
このビルドの前に Java 17 以降の JDK、Maven、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_config
と lds_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-demo
と envoy
という名前の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!
みらい翻訳では、アーキテクチャに興味のある方や技術ブログを盛り上げていただけるエンジニアを募集しています! ご興味のある方は、ぜひ下記リンクよりご応募・お問い合わせをお待ちしております。