はじめに
前回の「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
を、翻訳しながら進めていきたいと思います。
Migrating from NGINX to Envoy Proxy
このシナリオは NGINX から Envoy への移行をサポートすることを目的としています。これは NGINX に関する以前の経験と理解を Envoy に適用するのに役立ちます。
次の方法を学習します:
- Envoy サーバーの構成と設定を構成します
- Envoy を設定して、トラフィックを外部サービスにプロキシします。
- AccessLog と ErrorLog を設定します。
シナリオの最後では、 Envoy のコア機能と、既存の NGINX スクリプトをプラットフォームに移行する方法について理解します。
1. NGINX Example
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つの主要な要素があります。
- NGINXサーバー、ロギング構造、Gzip機能の構成。これはすべてのインスタンスにわたってグローバルに定義されます。
- ポート8080 でone.example.comホストのリクエストを受け入れるようにNGINXを構成します。
- 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 がどのようにスケーリングするかを示します。
worker_processes 2;
events {
worker_connections 2000;
}
Envoyは、システム内のすべてのハードウェアスレッドに対してワーカースレッドを生成します。各ワーカースレッドは、非ブロッキングイベントループを実行します。
- すべてのリスナーを聞く
- 新しい接続を受け入れる
- 接続用のフィルタースタックのインスタンス化
- 接続の存続期間中のすべての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 へのリクエストに応答するようになっています。
server {
listen 8080;
server_name one.example.com www.one.example.com;
Envoy では listeners コンポーネントによってこれらを設定します。
3-1. Envoy Listeners
Envoy を開始する際の最も重要な設定はリスナーを定義することです。Envoy インスタンスの実行方法を記述した設定ファイルを作成する必要があります。
以下のスニペットは、新しいリスナーを作成し、それをポート8080にバインドします。
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/
にプロキシされます。
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
に一致するリクエストが受信されると、トラフィックはクラスターに転送されます。
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つのクラスターが割り当てられています。
upstream targetCluster {
172.18.0.3:80;
172.18.0.4:80;
}
Envoy ではclusters
コンポーネントによって設定します。
5-1. Envoy Clusters
アップストリームに相当するものはclusters
として定義します。以下の場合、トラフィックを処理するホストが定義されています。タイムアウトなどのホストへのアクセス方法は、クラスター構成として定義されます。これにより、タイムアウトやロードバランシングなどの側面をより細かく制御できます。
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
に出力されます。
- 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
フィールドをカスマイズすることで、出力内容を変更することができます。
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 として出力することもできます。
access_log:
- name: envoy.file_access_log
config:
path: "/dev/stdout"
json_format: {"protocol": "%PROTOCOL%", "duration": "%DURATION%", "request_method": "%REQ(:METHOD)%"}
7. Launching
いままでの envoy用の設定を足し合わせると、以下のようになります。
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
設定により、プロセスを低い特権のユーザーとして実行しています。
$ 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使用してリクエストしています。
$ 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 は利用可能なサービスを使用して、トラフィックをターゲットの宛先に正常にプロキシできます。
$ 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