この記事は、NTT Communications Advent Calendar 2021 4日目の記事です。
こんにちは、イノベーションセンターでSREとして働いている昔農(@TAR_O_RIN)です。主にNTT Comのソフトウェアライフサイクルの改善への取り組みやアーキテクトに関わる仕事をしております。本日はサービスメッシュ
を題材に,その中で用いられるEnvoyの活用パターンを手を動かして理解するお話をさせていただきます。
また,昨年までのアドベントカレンダー記事もご興味があればご覧ください!
- 2020年 How do you like k3s ? - CoreDNSで作るお家DNS Cacheコンテナ
- 2019年 TektonでCI/CDパイプラインを手の内化しよう
- 2018年 DevOpsってこんな仕事!考え方とスキルセットのまとめ
- 2017年 DockerのnetworkをCalicoでBGP接続する
tl;dr
- サービスメッシュは便利だけど,本当に素敵なのはそのデータプレーンを支える透過型プロキシだと思っています!
The network should be transparent to applications
/ ネットワークはアプリケーションにとって透過的であるべき
- 実践的に組み上げられたサービスメッシュを使うのも良いけれど,透過型プロキシの1つである
Envoy
を利用してその面白さを知ろう- Double proxy with mTLS encryption というユースケースをインターネット越しで利用してみます
サービスメッシュにおけるEnvoyの役割
サービスメッシュとは
今回の記事ではサービスメッシュ自体の説明を詳しくは実施しませんが,どんな問題を解決しようとしている仕組みなのか簡単にご紹介します。基本的には複数のマイクロサービスを用いて構築されたシステムにおいて,マイクロサービス間の通信をスマートにハンドリングするための概念及びそのソフトウェアのことです。
下記の図はサービスメッシュ実装の1つであるIstioのアーキテクチャ図です。ここで概念として理解しておきたいのは,サービスメッシュはマイクロサービスのトラフィックを転送するプロキシ(データプレーン)
と,それらを管理するコントローラ(コントローラプレーン)
に役割が分かれていることです。この概念が サービスメッシュ
であり,その実装が Istio
などの具体的な製品やOSSになります。
少し本題と外れますがネットワークの業界にある程度いると,データプレーン
とコントローラプレーン
を分離するという考え方はSDNの概念が生まれた頃からよく登場していたと記憶しています。私は学生時代にOpenFlowというSDNの1つを研究題材にしていたのですが,初めてサービスメッシュを見た時はよく似たアーキテクチャだなと感じたことを覚えています。
引用元: https://istio.io/latest/docs/ops/deployment/architecture/
Envoyとは
本日の主役でございます。詳細なお話は公式ページに譲ることとして,下記の図で赤く示しているProxyの正体がEnvoy
となります。図中の緑の矢印から見て取れるように,数多くの通信を扱うコンポーネントであることが分かるかと思います。特にProxy間の通信を通してService AとService Bを繋いでいるような構成はサービスメッシュの文脈では多く登場します。
本日,具体的に深堀りしていくのはこのプロキシ間で通信するパターンをコントローラなしで作り上げてみて,実際にプロキシを介して通信が行えるのか,プロキシ間はどのように接続されているのか,をおうちのネットワークを活用して動かしてみましょう。
引用元: https://istio.io/latest/docs/ops/deployment/architecture/
インターネット越しでDouble proxyをやってみる
Double Proxyについて
Double proxyとはサービスメッシュとしてよく出てくるEnvoyの利用パターンの1つで,下記のようにアプリケーション間でEnvoyをサイドカーとして組み込むことでマイクロサービス間や下記の図のようなアプリケーションとミドルウェアとの間を接続する構成です。この構成を作ることでFlaskApp
はあたかも隣にPostgreSQL
がいるように利用できます。
今回はサンプル例として1対1の接続例となりますが,実際にサービスメッシュとして利用する際はもっと複雑な構成になることが多いでしょう。ミドルウェアへの接続として応用している実例としてはGCPにおけるCloud SQL Auth proxyなどが挙げられるでしょう。
mTLSによる暗号化と認証
もう1つ重要なポイントとしてプロキシ間の通信の暗号化と通信相手の認証があります。mTLSでは通信の暗号化にはTLSを用い,相互のプロキシ間の認証には証明書を用います。これにより今日のデモのようにインターネットを超えて通信を起こすような場合でも安全に通信を提供できます。mTLSについてもっと詳しく調べて見たい方はこちらのサイトが参考になるかと思います。
自宅とパブリッククラウドをEnvoyで接続してみよう
それでは実際に動作を確認する構成を作ってみましょう!下記の図のようにクライアント,サーバの双方でDockerコンテナを用いてプロキシを立てます。また,プロキシ間の通信にはIPv6を利用することとしました。この記事を読んでいる皆さん向け簡単に流れをご紹介します。
環境
サーバ
- OS: Ubuntu 20.04.3 LTS
- Kernel Version: 5.4.0-90-generic
- Docker Server Version: 20.10.7
- Envoy: v1.20.1
※ 諸般の事情でネットワーク帯域幅が100Mbps制限。
クライアント
- OS: Ubuntu 18.04.3 LTS
- Kernel Version: 5.4.0-80-generic
- Docker Server Version: 20.10.7
- Envoy: v1.20.1
構築の流れ
Envoyから素敵な公式手順書があるので, 基本的にはそれに沿って進めますが一部詰まったところや気になったところを補足していきます。
mTLSに利用する証明書を準備する(これが少し面倒ですが,手順書に沿って進めれば大丈夫です)
- 認証局(certificate authority)を作成する
- 接続に利用するドメイン向けの鍵を生成する
- 証明書署名要求(CSR)を生成する
- 証明書に署名する
Envoy Configの準備については公式リポジトリのサンプルコードを参考に編集していきます
Client側の参考Envoy Configを下記に示します。
static_resources: listeners: - name: iperf_listener address: socket_address: address: 0.0.0.0 port_value: 12345 # プロキシで通信を受け付けるポート番号なので任意で良い filter_chains: - filters: - name: envoy.filters.network.tcp_proxy typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy stat_prefix: iperf_tcp cluster: iperf_cluster clusters: - name: iperf_cluster type: STRICT_DNS connect_timeout: 10s load_assignment: cluster_name: iperf_cluster endpoints: - lb_endpoints: - endpoint: address: socket_address: address: <ご自身で取得したサブドメイン>.i.open.ad.jp # IPv6のアドレスを直に書くとエラーになるようなのでFQDNを書く port_value: 3022 # 対向のEnvoyとの通信に利用するポートなので任意で良い transport_socket: name: envoy.transport_sockets.tls typed_config: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext common_tls_context: tls_certificates: - certificate_chain: filename: certs/clientcert.pem # 証明書をEnvoyコンテナにマウントすることを忘れずに private_key: filename: certs/clientkey.pem # 秘密鍵をEnvoyコンテナにマウントすることを忘れずに validation_context: match_subject_alt_names: - exact: edge-proxy-server01.sekinet.example # ご自身で利用するサブジェクトALT名を入れてください trusted_ca: filename: certs/cacert.pem # CA CertsをEnvoyコンテナにマウントすることを忘れずに
サーバ側の参考Envoy Configを下記に示します。
static_resources: listeners: - name: iperf_server_listener address: socket_address: address: <EnvoyでLISTENするアドレス> # ここはIPv6アドレスを入れても大丈夫 port_value: 3022 # 対向のEnvoyとの通信に利用するポートなので任意で良い listener_filters: - name: "envoy.filters.listener.tls_inspector" filter_chains: - filters: - name: envoy.filters.network.tcp_proxy typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy stat_prefix: iperf_tcp cluster: iperf3_server_for_client01_cluster transport_socket: name: envoy.transport_sockets.tls typed_config: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext require_client_certificate: true common_tls_context: tls_certificates: - certificate_chain: filename: certs/servercert.pem # 証明書をEnvoyコンテナにマウントすることを忘れずに private_key: filename: certs/serverkey.pem # 秘密鍵をEnvoyコンテナにマウントすることを忘れずに validation_context: match_subject_alt_names: - exact: edge-proxy-client01.sekinet.example # ご自身で利用するサブジェクトALT名を入れてください trusted_ca: filename: certs/cacert.pem # CA CertsをEnvoyコンテナにマウントすることを忘れずに clusters: - name: iperf3_server_for_client01_cluster type: static connect_timeout: 10s load_assignment: cluster_name: iperf3_server_cluster endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 172.17.0.2 # ローカルで立ち上げたiperfのアドレス port_value: 5201 # ローカルで立ち上げたiperfのLISTENポート
※ IPv6アドレスのエンドポイントをFQDNで引けるようにするためOPEN IPv6 ダイナミック DNS for フレッツ・光ネクストを利用してます。
Envoyプロキシの立ち上げ
色々な方式がありますが, 基本的には立ち上げ時にEnvoyConfigと証明書に関連するファイルを正しくEnvoyに与えることが出来れば良いです。私の場合は,一度公式のEnvoyコンテナをラップするコンテナをビルドしていますが,同じことができれば公式のEnvoyコンテナをそのまま利用しても構いません。
Client側 Scriptで起動時にEnvoyConfigや証明書関連のファイルをマウントしています。
#!/usr/bin/env bash docker run -d --restart unless-stopped \ --net=host \ --volume $(pwd)/envoy.yaml:/etc/envoy.yaml:ro \ --volume $(pwd)/envoy-certs/ca.crt:/certs/cacert.pem:ro \ --volume $(pwd)/envoy-certs/edge-proxy-client01.sekinet.tokyo.crt:/certs/clientcert.pem:ro \ --volume $(pwd)/envoy-certs/edge.sekinet.example.key:/certs/clientkey.pem:ro \ --name sekinet-edge-gateway-envoyproxy \ sekinet/edge-gateway-envoyproxy
Server側 Scriptで起動時にEnvoyConfigや証明書関連のファイルをマウントしています。
#!/usr/bin/env bash docker run -d --restart unless-stopped \ --net=host \ --volume $(pwd)/envoy-backend.yaml:/etc/envoy.yaml:ro \ --volume $(pwd)/envoy-certs/ca.crt:/certs/cacert.pem:ro \ --volume $(pwd)/envoy-certs/edge-proxy-server01.sekinet.tokyo.crt:/certs/servercert.pem:ro \ --volume $(pwd)/envoy-certs/edge.sekinet.example.key:/certs/serverkey.pem:ro \ --name sekinet-edge-gateway-envoyproxy \ sekinet/edge-gateway-envoyproxy
※ 実際にはiper3もDockerで起動していますがここでは割愛します。
iperf3で計測してみる
クライアント側でEnvoyプロキシに対してiperf3を走らせます。ポート番号はEnvoyの設定ファイルで指定している番号になるので,必ずしもiperf3 Server側と同じポートである必要はありません。下記の結果では,100Mbps上限の環境下で十分なスループットが出ていることが観測されました。また,少なくとも100Mbps程度ではEnvoyのDouble proxyはボトルネックにはならないという結果が得られました。
sekinet@sekinet:~/envoy-mtls$ iperf3 -c 127.0.0.1 -p 12345 Connecting to host 127.0.0.1, port 12345 [ 4] local 127.0.0.1 port 57018 connected to 127.0.0.1 port 12345 [ ID] Interval Transfer Bandwidth Retr Cwnd [ 4] 0.00-1.00 sec 23.7 MBytes 198 Mbits/sec 14 4.56 MBytes [ 4] 1.00-2.00 sec 10.9 MBytes 91.2 Mbits/sec 6 4.56 MBytes [ 4] 2.00-3.00 sec 11.7 MBytes 98.5 Mbits/sec 7 4.56 MBytes [ 4] 3.00-4.00 sec 10.4 MBytes 87.5 Mbits/sec 10 4.56 MBytes [ 4] 4.00-5.00 sec 11.9 MBytes 99.5 Mbits/sec 11 4.56 MBytes [ 4] 5.00-6.00 sec 11.5 MBytes 96.4 Mbits/sec 8 4.56 MBytes [ 4] 6.00-7.00 sec 10.8 MBytes 90.6 Mbits/sec 10 4.56 MBytes [ 4] 7.00-8.00 sec 11.7 MBytes 98.5 Mbits/sec 11 4.56 MBytes [ 4] 8.00-9.00 sec 11.1 MBytes 92.7 Mbits/sec 9 4.56 MBytes [ 4] 9.00-10.00 sec 10.9 MBytes 91.7 Mbits/sec 7 4.56 MBytes - - - - - - - - - - - - - - - - - - - - - - - - - [ ID] Interval Transfer Bandwidth Retr [ 4] 0.00-10.00 sec 125 MBytes 105 Mbits/sec 93 sender [ 4] 0.00-10.00 sec 112 MBytes 93.9 Mbits/sec receiver iperf Done.
また,自宅の別のノードからクライアントプロキシに対して計測しても同様の結果が得られました。
[Mac]:~/ iperf3 -c 192.168.1.254 -p 12345 Connecting to host 192.168.1.254, port 12345 [ 5] local 192.168.1.69 port 64173 connected to 192.168.1.254 port 12345 [ ID] Interval Transfer Bitrate [ 5] 0.00-1.00 sec 16.5 MBytes 139 Mbits/sec [ 5] 1.00-2.00 sec 14.4 MBytes 121 Mbits/sec [ 5] 2.00-3.00 sec 11.4 MBytes 95.1 Mbits/sec [ 5] 3.00-4.00 sec 11.8 MBytes 99.3 Mbits/sec [ 5] 4.00-5.00 sec 11.3 MBytes 94.9 Mbits/sec [ 5] 5.00-6.00 sec 11.0 MBytes 92.1 Mbits/sec [ 5] 6.00-7.00 sec 11.2 MBytes 93.8 Mbits/sec [ 5] 7.00-8.00 sec 11.0 MBytes 91.9 Mbits/sec [ 5] 8.00-9.00 sec 11.9 MBytes 100 Mbits/sec [ 5] 9.00-10.00 sec 10.9 MBytes 91.5 Mbits/sec - - - - - - - - - - - - - - - - - - - - - - - - - [ ID] Interval Transfer Bitrate [ 5] 0.00-10.00 sec 121 MBytes 102 Mbits/sec sender [ 5] 0.00-10.01 sec 112 MBytes 93.8 Mbits/sec receiver
まとめ
今回はサービスメッシュを切り口に透過型プロキシであるEnvoyを使ってインターネット越しでアプリケーション間を接続する構成を試しました。このような取り組みはサービスメッシュを最初に触るとコントローラによって隠蔽してくれているペインポイントが如実に現れます。例えば証明書の管理やEnvoyConfigの管理,動的なUpdateなどはその最たる例でしょう。この経験を通してコントローラ側に何が求められるか,どんな機能が追加されていくのかを追いかけると少し違った目線でサービスメッシュという技術を楽しめるのではないでしょうか。
また,アーキテクチャとしても従来は拠点間をIPSECやWireguardのようなトンネル型VPNで接続する構成が多かったですが,要件によっては透過型プロキシやサービスメッシュを広域に展開するようなユースケースも選択肢として現れるかもしれません。ぜひ皆さんもおうちネットワークにEnvoyを導入して拠点を超えたアプリケーションを接続してみてください。
それでは、明日の記事もお楽しみに!