Envoy Proxyで作るHTTPロードバランサー

Envoy Proxy に入門してポチポチ勉強を進めてます。 Envoy Proxy は動的な設定と xDS API が魅力的ですが、手始めに静的な設定ファイルで動かしてみましょう。 この記事では HTTP ロードバランサーを Envoy で構築します。 完成図は以下の図になります。

Envoyで構築するHTTPプロキシの図

HTTP ロードバランサーの後ろには、目的の異なる 2 つのクラスタがあります。 今回は NGINX と Apache httpd を使っています。 それぞれのクラスタはnginx.localhttpd.localというホスト名(バーチャルホスト)で経路を分岐します。

  • 2019-02-06 追記 : DEPRECATED なプロパティを修正しました。

Envoy の設定ファイル

Envoy をどういう環境で利用するにもBootstrap Configurationが必要です。 静的な設定ファイルで Envoy を利用する場合も必要ですし、xDS API を利用する場合も xDS サービスの設定を書きます。 Bootstrap Configuration には他にも、ノードの識別子や管理画面を設定できます。

Envoy の設定の仕様は公式ドキュメントにあります。

Envoy の設定は v1 API と v2 API の 2 つのバージョンがあります。 公式ドキュメントも v1 API に関する項目が削除され、将来使えなくなる可能性があるので、本記事も v2 APi のみを対象とします。

クラスタの起動

実験のためにまずは HTTP サーバーを建てます。NGINX と httpd は Docker Hub の nginx を使います。 これらのコンテナイメージはdocker runするだけで HTTP サーバーが起動します。 また HTTP にアクセスするとログにアクセスログが出ます。 それぞれ 4 つのターミナル上で立ち上げてログを眺めましょう。

$ docker run --rm --name nginx1 nginx
$ docker run --rm --name nginx2 nginx
$ docker run --rm --name httpd1 httpd
$ docker run --rm --name httpd2 httpd

どういう結果を確かめるには、Docker コンテナに直接アクセスしてみたらわかるでしょう。

$ curl $(docker inspect nginx1 | jq -r '.[].NetworkSettings.IPAddress')
$ curl $(docker inspect httpd1 | jq -r '.[].NetworkSettings.IPAddress')

設定を書く

Bootstrap config を書きましょう。 以下の設定を /tmp/envoy/envoy.yaml に保存します。

# /tmp/envoy/envoy.yaml
static_resources:
  listeners:
    - name: listener_0
      address:
        socket_address: { address: 0.0.0.0, port_value: 80 }
      filter_chains:
        - filters:
            - name: envoy.http_connection_manager
              config:
                stat_prefix: ingress_http
                http_filters:
                  - name: envoy.router
                route_config:
                  name: route
                  virtual_hosts:
                    - name: nginx_service
                      domains: ["nginx.local"]
                      routes:
                        - match:
                            prefix: "/"
                          route:
                            cluster: nginx_cluster
                    - name: nginx_service
                      domains: ["httpd.local"]
                      routes:
                        - match:
                            prefix: "/"
                          route:
                            cluster: httpd_cluster
  clusters:
    - name: nginx_cluster
      type: STRICT_DNS
      connect_timeout: 0.25s
      lb_policy: ROUND_ROBIN
      load_assignment:
        cluster_name: nginx_cluster
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address: { address: nginx1, port_value: 80 }
              - endpoint:
                  address:
                    socket_address: { address: nginx2, port_value: 80 }
    - name: httpd_cluster
      type: STRICT_DNS
      connect_timeout: 0.25s
      lb_policy: ROUND_ROBIN
      load_assignment:
        cluster_name: httpd_cluster
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address: { address: httpd1, port_value: 80 }
              - endpoint:
                  address:
                    socket_address: { address: httpd2, port_value: 80 }

Envoy を起動

Envoy は公式イメージが Docker Hub で配布されてるものを使います。 この Docker イメージは/etc/envoy/envoy.yamlから設定をロードします。 手元で記述した設定ファイルはコンテナ内にマウントします。 また先程の HTTP サーバーを名前解決するために --link オプションでそれぞれの Docker コンテナ名を指定します。

$ docker run \
    --name envoy --rm --publish 80:80 \
    --link nginx1 --link nginx2 --link httpd1 --link httpd2 \
    -v /tmp/envoy:/etc/envoy \
    envoyproxy/envoy:v1.9.0

