おうちで学ぶサービスメッシュを支える透過型プロキシとしてのEnvoy

この記事は、NTT Communications Advent Calendar 2021 4日目の記事です。

こんにちは、イノベーションセンターでSREとして働いている昔農(@TAR_O_RIN)です。主にNTT Comのソフトウェアライフサイクルの改善への取り組みやアーキテクトに関わる仕事をしております。本日はサービスメッシュを題材に,その中で用いられるEnvoyの活用パターンを手を動かして理解するお話をさせていただきます。

また,昨年までのアドベントカレンダー記事もご興味があればご覧ください!

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つを研究題材にしていたのですが,初めてサービスメッシュを見た時はよく似たアーキテクチャだなと感じたことを覚えています。

f:id:NTTCom:20211204125431p:plain 引用元: https://istio.io/latest/docs/ops/deployment/architecture/

Envoyとは

本日の主役でございます。詳細なお話は公式ページに譲ることとして,下記の図で赤く示しているProxyの正体がEnvoyとなります。図中の緑の矢印から見て取れるように,数多くの通信を扱うコンポーネントであることが分かるかと思います。特にProxy間の通信を通してService AとService Bを繋いでいるような構成はサービスメッシュの文脈では多く登場します。

本日,具体的に深堀りしていくのはこのプロキシ間で通信するパターンをコントローラなしで作り上げてみて,実際にプロキシを介して通信が行えるのか,プロキシ間はどのように接続されているのか,をおうちのネットワークを活用して動かしてみましょう。

f:id:NTTCom:20211204125438p:plain 引用元: 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などが挙げられるでしょう。

f:id:NTTCom:20211204130926p:plain

mTLSによる暗号化と認証

もう1つ重要なポイントとしてプロキシ間の通信の暗号化と通信相手の認証があります。mTLSでは通信の暗号化にはTLSを用い,相互のプロキシ間の認証には証明書を用います。これにより今日のデモのようにインターネットを超えて通信を起こすような場合でも安全に通信を提供できます。mTLSについてもっと詳しく調べて見たい方はこちらのサイトが参考になるかと思います。

f:id:NTTCom:20211204130933p:plain

自宅とパブリッククラウドをEnvoyで接続してみよう

それでは実際に動作を確認する構成を作ってみましょう!下記の図のようにクライアント,サーバの双方でDockerコンテナを用いてプロキシを立てます。また,プロキシ間の通信にはIPv6を利用することとしました。この記事を読んでいる皆さん向け簡単に流れをご紹介します。

f:id:NTTCom:20211204130941p:plain

環境

サーバ
  • 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に利用する証明書を準備する(これが少し面倒ですが,手順書に沿って進めれば大丈夫です)
  1. 認証局(certificate authority)を作成する
  2. 接続に利用するドメイン向けの鍵を生成する
  3. 証明書署名要求(CSR)を生成する
  4. 証明書に署名する
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を導入して拠点を超えたアプリケーションを接続してみてください。

それでは、明日の記事もお楽しみに!

参考

© NTT Communications Corporation All Rights Reserved.