Try Envoy: その2 Migrating from NGINX to Envoy Proxy をやってみた

# はじめに

前回の「Try Envoy: その1 Getting Started with Envoy をやってみた」に引き続き、Try Envoy の`Migrating from NGINX to Envoy Proxy`を勉強していきたいと思います。

というのも、現在本番稼働している Kubernetes では Nginx Ingress Controller を使用しており、もし Envoy が Nginx よりメリットがあり、かつ簡単にマイグレーションできるのであれば Envoy に変更したいな〜と思ったからです。

では`Migrating from NGINX to Envoy Proxy`を、翻訳しながら進めていきたいと思います。


https://www.katacoda.com/envoyproxy/scenarios/migrating-from-nginx-to-envoy


# Migrating from NGINX to Envoy Proxy
このシナリオは NGINX から Envoy への移行をサポートすることを目的としています。これは NGINX に関する以前の経験と理解を Envoy に適用するのに役立ちます。

次の方法を学習します：

- Envoy サーバーの構成と設定を構成します
- Envoy を設定して、トラフィックを外部サービスにプロキシします。
- AccessLog と ErrorLog を設定します。

シナリオの最後では、 Envoy のコア機能と、既存の NGINX スクリプトをプラットフォームに移行する方法について理解します。

## 1. NGINX Example

NGINX の設定ファイルのサンプル

```nginx
user  www www;
pid /var/run/nginx.pid;
worker_processes  2;

events {
  worker_connections   2000;
}

http {
  gzip on;
  gzip_min_length  1100;
  gzip_buffers     48k;
  gzip_types       text/plain;

  log_format main      '$remote_addr - $remote_user [$time_local]  '
    '"$request" $status $bytes_sent '
    '"$http_referer" "$http_user_agent" '
    '"$gzip_ratio"';

  log_format download  '$remote_addr - $remote_user [$time_local]  '
    '"$request" $status $bytes_sent '
    '"$http_referer" "$http_user_agent" '
    '"$http_range" "$sent_http_content_range"';


  upstream targetCluster {
    172.18.0.3:80;
    172.18.0.4:80;
  }

  server {
    listen        8080;
    server_name   one.example.com  www.one.example.com;

    access_log   /var/log/nginx.access_log  main;
    error_log  /var/log/nginx.error_log  info;

    location / {
      proxy_pass         http://targetCluster/;
      proxy_redirect     off;

      proxy_set_header   Host             $host;
      proxy_set_header   X-Real-IP        $remote_addr;
    }
  }
}
```

NGINX の設定には通常3つの主要な要素があります。

1. NGINXサーバー、ロギング構造、Gzip機能の構成。これはすべてのインスタンスにわたってグローバルに定義されます。
2. ポート8080 でone.example.comホストのリクエストを受け入れるようにNGINXを構成します。
3. URLのさまざまな部分へのトラフィックを処理する方法のターゲットの場所を構成します。

すべての構成が Envoy プロキシに適用されるわけではなく、特定の側面を構成する必要はありません。Envoy Proxy には NGINX が提供するコアインフラストラクチャをサポートする4つの主要なコンポーネントがあります。

|コンポーネント|説明|
|:--|:--|
|Listeners|Envoy Proxyが着信要求を受け入れる方法を定義します。現在、Envoy ProxyはTCPベースのリスナーのみをサポートしています。接続が確立されると、処理のために一連のフィルターに渡されます。|
|Filters|インバウンドおよびアウトバウンドのデータを処理できるパイプラインアーキテクチャの一部です。この機能により、クライアントに送信する前にデータを圧縮するGzipなどのフィルターが有効になります。|
|Routers|クラスターとして定義されている必要な宛先にトラフィックを転送します。|
|Clusters|トラフィックのターゲットエンドポイントと構成設定を定義します。|

これら4つのコンポーネントを使用して、定義された NGINX 構成と一致する Envoy プロキシ設定を作成します。 

## 2.NGINX Configuration
### 2-1. Worker Connections
以下の設定は、ワーカープロセスと接続の数の定義に焦点を当てています。これは、需要を処理するために NGINX がどのようにスケーリングするかを示します。