ここまでで Envoy によるロードバランサーが完成しました。 バーチャルホスト名で Envoy に到達できるように、/etc/hostsに以下の2つのエントリを追加します。

# /etc/hosts
127.0.0.1 nginx.local
127.0.0.1 httpd.local

Envoy が起動したら、nginx.localhttpd.local にアクセスしてみましょう。

$ curl nginx.local
$ curl httpd.local

nginx.local でアクセスすると「Welcome to nginx!」が表示され、httpd.localでアクセスすると「It works!」が表示されると思います。

設定ファイルを眺める

それでは順を追って設定ファイルを読んでいきましょう。 上記の設定は必要最低限なフィールドのみ埋めています。 それぞれのフィールドの定義は適宜ドキュメントへのリンクを貼ってあるので、必要に応じて参照してください。

1 行目のstatic_resources はその名の通り静的なリソースを記述できます(定義はStaticResourcesにあります)。 ほかにもdynamic_resources などがあります。

static_resources:

listenersにはListenerの設定を記述します。

listeners:
  - name: listener_0
    address:
      socket_address: { address: 0.0.0.0, port_value: 8000 }
    filter_chains:
      - filters:

Listener の設定には待ち受けるアドレス・ポートと、受け取ったパケットをどう処理するかを決めるフィルターを記述します。 Listener はFilterChainの設定を持ち、さらに FilterChain がFilterの設定を持ちます。

ここでは 1 つのフィルターを定義しています。

- name: envoy.http_connection_manager
  config:
    http_filters:
      - name: envoy.router
    stat_prefix: ingress_http
    route_config:
      name: route
      virtual_hosts:

name フィールドは利用するフィルターの種類です。 envoy.http_connection_managerは Envoy の組み込みフィルターで、HTTP (L7 レイヤー) の情報に基づいて処理します。 envoy.http_connection_managerを指定する場合は、configフィールドにHttpConnectionManager を指定します。

http_filters フィールドは HTTP connection manager がどういう HTTP フィルタを行うかを指定します。 ここでは envoy.router という経路制御のための HTTP フィルタを利用します。 stat_prefix はモニタリング用途に使うメトリクス名のプレフィクスです。

envoy.router では route_config フィールドでRoute Configurationを設定します。 その中の virtual_hosts フィールドでVirtualHostを設定します。 ここではバーチャルホストに基づいて 2 つのクラスタに経路を設定します。 他にも URL などに基づいて経路を制御可能です。

- name: nginx_service
  domains: ["nginx.local"]
  routes:
    - match:
        prefix: "/"
      route:
        cluster: nginx_cluster
- name: nginx_service
  domains: ["httpd.local"]
  routes:
    - match:
        prefix: "/"
      route:
        cluster: httpd_cluster

最後にClusterを定義します。 各 Cluster にはそれぞれ 2 つの Endpoint があり、ラウンドロビンで接続先を決定します。

以前は Cluster のhostsフィールドが利用できてましたが、現在は DEPRECATED になり、かわりにload_assignmentを使います。 load_assignmentフィールドは Endpoint Discovery Service (EDS) が返す値と同じ構造をしており、hostsフィールドより細やかな設定ができます。 いまはひとまず、2 つのエンドポイントにロードバランシングします。

clusters:
  - name: nginx_cluster
    type: STRICT_DNS
    connect_timeout: 0.25s
    lb_policy: ROUND_ROBIN
    load_assignment:
      cluster_name: nginx_cluster
      endpoints:
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address: { address: nginx1, port_value: 80 }
            - endpoint:
                address:
                  socket_address: { address: nginx2, port_value: 80 }
  - name: httpd_cluster
    type: STRICT_DNS
    connect_timeout: 0.25s
    lb_policy: ROUND_ROBIN
    load_assignment:
      cluster_name: httpd_cluster
      endpoints:
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address: { address: httpd1, port_value: 80 }
            - endpoint:
                address:
                  socket_address: { address: httpd2, port_value: 80 }

まとめ

この記事では Envoy で L7LB を構築しました。 これだけだと NGINX とどう違うんだと感じるかも知れません。 Envoy の真髄はクラウドネイティブなアプリケーションで利用できる、柔軟性や可観測性です。 それらの記事については追々書いていきたいと思います。


Profile picture

Shin'ya Ueoka

B2B向けSaaSを提供する会社の、元Webエンジニア。今はエンジニアリング組織のマネジメントをしている。