Envoyでサービスメッシュを作ろう

# tl; dr
- [Envoy](https://www.envoyproxy.io/)と[go-control-plane](https://github.com/envoyproxy/go-control-plane)を利用すれば、そこそこ手軽にサービスメッシュを作ることができます。
- ソースコードとサンプルはこちらにあります。
    - meshem: https://github.com/rerorero/meshem

サービスメッシュとは
================
サービスメッシュは、マイクロサービスが抱える問題を解決するためのアーキテクチャのひとつです。2017年に[Linkerd](https://linkerd.io/)を開発しているBuoyant社のCTOが使うようになり、広まったとされています。

- [What's a service mesh? And why do I need one?](https://buoyant.io/2017/04/25/whats-a-service-mesh-and-why-do-i-need-one/)
- [Pattern: Service Mesh](http://philcalcado.com/2017/08/03/pattern_service_mesh.html)

２つ目の記事にあるように、言葉自体は新しいものの、その考え方は突然登場したというわけではなく、以前より各社が試行錯誤してきてたどり着いた一つのアプローチであり、実践や知見に基づいた問題解決手法であるということがわかります。

### マイクロサービスが抱える問題

マイクロサービスがエンジニアにもたらした恩恵には様々なものがあります。

- スケーラビリティ
- 変更に対する柔軟性、CI/CD
- 問題解決に適した言語・フレームワーク・ツールの選択
- etc..

一方でサービスが分断され、ネットワークを経由して通信することにより新たな問題と直面することになります。

- サービスの発見（サービスディスカバリー）
    - サービスは通常、可用性や負荷分散のために複数のホストで構成されます。どのホストと通信すべきかを管理することはマイクロサービスで重要な課題となりました。サービス間の依存関係の解決は一般的にDNS、ロードバランサーを利用して実装されます。開発者はホストの構成を変更したり、デプロイやローリングリスタートのたびに、これらを制御する必要があります。近年はKubernetesのようなコンテナマネジメントプラットフォームがその機能を提供していたりします。
- カスケード障害のリスク
    - とあるサービスで発生した障害が依存するサービスに連鎖的に波及し、大規模な障害につながるリスクがあります。レートリミットはサービスのキャパシティが超過することを防ぎ、サーキットブレーカーは異常なホストへのアクセスを遮断しカスケード障害のリスクを減らすのに役立ちます。しかしサーキットブレーカーは各サービスの外部呼び出し箇所に実装しなければならず、実装や多様な言語のサポートにはコストがかかります。
- 監視・デバッグ
    - サービス単位での監視は容易になりましたが、監視方法がばらばらだと、システム全体を俯瞰して監視することは難しくなります。どのサービスがダウンしているかをすばやく把握するには、各サービスがサービスのエンドポイントを監視している必要があります。またサービスをまたいでのデバッグが必要となるケースも増えます。分散トレーシングは横断的にリクエストを追跡する手段として有効ですが、一般的には各サービスでトレース情報を送信するなどの実装コストがかかります。
- セキュリティ
    - ネットワークの暗号化、認証・認可の機能が必要です。

### サイドカープロキシ

サービスメッシュは、これらの課題をサイドカープロキシを用いて解決しようとするものです。

![6-b.png](https://qiita-image-store.s3.amazonaws.com/0/66677/27f995ed-b96f-ab18-4a68-ff99ff5fba8d.png)
*「Pattern: Service Mesh」 http://philcalcado.com/2017/08/03/pattern_service_mesh.html より引用*

### データプレーン

各サービスは、サービスのプロセスと対になるように立てられたプロキシサーバーを介してやりとりを行います。これらのプロキシのことを総称してデータプレーンと呼びます。データプレーンは、外部からAPI等でのオンライン制御が可能な「Intelligent」なプロキシが用いられます。各ホストへの負荷分散の他に、サーキットブレーカー、レートリミット、メトリクスの収集、リトライ処理などを行います。

### コントロールプレーン

データプレーンを制御するコンポーネントはコントロールプレーンと呼ばれます。コントロールプレーンはデータプレーンをどのように制御するかの責務を負っています。サービスの依存関係を永続化したり、あるいはKubernetes等の外部プラットフォームから得られる情報を元に、サイドカープロキシを柔軟に制御します。

[Envoyの中の人のこちらの記事](https://blog.envoyproxy.io/service-mesh-data-plane-vs-control-plane-2774e720f7fc) によると、データプレーンのプロキシとして　Linkerd、Envoy、Traefik、NGINX、HAProxyが紹介されています。コントロールプレーンはIstioの他にNelsonとsynapse（Airbnb）が紹介されています。後者二つはサービスメッシュという言葉が登場する以前からあるようです（Airbnbは[Pattern: Service Mesh](http://philcalcado.com/2017/08/03/pattern_service_mesh.html)でも登場します）。

サイドカープロキシを導入する上での一つの壁はプロキシのデプロイにあります。各サービスで採用されているフレームワークごとにデプロイ方法が異なるため、アドホックな対応が必要になりコストがかかります。KuberentesやNomadのようなプラットフォームに依存し、デプロイの機能と連携することで、プロキシの導入は容易になります。Istioも当初はKubernetesだけをターゲットにしていました（今後他のプラットフォームも提供される予定です）。

今注目を集めているIstioは、[様々なプラットフォームと連携できるようプラガブルに作られていて、またアプリケーションの透過性を損なわないことを設計デザインに掲げています](https://istio.io/docs/concepts/what-is-istio/goals.html)。アプリケーションのコードをそのままに、Istioの導入や、導入後のプラットフォームの変更を容易にすることが目的です。データプレーンにはデフォルトではEnvoyが使われますが、ここも拡張可能になっているようで、[Nginx](https://github.com/nginmesh/nginmesh)や[Linkerdを利用する](https://linkerd.io/getting-started/istio/)取り組みもあるようです。


# サービスメッシュを作ろう

この記事を書いている時点（2018年3月時点）では[Istioの一部機能はまだプロダクションレディではありません](https://istio.io/about/feature-stages.html)。また私の携わっているプロジェクトでは当面コンテナマネジメントの導入の予定はなさそうなため、今回はIstioやKubernetesを利用せずにサービスメッシュを構築する方法について調べました（なお[Istioはベアメタルでの導入も可能なようです](https://istio.io/docs/setup/consul/install.html))。
データプレーンにはIstioでも利用されているEnvoyを使い、コントロールプレーン部分を作ってみました。

### Envoyとは

[Envoy](https://www.envoyproxy.io/)はLyft社が[社内でサービスメッシュを構築するために開発した](https://eng.lyft.com/announcing-envoy-c-l7-proxy-and-communication-bus-92520b6c81910)プロキシサーバーで、以下のような機能を持っています。

- 様々なプロトコルをサポート
    - TCP, TLS, HTTP1.1, HTTP/2, gRPC に加え、 MongoDB, DynamoDB, Redis などもサポートしています。UDPも今後サポートされるようです。
- ロードバランシング
    - ヘルスチェック、外れ値検出、レートリミット、サーキットブレーカー、自動リトライといった機能が利用できます。
    - [シャドーイング](https://www.envoyproxy.io/docs/envoy/latest/api-v1/route_config/route.html#config-http-conn-man-route-table-route-shadow)という機能があり、Envoyを流れるリクエストの複製を別のホストに流すことが可能です。ホストからのレスポンスは捨てられるため、[例えば本番環境で実際に流れるリクエストを使ってデプロイ前にテストを行うことができ、](http://blog.christianposta.com/microservices/advanced-traffic-shadowing-patterns-for-microservices-with-istio-service-mesh/)本番へのデプロイのリスクを下げるといった使い方ができます。
- サービスディスカバリー
    - 設定ファイルによる静的な設定に加え、RESTまたはgRPCのAPIによるサービスディスカバリーが可能です
- モニタリング
    - Zipkinなどの分散トレーシングに対応しています。
    - メトリクスのエクスポートも行います。
- ホットリスタート

より詳しくは[公式のページ](https://www.envoyproxy.io/docs/envoy/latest/intro/what_is_envoy)を参照してください。


## xDS Protocol

コントロールプレーンではxDS APIというものを利用して、Envoyを動的に制御します。

xDS APIはコントロールプレーン側が提供するもので、gRPCのbidiストリーム、またはロングポーリングのRESTで実装します。APIはEnvoyからの問い合わせに対して、コントロールプレーンがどのような設定（ListenerやRouteなど）をすればよいかのレスポンスを返す、という形式になっています。これだけ聞くと単純なようですが、コントロールプレーン側は可用性のため複数存在していたり、通信に失敗しリクエストが再送される可能性があるため、リソースごとのバージョンや、Envoyがどのレスポンスを受け入れたかを示すためのnonceを送信しあっています。またレスポンスを返す必要がない時（設定に変更ない時など）はレスポンスを返さないという制御が必要になります。この辺りの仕様は[xDS Protocol](https://github.com/envoyproxy/data-plane-api/blob/master/XDS_PROTOCOL.md#aggregated-discovery-services-ads)としてまとめられています。

これを実装するのはやや骨が折れますが、幸いなことにEnvoyの公式Orgでこの辺りのリファレンス実装を公開してくれています。Go版とそれを移植したJava版があります。

- [go-control-plane](https://github.com/envoyproxy/go-control-plane)
- [java-control-plane](https://github.com/envoyproxy/java-control-plane)

これを利用すればお手軽にxDS APIが実装できます。

# サービスメッシュを作ろう

[go-control-plane](https://github.com/envoyproxy/go-control-plane)を使ってシンプルな実装のコントロールプレーンを作りました。ソースコードとサンプル一式を以下のリポジトリに置いています。

- meshem: https://github.com/rerorero/meshem

構成は以下の図のようになっています（作成したのは黄色い `meshem` という部分だけです）

![Untitled presentation-5.png](https://qiita-image-store.s3.amazonaws.com/0/66677/bd1f4fe4-66c4-ce74-e05e-21a5161e5f87.png)

- `xDS Server` はEnvoyからリクエストを受け付けるxDS APIサーバーです
- `API Server` はホストの登録情報を更新するためのAPIを提供するサーバーです
    - xDS ServerとAPI Serverは一つのプロセス(`meshem`)で実行されています。
- `meshemctl` はAPI Serverを叩くCLIツールです。APIを直接叩いてもいいので必須ではありません。
- `Consul` データプレーンのホストやサービスの登録情報を保存するKVSストアとして利用します。またCatalogにホストを登録することでConsulのディスカバリー機能を利用可能にします。
- `Prometheus`,`Zipkin` これらはオプションです。メトリクス収集とトレースの記録を行います。

このコントロールプレーンを使ってサンプルアプリケーションを動かす環境も用意しています。このサンプルでは基本的なディスカバリーの実装と、メトリクス・トレーシングの設定だけを行っています。サンプルの動作環境はVagrant+AnsibleとDocker環境の２つをを用意しています。

### xDS Server 実装

xDS API（=サービスディスカバリ）の部分は前述した[go-control-plane](https://github.com/envoyproxy/go-control-plane)を利用します。Envoyは、Listner,Cluster,Routeなどいくつかの設定ごとに、コントロールプレーンに対し、取得を要求するリクエストを投げてきます。go-control-planeはそれを受け付けるサーバーを立てて、ヒープのキャッシュの内容から応答します。go-control-planeを利用するには

 1. サーバーを立てる
 2. 更新する必要があったら[キャッシュ](https://github.com/envoyproxy/go-control-plane/blob/89abfa00ef3cae559f2739ce9e1f181ffc7caf92/pkg/test/main/main.go#L122)を更新する

ということをするだけです。サンプルコードでは以下の箇所になります

- https://github.com/rerorero/meshem/blob/0.1.1/src/core/xds/server.go#L60-L81
- https://github.com/rerorero/meshem/blob/0.1.1/src/core/xds/server.go#L117

Envoyに設定したい内容はConsulに永続化しておき、定期的にgo-control-planeのキャッシュに設定するようにしています。キャッシュ更新の際、設定のバージョンがいくつであるかを一緒に渡すのですが、go-control-planeは同じバージョンで更新された場合は以前と変更がないと認識し、Envoyへレスポンスを返しません。バージョンが更新されるとEnvoyへレスポンスを返し、受け取ったEnvoyは設定の適用を行いますが、この処理は頻繁に行うには重い処理のため、必要があるときだけバージョンを更新する必要があります。

### API Server / CLI 実装

Envoyに設定したい情報をConsulのKVストアに保存するためのHTTP APIです。CLIはHTTP APIのエンドポイントを叩いているだけです。新たにホスト情報を登録した場合には、KVストアに保存するだけでなく[Catalogにも登録するということをしています](https://github.com/rerorero/meshem/blob/0.1.1/src/core/inventory.go#L441)。これにより、Consulのディスカバリー機能を利用できるPrometheusが、自動でメトリクスの収集を開始することができます。

GUIも作りたかったのですが時間とやる気が足りませんでした :disappointed: 

### Envoyの設定

Envoyはコントロールプレーンへアクセスできるように設定して起動する必要があります。以下は設定ファイルの例です。

```
admin:
  access_log_path: "/dev/null"
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 8001

dynamic_resources:
  lds_config:
    api_config_source:
      api_type: GRPC
      cluster_names: [xds_cluster]
  cds_config:
    api_config_source:
      api_type: GRPC
      cluster_names: [xds_cluster]

static_resources:
  clusters:
  - name: xds_cluster
    type: STATIC
    connect_timeout: 10s
    http2_protocol_options: {}
    hosts:
    - socket_address:
        address: 10.2.0.2
        port_value: 8090
```
 - `static_resources.clusters[0].socket_address` にxDS APIのエンドポイントを指定します。複数ホストの指定も可能です
 - [Ansibleサンプルの設定](https://github.com/rerorero/meshem/blob/0.1.1/examples/ansible/roles/envoy/templates/envoy-conf.yaml.j2) では`tracing`の設定も追加しています。今のところ`tracing`の設定はまだxDS APIでは設定できません。

もう一つ大事な設定としてEnvoyの起動時のオプションに渡す `--service-node` があります。Envoyは、自分がどのノードであるかを示すため`--service-node`で指定した名前をリクエストに含めてxDS APIを叩きます。go-control-planeのキャッシュは`map`になっていて、キーにこの値を用いるため、どのノードにどの値を返すかを制御する際にこの値を利用することになります。

### アプリケーションコードの変更

サービスメッシュの利点として、アプリケーションコードを変更せずに透過的に導入できるという点があります。しかしKuberentesなどを利用しない場合は、最低限外部API呼び出しの部分をEnvoyを通すように変える必要があります。サンプルの動作環境ではAnsibleの変数 [front_app_endpoint](https://github.com/rerorero/meshem/blob/0.1.1/examples/ansible/group_vars/vagrant.yml#L13) で[書き換えています](https://github.com/rerorero/meshem/blob/0.1.1/run.sh#L24)。

また必須ではありませんが、分散トレーシングを行いたい場合は、トレースIDなどのHTTPヘッダを依存先のサービスに伝搬させる処理が必要になります。[今回のサンプルではこの部分](https://github.com/rerorero/meshem/blob/0.1.1/examples/ansible/roles/front/templates/front.go.j2#L26-L33)。トレースIDの生成やzipkinへの送信はEnvoyが行ってくれます。トレースが不要な場合はこれを行わなくても構いません（サービスメッシュの動作には影響ありません）。


作ったコントロールプレーンでサービスメッシュを動かす
=======

### サンプル環境の構築

Vagrant+Ansibleのサンプルを用意しています。実行には VirtualBox, Vagrant, Go, python+pip が必要です。

```
go get github.com/rerorero/meshem
cd $GOPATH/src/github.com/rerorero/meshem
./run.sh
```

Ansibleのプレイブックは[コントロールプレーン用](https://github.com/rerorero/meshem/tree/0.1.1/ansible)と[データプレーン用](https://github.com/rerorero/meshem/tree/0.1.1/examples/ansible) で別れています。 `./run.sh` はこれらをまとめて実行し、以下のような構成でアプリケーションをデプロイします。サンプルアプリケーションは、2つのホストで負荷分散されている `app` API サービスと、それを呼び出す1台の `front` の２種類のサービスで構成されれています。
![Untitled presentation-4.png](https://qiita-image-store.s3.amazonaws.com/0/66677/dea5e54c-ad60-163e-9618-9670562d5d4c.png)

### リクエストを送ってみる

```
$curl 192.168.34.70
front: app response = {"msg": "app info", "from" : "myapp1"}
$curl 192.168.34.70
front: app response = {"msg": "app info", "from" : "myapp2"}
$curl 192.168.34.70
front: app response = {"msg": "app info", "from" : "myapp1"}
```
`front`のEnvoyにリクエストを何回か投げると、appがラウンドロビンしていることが確かめられます。

メトリクスが収集されているか確認してみます。Prometheusで収集したメトリクスはGrafanaで確認できます。
 http://192.168.34.62:3000/
![スクリーンショット 2018-03-23 16.11.22.png](https://qiita-image-store.s3.amazonaws.com/0/66677/e0efa58e-772a-6445-ee50-90b3a0af545f.png)

次にトレースの動作を確認します。 Zipkin は http://192.168.34.62:9411/ で動いています。
![スクリーンショット 2018-03-23 16.11.51.png](https://qiita-image-store.s3.amazonaws.com/0/66677/735ac60f-7da4-45f6-435f-219cabae3190.png)

### 動的な変更

appサービスを2台から1台はずし、1台構成にしてみます。`meshemctl`(CLI)は[releaseページ](https://github.com/rerorero/meshem/releases) からダウンロードするか、 `go install github.com/rerorero/meshem/src/meshemctl` でインストールできます。

CLIはいまのところ、下記のコマンド1つだけしか実行できません :rolling_eyes: 

- `meshemctl svc apply <サービス名> -f <ファイル名>`

このコマンドはファイルの内容に従いサービスの状態（どんなプロトコルを使っているか、どんなホストが存在するか）をべき等に設定します。ファイルはyamlで記述します。

```
# meshemの設定値を書き換えます
vi ./examples/ansible/meshem-conf/app.yaml
```
```
protocol: HTTP
# サービスのすべてのホストを定義します。これらはラウンドロビンで分散されます
hosts:
  - name: myapp1
    # 外部サービスからの入力を受け付けるアドレストポートを指定します
    ingressAddr:
      host: 192.168.34.71
      port: 80
    # アプリケーションのポートを指定します（サイドカープロキシからの参照に利用されるため、127.0.0.1となっている）
    substanceAddr:
      host: 127.0.0.1
      port: 9000
    egressHost: 127.0.0.1
  # myapp2をコメントアウトしました
  # - name: myapp2
  #   ingressAddr:
  #     host: 192.168.34.72
  #     port: 80
  #   substanceAddr:
  #     host: 127.0.0.1
  #     port: 9000
  #   egressHost: 127.0.0.1
```

```
# 環境変数でCLIにAPIエンドポイントを知らせます
export MESHEM_CTLAPI_ENDPOINT="http://192.168.34.61:8091"
meshemctl svc apply app -f ./examples/ansible/meshem-conf/app.yaml
```
成功すれば `ok (Changed=true)` と表示されます。もう一度`front`を叩いてみると、`myapp1`からのレスポンスしか返さないことが確認できます。

```
$ curl 192.168.34.70
front: app response = {"msg": "app info", "from" : "myapp1"}
$ curl 192.168.34.70
front: app response = {"msg": "app info", "from" : "myapp1"}
$ curl 192.168.34.70
front: app response = {"msg": "app info", "from" : "myapp1"}
```

# 最後に

今回作ったものはEnvoyの機能のごくわずかしか利用していませんが、それでもDNS・ロードバランサーに依存せず、CLIによるロードバランシングの設定変更が可能になりました。またアプリケーションのわずかな修正でメトリクスの収集や分散トレースができるようになりました。

xDS APIで返す値をもっと充実させ柔軟に変えていくことで、Envoyの機能をさらに引き出すことができます。小さい機能から初めて、それぞれのシステムが抱える問題に応じコントロールプレーンを発展させていくという導入の仕方もできそうです。

Istioには今後期待していますが、Istio自体多機能でコードベースも大きいものですので、運用を考えた時に、自分たちのシステムに合わせてコントロールプレーンを開発するという方法も検討の余地はありそうだなと思いました。