```nginx
worker_processes  2;

events {
  worker_connections   2000;
}
```

Envoyは、システム内のすべてのハードウェアスレッドに対してワーカースレッドを生成します。各ワーカースレッドは、非ブロッキングイベントループを実行します。

1. すべてのリスナーを聞く
2. 新しい接続を受け入れる
3. 接続用のフィルタースタックのインスタンス化
4. 接続の存続期間中のすべてのIOの処理。

それ以降の接続の処理はすべて転送動作を含め、ワーカースレッド内で完全に処理されます。

Envoy のすべての接続プールはワーカースレッドごとです。HTTP/2接続プールは一度に各上流ホストへの接続を1つだけ作成しますが、ワーカーが4つある場合、定常状態では上流ホストごとに4つのHTTP/2接続が存在します。すべてを単一のワーカースレッド内に保持することにより、ほとんどすべてのコードをロックなしで、まるでシングルスレッドのように書くことができます。ワーカー数が必要以上に多いと、メモリが無駄になり、アイドル接続が多くなり、接続プールのヒット率が低下します。

まあぶっちゃけよくわかりませんでした。


### 2-1. HTTP Configuration
NGINX設定の次のブロックでは、次のようなHTTP設定を定義します。

- サポートされているMIMEタイプ
- デフォルトのタイムアウト
- Gzip設定

これらについては、後の手順で説明する Envoy Proxy 内の Filters コンポーネント内で設定します。

## 3. Server Configuration
以下の NGINX 設定の http ブロック内では、ポート8080でリッスンし、ドメイン one.example.com および www.one.example.com へのリクエストに応答するようになっています。

```nginx
 server {
    listen        8080;
    server_name   one.example.com  www.one.example.com;
```

Envoy では listeners コンポーネントによってこれらを設定します。

### 3-1. Envoy Listeners
Envoy を開始する際の最も重要な設定はリスナーを定義することです。Envoy インスタンスの実行方法を記述した設定ファイルを作成する必要があります。

以下のスニペットは、新しいリスナーを作成し、それをポート8080にバインドします。

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

なお NGNIX で言うところの`server_name`は、Envoy の `filters`コンポーネントで設定するため、ここで定義する必要はありません。

## 4. Location Configuration
NGINXの`location`ブロックでは、トラフィックの処理方法と転送先を定義します。下記の設定では、サイトへのすべてのトラフィック(/)が、`http://targetCluster/`にプロキシされます。

```nginx
location / {
    proxy_pass         http://targetCluster/;
    proxy_redirect     off;

    proxy_set_header   Host             $host;
    proxy_set_header   X-Real-IP        $remote_addr;
}
```

Envoy では`filters`コンポーネントで設定します。

### 4-1. Envoy Filters
静的設定の場合、`filters`はリクエストを処理する方法を定義します。この場合、前のステップで`server_names`に一致する`filters`を設定しています。定義された`domains`と`routes`に一致するリクエストが受信されると、トラフィックはクラスターに転送されます。

```yaml
    filter_chains:
    - filters:
      - name: envoy.http_connection_manager
        config:
          # "auto/HTTP1/HTTP2" 基本的にはデフォルトのautoで良さそう。HTTP1を設定すると通信を HTTP/1.1に強制できる．
          codec_type: auto
          stat_prefix: ingress_http
          route_config:
            name: local_route
            virtual_hosts:
            - name: backend
              domains:
                - "one.example.com"
                - "www.one.example.com"
              routes:
              - match:
                  prefix: "/"
                route:
                  cluster: targetCluster
          http_filters:
          - name: envoy.router
```

「Try Envoy: その1 Getting Started with Envoy をやってみた」でも出てきた`envoy.http_connection_manager`は、Envoy 内の組み込みフィルターです。名前の通り`HTTP`を制御するフィルターです。その他のフィルターの例として、`Redis`、`Mongo`、`TCP`などがあるようです。

またその他の負荷分散ポリシーの詳細については、Envoyのドキュメントをご覧ください。https://www.envoyproxy.io/docs/envoy/v1.8.0/intro/arch_overview/load_balancing

