Try Envoy: その1 Getting Started with Envoy をやってみた

# はじめに
現在 Kubernetes を本番運用中ですが、まだサービス数が少ないのでサービスメッシュ化はしていません。今後サービス数が増えてくるとサービスメッシュ化を検討したほうがよいらしいです。
サービスメッシュ化するためのサービスとして、`Istio`、`AWS app mesh`などがありますが、これらは`envoy`という proxy をコントロールするための機能であり、実際に通信を制御しているのは enovy のようです。そのため、まずは素の`envoy`がどのようなことができるかを勉強していきたいと思います。


Envoy のサイトに `Try Envoy` という学習コンテンツがあるので、これで勉強を進めていきたいと思います。

Try Envoy は、ブラウザベースで進められる Katacoda の学習コンテンツが埋め込まれているものになります。
Try Envoyでは Chome 翻訳が何故か効かなかったのですが、katacoda を直接使用すると Chome 翻訳できたのでオススメです。

https://www.katacoda.com/envoyproxy/scenarios/getting-started

では今回は `Getting Started with Envoy`を進めていきたいと思います。

# Getting Started with Envoy
## 1. Create Proxy Config
Envoy はプロキシの動作を制御するために YAML 定義ファイルを使用して設定します。このステップでは、静的構成 API を使用して設定します。これはすべての設定が定義ファイルで事前定義されていることを意味します。

※Envoy は動的構成もサポートしています。これにより、外部ソースを介して設定を検出することもできます。

### 1-1. Resources
conf の最初の行は、使用される API 構成を定義します。今回は静的 API を設定したいので、最初の行は`static_resources`である必要があります。

```yaml
static_resources:
```

### 1-2. Listeners
次はリスナーを定義します。リスナーは Envoy がリクエストをリッスンするIPアドレスやポートなど、ネットワークに関する設定です。Envoy は Docker コンテナー内で実行されるため、IPアドレス`0.0.0.0`でリッスンする必要があります。下記設定では、Envoy は `port 10000`でリッスンします。

```yaml
listeners:
  - name: listener_0
    address:
      socket_address: { address: 0.0.0.0, port_value: 10000 }
```

### 1-3. Filter Chains and Filters
次はリクエストの処理方法を定義します。各リスナーにはフィルターのセットがあり、リスナーごとに異なるフィルターのセットを持つことができます。

この例では、すべてのトラフィックをGoogle.comにプロキシします。そのため Envoy エンドポイントをリクエストすると、URL がEnvoyエンドポイントのまま、Googleホームページが表示されるはずです。
(リダイレクトではなくリバースプロキシされている)

フィルタリングは`filter_chains`を使用して定義されます。各フィルターの目的は、リスクエスト要求に一致するものを見つけ、それをターゲット宛先に一致させることです。

```yaml
    filter_chains:
    - filters:
      - name: envoy.http_connection_manager
        config:
          stat_prefix: ingress_http
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: ["*"]
              routes:
              - match: { prefix: "/" }
                route: { host_rewrite: www.google.com, cluster: service_google }
          http_filters:
          - name: envoy.router
```

