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の真髄はクラウドネイティブなアプリケーションで利用できる、柔軟性や可観測性です。 それらの記事については追々書いていきたいと思います。