## 5. Proxy and Upstream Configuration
NGINX でのアップストリーム構成は、トラフィックを処理するターゲットサーバーのセットを定義します。以下の場合2つのクラスターが割り当てられています。

```nginx
  upstream targetCluster {
    172.18.0.3:80;
    172.18.0.4:80;
  }
```
Envoy では`clusters`コンポーネントによって設定します。

### 5-1. Envoy Clusters 
アップストリームに相当するものは`clusters`として定義します。以下の場合、トラフィックを処理するホストが定義されています。タイムアウトなどのホストへのアクセス方法は、クラスター構成として定義されます。これにより、タイムアウトやロードバランシングなどの側面をより細かく制御できます。

```yaml
  clusters:
  - name: targetCluster
    connect_timeout: 0.25s
    type: STRICT_DNS
    dns_lookup_family: V4_ONLY
    lb_policy: ROUND_ROBIN
    hosts: [
      { socket_address: { address: 172.18.0.3, port_value: 80 }},
      { socket_address: { address: 172.18.0.4, port_value: 80 }}
    ]
```

`STRICT_DNS`サービスディスカバリを使用する場合、Envoy は指定された DNS ターゲットを継続的かつ非同期的に解決します。DNS の結果で返された各 IP アドレスは、上流クラスターの明示的なホストと見なされます。つまりクエリが2つのIPアドレスを返す場合、Envoyはクラスターに2つのホストがあると想定し、両方の負荷を分散する必要があります。ホストが結果から削除されると、Envoyはホストが存在しないと見なし、既存の接続プールからトラフィックを排出します。

## 6. Logging Access and Errors
最後はロギングの設定です。

### Error log
エラーログをディスクにパイプする代わりに、Envoy クラウドネイティブのアプローチに従います。つまりすべてのアプリケーションログが`stdout`および`stderr`に出力されます。

### Access log
アクセスログはオプションでありデフォルトでは無効になっています。アクセスログを有効にするには、`envoy.http_connection_manager`内に`access_log`句を設定します。出力先のパスは要件に応じて、`stdout`などのデバイスか、ディスク上のファイルのいずれかになります。

以下の例では、すべてのアクセスログが`stdout`に出力されます。
```yaml
      - name: envoy.http_connection_manager
        config:
          codec_type: auto
          stat_prefix: ingress_http
          access_log:
          - name: envoy.file_access_log
            config:
              path: "/dev/stdout"
          route_config:
```
デフォルトのLogFormatは以下のようになっています、。

```
# LogFormat
[%START_TIME%] "%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%"
%RESPONSE_CODE% %RESPONSE_FLAGS% %BYTES_RECEIVED% %BYTES_SENT% %DURATION%
%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% "%REQ(X-FORWARDED-FOR)%" "%REQ(USER-AGENT)%"
"%REQ(X-REQUEST-ID)%" "%REQ(:AUTHORITY)%" "%UPSTREAM_HOST%"\n

# 出力例
[2018-11-23T04:51:00.281Z] "GET / HTTP/1.1" 200 - 0 58 4 1 "-" "curl/7.47.0" "f21ebd42-6770-4aa5-88d4-e56118165a7d" "one.example.com" "172.18.0.4:80"

```
`format`フィールドをカスマイズすることで、出力内容を変更することができます。


```yaml
access_log:
- name: envoy.file_access_log
  config:
    path: "/dev/stdout"
    format: "[%START_TIME%] "%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%" %RESPONSE_CODE% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% "%REQ(X-REQUEST-ID)%" "%REQ(:AUTHORITY)%" "%UPSTREAM_HOST%"\n"
```

`json_format`フィールドを使用すれば、ログを json として出力することもできます。

```yaml
access_log:
- name: envoy.file_access_log
  config:
    path: "/dev/stdout"
    json_format: {"protocol": "%PROTOCOL%", "duration": "%DURATION%", "request_method": "%REQ(:METHOD)%"}
```