フィルターは、HTTP接続用に設計された組み込みフィルターである[envoy.http_connection_manager](https://www.envoyproxy.io/docs/envoy/latest/api-v2/config/filter/network/http_connection_manager/v2/http_connection_manager.proto)を使用しています。


|項目名|説明|
|:-|:-|
|stat_prefix|接続マネージャーの統計を出力するときに使用する、人間が読める接頭辞|
|route_config|ルートの設定。仮想ホストが一致すると、ルートがチェックされます。この例では`route_config`は、要求されたホストドメインに関係なく、すべての着信HTTPリクエストに一致します。|
|routes|URLプレフィックスが一致した場合、一連のルートルールが次に何が起こるかを定義します。この場合`/`はリクエストのルートに一致することを意味します|
|host_rewrite|HTTPリクエストのHostヘッダーを書き換えます|
|cluster|リクエストを処理するクラスターの名前。実装は後述|
|http_filters|フィルターにより、Envoyはリクエストの処理時にリクエストを適応および変更できます|



### 1-4. Clusters
リクエストがフィルターに一致すると、リクエストはクラスターに渡されます。
以下に示すクラスターは、ホストがHTTPSで実行されているgoogle.comであることを定義しています。複数のホストが定義されている場合 Envoy は`ラウンドロビン戦略`を実行します。

```yaml
  clusters:
  - name: service_google
    connect_timeout: 0.25s
    type: LOGICAL_DNS
    dns_lookup_family: V4_ONLY
    lb_policy: ROUND_ROBIN
    hosts: [{ socket_address: { address: google.com, port_value: 443 }}]
    tls_context: { sni: www.google.com }
```

### 1-5. Admin
最後に管理セクションが必要です。管理セクションについては、次の手順で詳しく説明します。

```yaml
admin:
  access_log_path: /tmp/admin_access.log
  address:
    socket_address: { address: 0.0.0.0, port_value: 9901 }
```

この構造は、Envoy Static Configurationのボイラープレートを定義します。リスナーは、EnvoyのポートとIPアドレスを定義します。リスナーには、着信要求に一致する一連のフィルターがあります。リクエストが一致すると、クラスターに転送されます。


## 2. Start Proxy
### 2-1. Start Envoy
`1.Create Proxy Config`で作成した`envoy.yaml`を使用して、port 80にバインドされた Envoy を起動します。

```bash:envoyコンテナの起動
$ docker run --name=proxy -d \
  -p 80:10000 \
  -v $(pwd)/envoy/envoy.yaml:/etc/envoy/envoy.yaml \
  envoyproxy/envoy:latest
```

### 2-2. View Envoy
katacoda 上のターミナルで`curl localhost`を叩くと Google.com のソースが表示されます。(HTMLなのでよくわからないですが…)

また katacoda が発行した URL を自身の PC のブラウザから叩くことができます。叩いてみると、リクエストが設定通り Google.com にプロキシされたことがわかります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306374/2b01c72c-1acc-a196-d776-bf1c16ae11d8.png" width=80%>

## 3. Admin View
Envoyには管理用ビューが用意されており、構成、統計、ログ、その他の内部Envoyデータを表示できます。
管理者は、管理者ビューのポートが定義されている追加のリソース定義を追加することによって定義できます。
ポートは他のリスナー設定と競合してはなりません。

```yaml
admin:
  access_log_path: /tmp/admin_access.log
  address:
    socket_address: { address: 0.0.0.0, port_value: 9901 }
```

### 3-1. Start Admin
このDockerコンテナは、管理ポートを外部に公開します。上記のリソース設定により、管理者ビューが一般に公開されます。デモ目的でのみ使用してください。管理者ポータルを保護する方法に関するドキュメントを参照してください。

管理ポータルを公開するには、次のコマンドを実行します。

```bash
$ docker run --name=proxy-with-admin -d \
      -p 9901:9901 \
      -p 10000:10000 \
      -v $(pwd)/envoy/envoy.yaml:/etc/envoy/envoy.yaml \
      envoyproxy/envoy:latest
```

現在の形式の管理インターフェイスでは、破壊的な操作（サーバーのシャットダウンなど）を実行できるだけでなく、プライベート情報（統計、クラスター名、証明書情報など）を公開する可能性もあります。アクセスすることが重要です。管理インターフェースへのアクセスは安全なネットワークを介してのみ許可されています

ブラウザから管理者ビューを開いてみると、以下のような画面が表示されました。ここからいろいろな情報が取れるみたいです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306374/9a16c851-7eec-093d-52b2-806c3f8d7b36.png" width="50%">

## 4. Route to Docker Containers
最後の例では Envoy を使用して、リクエストされた URL パスに基づいてトラフィックをさまざまな Python サービスにプロキシします。


### 4-1. Configuration
アプリケーションの構成は Docker Compose ファイルとして定義されます。複数のコンテナを同時に実行したいので Docker Compose ファイルを使用します。1つはプロキシ用で、もう1つは個々のサービスごとです。

```yaml:examples/front-proxy/docker-compose.yml
version: '2'
services:

  front-envoy:
    build:
      context: .
      dockerfile: Dockerfile-frontenvoy
    volumes:
      - ./front-envoy.yaml:/etc/front-envoy.yaml
    networks:
      - envoymesh
    expose:
      - "80"
      - "8001"
    ports:
      - "8000:80"
      - "8001:8001"

  service1:
    build:
      context: .
      dockerfile: Dockerfile-service
    volumes:
      - ./service-envoy.yaml:/etc/service-envoy.yaml
    networks:
      envoymesh:
        aliases:
          - service1
    environment:
      - SERVICE_NAME=1
    expose:
      - "80"

  service2:
    build:
      context: .
      dockerfile: Dockerfile-service
    volumes:
      - ./service-envoy.yaml:/etc/service-envoy.yaml
    networks:
      envoymesh:
        aliases:
          - service2
    environment:
      - SERVICE_NAME=2
    expose:
      - "80"

networks:
  envoymesh: {}
```

### 4-2. Application
このサービスは Python Web アプリケーションであり、コンテナー内で Envoy を使用して、トラフィックを Python アプリケーションに転送します。アプリケーションの前に Envoy を配置する必要はありません。

```python:examples/front-proxy/service.py
from flask import Flask
from flask import request
import socket
import os
import sys
import requests

app = Flask(__name__)

TRACE_HEADERS_TO_PROPAGATE = [
    'X-Ot-Span-Context',
    'X-Request-Id',

    # Zipkin headers
    'X-B3-TraceId',
    'X-B3-SpanId',
    'X-B3-ParentSpanId',
    'X-B3-Sampled',
    'X-B3-Flags',

    # Jaeger header (for native client)
    "uber-trace-id"
]

@app.route('/service/<service_number>')
def hello(service_number):
    return ('Hello from behind Envoy (service {})! hostname: {} resolved'
            'hostname: {}\n'.format(os.environ['SERVICE_NAME'], 
                                    socket.gethostname(),
                                    socket.gethostbyname(socket.gethostname())))

@app.route('/trace/<service_number>')
def trace(service_number):
    headers = {}
    # call service 2 from service 1
    if int(os.environ['SERVICE_NAME']) == 1 :
        for header in TRACE_HEADERS_TO_PROPAGATE:
            if header in request.headers:
                headers[header] = request.headers[header]
        ret = requests.get("http://localhost:9000/trace/2", headers=headers)
    return ('Hello from behind Envoy (service {})! hostname: {} resolved'
            'hostname: {}\n'.format(os.environ['SERVICE_NAME'], 
                                    socket.gethostname(),
                                    socket.gethostbyname(socket.gethostname())))

if __name__ == "__main__":
    app.run(host='127.0.0.1', port=8080, debug=True)
```


### 4-3. Envoy Frontend Proxy
Envoy プロキシの設定は以下のようになります。

```yaml:
static_resources:
  listeners:
  - address:
      socket_address:
        address: 0.0.0.0
        port_value: 80
    filter_chains:
    - filters:
      - name: envoy.http_connection_manager
        config:
          codec_type: auto
          stat_prefix: ingress_http
          route_config:
            name: local_route
            virtual_hosts:
            - name: backend
              domains:
              - "*"
              routes:
              - match:
                  prefix: "/service/1"
                route:
                  cluster: service1
              - match:
                  prefix: "/service/2"
                route:
                  cluster: service2
          http_filters:
          - name: envoy.router
            config: {}
  clusters:
  - name: service1
    connect_timeout: 0.25s
    type: strict_dns
    lb_policy: round_robin
    http2_protocol_options: {}
    hosts:
    - socket_address:
        address: service1
        port_value: 80
  - name: service2
    connect_timeout: 0.25s
    type: strict_dns
    lb_policy: round_robin
    http2_protocol_options: {}
    hosts:
    - socket_address:
        address: service2
        port_value: 80
admin:
  access_log_path: "/dev/null"
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 8001
```

`routes`は、リクエストの URL に基づいて一致します。

```yaml
routes:
    - match:
        prefix: "/service/1"
    route:
        cluster: service1
    - match:
        prefix: "/service/2"
    route:
        cluster: service2
```

envoy の設定により、service1 および service2 というエンドポイントにトラフィックを転送します。これらは Docker Compose で構成された Docker Network によって提供される DNS エントリーです。

### 4-4. Deploy
サンプルを開始します。

```bash
$ docker-compose -f ~/envoy/examples/front-proxy/docker-compose.yml up -d
```

### 4-5. Admin View
管理ビューを見ると、色々な情報を確認できます。

|URL|内容|
|:--|:--|
| https://管理画面用URLやポート/ | 管理インターフェイス |
|https://管理画面用URLやポート/config_dump|ルーティング構成をJSONとして表示できます。|
|https://管理画面用URLやポート/clusters|利用可能なクラスターやそのメトリックなどの追加情報|
|https://管理画面用URLやポート/stats|各種メトリクス|

### 5-6. Application Routing
Envoy は port 8000 で リッスンさせています。よって katacoda 上のターミナルで`curl localhos:8080`を叩くと、付加したURLに基づいて、さまざまなサービスが設定に従って応答します。

```bash
$ curl localhost:8000/service/1
Hello from behind Envoy (service 1)! hostname: 24c46f5120ad resolvedhostname: 172.19.0.2
$ curl localhost:8000/service/2
Hello from behind Envoy (service 2)! hostname: 8b93ae3dd237 resolvedhostname: 172.19.0.3
```

#おわりに
とりあえず`Getting Started with Envoy`が完了しました。リクエストを外部サービスにリバースプロキシできたり，複数のアプリケーションにパスベースのルーティングができたりしました。まあまだ基本の設定だけなので、「で? ApacheやNginxでもできるんですけど」という状態です。
Envoy の良さを知るために、引き続き`Try Envoy`を進めていこうと思います。