Envoy 初心者がいくつかハマったこと

# この記事について

Envoy の初心者である自分が  [Getting Started](https://www.envoyproxy.io/docs/envoy/v1.16.2/start/start)  などをやってみてハマったことやその調査方法・解決策をまとめたものです。

- 環境
  - Envoy 1.16.2
  - Ubuntu 18.04

# ハマったこと

## Google にしかリクエストが通らない？

### 概要
Envoy のドキュメントの [Getting Started](https://www.envoyproxy.io/docs/envoy/v1.16.2/start/start) では、以下のような Google にリクエストをルーティングする設定ファイルを作る。

```yaml:envoy.yaml
static_resources:
  listeners:
  - name: listener_0
    address:
      socket_address: { address: 0.0.0.0, port_value: 10000 }
    filter_chains:
    - filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          stat_prefix: ingress_http
          codec_type: AUTO
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: ["*"]
              routes:
              - match: { prefix: "/" }
                route: { host_rewrite_literal: www.google.com, cluster: service_google }
          http_filters:
          - name: envoy.filters.http.router
  clusters:
  - name: service_google
    connect_timeout: 0.25s
    type: LOGICAL_DNS
    # Comment out the following line to test on v6 networks
    dns_lookup_family: V4_ONLY
    lb_policy: ROUND_ROBIN
    load_assignment:
      cluster_name: service_google
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: www.google.com
                port_value: 443
    transport_socket:
      name: envoy.transport_sockets.tls
      typed_config:
        "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
        sni: www.google.com
admin:
  access_log_path: /tmp/admin_access.log
  address:
    socket_address: { address: 0.0.0.0, port_value: 9901 }
```

これ自体は問題ないが、試しに上記 YAML で `www.google.com` になってる個所を全て `www.yahoo.com` や `www.httpbin.org` など別のサイトにしたらエラーが返るようになった。ただ、まれに応答が返る場合もある。

```bash
$ curl http://localhost:10000
upstream connect error or disconnect/reset before headers. reset reason: local reset
```

### 原因

コネクションタイムアウトの時間が 0.25s(250ms) と短いためタイムアウトになっていた。10s とかにしたら問題が無くなった。

```yaml
  clusters:
  - name: service_google
    connect_timeout: 0.25s  #ここを 10s とかにすればOK
```

### 調査方法

- Envoy から upstream(この場合の www.yahoo.com や www.httpbin.org など)に送るパケットをキャプチャ。
- Envoy が TLS の ClientHello を送ったすぐ後に自ら TCP の FIN を送っており、タイムアウトではないかと判断。
  - なお、キャプチャを見ると SYN ～ FIN の間が 250ms になってる
![yahoo_timeout.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/9dc67624-282a-729b-bc61-dbbbce45021b.png)

## Upstream のサーバーから切られる

### 概要
これは単純ミスなので書くほどでないが、何が起きるかという記録のために記載。
先述の問題で試行錯誤しているうちに envoy.yaml の `clusters` 配下から `transport_socket:` 以下を削除してしまった。なお、問題とは直接関係ないがその過程で `http2_protocol_options` を付けている。

```yaml:envoy.yamlの一部を掲載
  clusters:
  - name: target_service
    connect_timeout: 10s
    type: LOGICAL_DNS
    http2_protocol_options: {}
    dns_lookup_family: V4_ONLY
    lb_policy: ROUND_ROBIN
    load_assignment:
      cluster_name: target_service
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: www.google.com
                port_value: 443
``` 

すると、以下のようなエラーになる。

```bash
$ curl http://localhost:10000
upstream connect error or disconnect/reset before headers. reset reason: connection termination
```

### 原因
上記の通り `transport_socket:` を削除してしまったため、HTTPSサーバー(Port 443)に対して TLS を介さずクリアテキスト(HTTP, h2c)でリクエストを送っていた。
![h2c.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/f25afb4f-bd58-7379-7763-7c970d7e65f7.png)

## ファイルベースの xDS で YAML が使えない

### 概要
TRY ENVOY の [File Based Dynamic Routing Configuration](https://www.envoyproxy.io/try/file-based-dynamic-routing-configuration) を流すと EDS(Endpoint Discovery Service)の設定を `eds.conf` というファイルに外だしするが、中身は JSON で書かされる。
YAML で書きたいとググっていたところ、[このあたりの記事](https://www.tetrate.io/blog/envoy-101-file-based-dynamic-configurations/)で YAML で書いてる人を発見。

さっそく `eds.conf` を YAML 形式で書いてみたが全くリクエストが通らない。

```bash
$ curl http://localhost:10000
no healthy upstream
```

### 原因
`eds.conf` の拡張子を `.conf` から `.yaml` に変える必要がある。

### 調査方法

**1. 管理サーバー**
Envoy の 管理サーバー(っていう呼び方なのか知らないですが)にアクセスすると、クラスターの Endpoint が登録されてない。やはり Endpoint を定義してる `eds.conf` を読み込めてないらしい。
![cluster_none.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/b0b071fb-ab09-db02-5251-be4fb6e3a04b.png)

正常時はこんな感じ。
![cluster_ok.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/d19913ef-e7df-1cc5-3449-4048c0d773f7.png)


**2. デバッグログ**
Envoy を `--log-level` オプションでデバッグログを有効にして起動。この時は docker-compose を使ったが command を以下のようにした。

```yaml
version: "3.7"
services:
  envoy:
    // 中略
    command: 
      - "--config-path /etc/envoy/envoy.yaml"
      - "--log-level trace"
```

`docker logs` で見たらこんなログが出ていた(見やすさのために一部の情報は削除)。やはり JSON としてパースされてそう。

```
[2020-12-29 09:40:00.438] added watch for directory: '/etc/envoy' file: 'eds.conf' fd: 1
[2020-12-29 09:40:00.438] adding TLS initial cluster target_cluster
[2020-12-29 09:40:00.438] Filesystem config refresh for /etc/envoy/eds.conf
[2020-12-29 09:40:00.439] Filesystem config update failure: Unable to parse JSON as proto (INVALID_ARGUMENT:Unexpected token.
version_info: "0"
(以下略)
```

ログを元にソースを見たら拡張子を見て読み込み処理を分けていた。YAML の場合は `.yaml` にする必要がある(なお、該当するものがなければJSONとしてパースしてる)。

```cpp:envoy/source/common/protobuf/utility.cc
void MessageUtil::loadFromFile(const std::string& path, Protobuf::Message& message,
                               ProtobufMessage::ValidationVisitor& validation_visitor,
                               Api::Api& api, bool do_boosting) {

  // ".pb" の場合
  if (absl::EndsWith(path, FileExtensions::get().ProtoBinary)) {
      //...
  }
  // ".pb_text" の場合
  if (absl::EndsWith(path, FileExtensions::get().ProtoText)) {
      //...
  }

  // YAML の場合
  if (absl::EndsWith(path, FileExtensions::get().Yaml)) {
    loadFromYaml(contents, message, validation_visitor, do_boosting);
  } else {
    // その他は JSON とみなす
    loadFromJson(contents, message, validation_visitor, do_boosting);
  }
```

```cpp:envoy/source/common/protobuf/utility.h
  class FileExtensionValues {
  public:
    const std::string ProtoBinary = ".pb";
    const std::string ProtoBinaryLengthDelimited = ".pb_length_delimited";
    const std::string ProtoText = ".pb_text";
    const std::string Json = ".json";
    const std::string Yaml = ".yaml";  // YAML の場合の拡張子。
  };

  using FileExtensions = ConstSingleton<FileExtensionValues>;
```

# 調査方法まとめ

## Envoy のデバッグログ

envoy のコマンドラインオプションで `-l` または `--log-level` によりログレベルを指定できる。詳細なログを見る場合は `trace` か `debug` を指定すればよさそう。

```
   -l <string>,  --log-level <string>
     Log levels: [trace][debug][info][warning
     |warn][error][critical][off]
```

Docker で起動する場合はこんな感じ。

```bash
docker run --name=envoy -d \
  -p 10000:10000 \
  -p 9901:9901 \
  -v $(pwd)/envoy.yaml:/etc/envoy/envoy.yaml \
  envoyproxy/envoy:v1.16.2 \
    --config-path /etc/envoy/envoy.yaml \
    --log-level debug
```

他にもログ関連のオプションがあるため、[Command line options](https://www.envoyproxy.io/docs/envoy/latest/operations/cli) を見てください。

## 管理サーバー
envoy.yaml で `admin:` で定義するやつです。

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

Envoy の構成やステータス情報を見れて、自分は今回主に`clusters` を見たけど他にも調査に役立つはず。

![admin_server.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/15929584-c92d-d824-ee04-ff9f8ae3922e.png)

ところで、envoy.yaml の `access_log_path: /tmp/admin_access.log` で指定してるログは、自分がハマったように Upstream からエラーが返るといった場合は記録が残らなかった(あくまで試した範囲では)。

## その他

その他は定番ですがパケットキャプチャ、あとは、中身を見ないと分からない場合はEnvoyのソースを追う、などでしょうか。