## 7. Launching
いままでの envoy用の設定を足し合わせると、以下のようになります。

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

    filter_chains:
    - filters:
      - name: envoy.http_connection_manager
        config:
          codec_type: auto
          stat_prefix: ingress_http
          access_log:
          - name: envoy.file_access_log
            config:
              path: "/dev/stdout"
          route_config:
            name: local_route
            virtual_hosts:
            - name: backend
              domains:
                - "one.example.com"
                - "www.one.example.com"
              routes:
              - match:
                  prefix: "/"
                route:
                  cluster: targetCluster
          http_filters:
          - name: envoy.router

  clusters:
  - name: targetCluster
    connect_timeout: 0.25s
    type: STRICT_DNS
    dns_lookup_family: V4_ONLY
    lb_policy: ROUND_ROBIN
    hosts: [
      { socket_address: { address: 172.18.0.3, port_value: 80 }},
      { socket_address: { address: 172.18.0.4, port_value: 80 }}
    ]
```

この設定を元に Envoy を起動させます。


### 7-1. Run As User
nginx 設定ファイルの上部にある行`user  www www;`は、セキュリティを向上させるために、権限の低いユーザーとして NGINX を実行することを示しています。

Envoy は、クラウドネイティブアプローチを使用して、プロセス所有者を管理します。コンテナを介してEnvoyを起動するときに、低い特権のユーザーを指定できます。

### 7-2. Starting Envoy Proxy
以下のコマンドは、ホスト上の Docker コンテナを介して Envoy を起動します。このコマンドはポート80でリクエストをリッスンするように Envoy を公開します。ただしリスナーで指定されているように Envoy 自体はポート8080でリッスンしています。また`--user`設定により、プロセスを低い特権のユーザーとして実行しています。


```bash
$ docker run --name proxy1 -p 80:8080 --user 1000:1000 -v /root/envoy.yaml:/etc/envoy/envoy.yaml envoyproxy/envoy
```

### 7-3. Testing
以下の curl コマンドは、`envoy.yaml`で定義した host header使用してリクエストしています。

```bash
$ curl -H "Host: one.example.com" localhost -i
HTTP/1.1 503 Service Unavailable
content-length: 57
content-type: text/plain
date: Wed, 19 Aug 2020 10:21:17 GMT
server: envoy

upstream connect error or disconnect/reset before headers

このリクエストの結果は 503 エラーになります。これは単純にアップストリーム接続が実行されておらず、使用できないためです。Envoy から見てリクエストに使用できるターゲットの宛先がありません。
次のコマンドは`envy.yaml`に定義された構成と一致する一連のHTTPサービスを起動します。

```bash
$docker run -d katacoda/docker-http-server;
$docker run -d katacoda/docker-http-server;
```
これで Envoy は利用可能なサービスを使用して、トラフィックをターゲットの宛先に正常にプロキシできます。

```bash
$ curl -H "Host: one.example.com" localhost -i
HTTP/1.1 200 OK
date: Wed, 19 Aug 2020 10:21:31 GMT
content-length: 58
content-type: text/html; charset=utf-8
x-envoy-upstream-service-time: 0
server: envoy

<h1>This request was processed by host: c64a8c264f62</h1>
```

どの Docker コンテナーがリクエストを処理したかを示す応答が表示されます。Envoy ログ内に出力されたアクセスラインも表示されます。

```
[2020-08-19T10:21:17.737Z] "GET / HTTP/1.1" 503 UF 0 57 252 - "-" "curl/7.47.0" "f55161f1-9b58-4ac0-b8a5-5db7466d2ea8" "one.example.com" "172.18.0.3:80"
[2020-08-19T10:21:31.759Z] "GET / HTTP/1.1" 200 - 0 58 1 0 "-" "curl/7.47.0" "67999eeb-2333-4b4a-a981-7bd5d1152cc8" "one.example.com" "172.18.0.4:80"
```

### 7-4. Additional HTTP Response Headers
有効なリクエストのレスポンスヘッダー内に、追加の HTTP header が表示されます。header には、上流ホストがリクエストの処理に費やした時間がミリ秒単位で表示されます。

```
x-envoy-upstream-service-time: 0
server: envoy
```