kakakakakku blog

Weekly Tech Blog: Keep on Learning!

Envoy のディスカバリサービス (xDS) を学べる「File Based Dynamic Routing Configuration」を試した

今回は「Try Envoy」「File Based Dynamic Routing Configuration」を紹介する.今までの内容は envoy.yaml に static な設定をしていたけど,設定を dynamic に反映できる Envoy の「ディスカバリサービス (xDS)」を学べる.また Envoy は xDS として「File Based(ファイル)」「API Based(REST / gRPC)」をサポートしている.今回は「File Based」を試す.

File Based Dynamic Routing Configuration

手順は以下の「計8種類」ある.それでもなお「Estimated Time: 10 minutes」と書いてあって,雑すぎるでしょ!

  • Step.1 「Envoy Dynamic Configuration」
  • Step.2 「Cluster ID」
  • Step.3 「EDS Configuration」
  • Step.4 「EDS Configuration」
  • Step.5 「Start Envoy」
  • Step.6 「Apply Changes」
  • Step.7 「CDS Configuration」
  • Step.8 「CDS Apply Changes」

www.envoyproxy.io

www.katacoda.com

Step.1 「Envoy Dynamic Configuration」

Envoy には,今まで使ってきた static な設定以外に dynamic な設定があり,「ディスカバリサービス (xDS)」と言う.xDS と言われている通り,様々な種類がある.今回のコンテンツでは「EDS / CDS / LDS」を試す.

  • EDS (Endpoint Discovery Service)
  • CDS (Cluster Discovery Service)
  • RDS (Route Discovery Service)
  • LDS (Listener Discovery Service)
  • SDS (Secret Discovery Service)

ドキュメントを読むと,コンテンツに載っている xDS 以外にも種類があった.例えば,以下など.

  • VHDS (Virtual Host Discovery Service)
  • SRDS (Scoped Route Discovery Service)
  • RTDS (RunTime Discovery Service)

www.envoyproxy.io

Step.2 「Cluster ID」

最初に envoy.yamlnode を設定する.この設定は今までのコンテンツには出てこなかった.xDS を使うときに Envoy 自体を識別する ID となり,今回は適当に id_1 とした.

node:
  id: id_1
  cluster: test

Step.3 「EDS Configuration」

今までは envoy.yamlclusters に転送するホストを static に設定していたけど,今回は EDS (Endpoint Discovery Service) を使うため eds_configeds.conf を指定する.ファイル自体は Step.4 で作成する.

clusters:
- name: targetCluster
  connect_timeout: 0.25s
  lb_policy: ROUND_ROBIN
  type: EDS
  eds_cluster_config:
    service_name: localservices
    eds_config:
      path: '/etc/envoy/eds.conf'

今回は「File Based」なので path を指定した.もし api_config_source を指定すると「API Based」になる.

www.envoyproxy.io

Step.4 「EDS Configuration」

次に eds.conf を作成する.まず,エンドポイントを1個にして,IP アドレスを 172.18.0.3 にする.

{
  "version_info": "0",
  "resources": [{
    "@type": "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment",
    "cluster_name": "localservices",
    "endpoints": [{
      "lb_endpoints": [{
        "endpoint": {
          "address": {
            "socket_address": {
              "address": "172.18.0.3",
              "port_value": 80
            }
          }
        }
      }]
    }]
  }]
}

Step.5 「Start Envoy」

実際に Envoy と katacoda/docker-http-server を起動し,以下の構成図のようになる.

$ docker run --name=proxy-eds-filebased -d \
    -p 9901:9901 \
    -p 80:10000 \
    -v /root/:/etc/envoy \
    envoyproxy/envoy:latest

$ docker run -d katacoda/docker-http-server

$ docker run -d katacoda/docker-http-server

f:id:kakku22:20191217065539p:plain

eds.conf にエンドポイントを1個しか設定していないため,何度リクエストを送っても,特定のコンテナに転送される.

$ curl localhost
<h1>This request was processed by host: c9d6a2229f32</h1>

$ curl localhost
<h1>This request was processed by host: c9d6a2229f32</h1>

$ curl localhost
<h1>This request was processed by host: c9d6a2229f32</h1>

Step.6 「Apply Changes」

eds.conf を修正し,エンドポイントを2個にする.以下の構成図のようになる.

{
  "version_info": "0",
  "resources": [{
    "@type": "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment",
    "cluster_name": "localservices",
    "endpoints": [{
      "lb_endpoints": [{
        "endpoint": {
          "address": {
            "socket_address": {
              "address": "172.18.0.3",
              "port_value": 80
            }
          }
        }
      },
        {
        "endpoint": {
          "address": {
            "socket_address": {
              "address": "172.18.0.4",
              "port_value": 80
            }
          }
        }
      }]
    }]
  }]
}

f:id:kakku22:20191217065554p:plain

修正した eds.conf を Envoy に自動的に反映するために mv コマンドを使ってファイルを差し替える.ドキュメントにも記載がある通り,Envoy はファイルの mv を監視している.

# Envoy will only watch the file path for moves.
$ mv eds.conf tmp; mv tmp eds.conf

直後に Envoy にリクエストを送ると,Envoy コンテナを再起動せずに eds.conf を反映できた.

$ curl localhost
<h1>This request was processed by host: c9d6a2229f32</h1>

$ curl localhost
<h1>This request was processed by host: edd664e9d604</h1>

$ curl localhost
<h1>This request was processed by host: c9d6a2229f32</h1>

$ curl localhost
<h1>This request was processed by host: edd664e9d604</h1>

Step.7 「CDS Configuration」

最後 2 Steps は CDS (Cluster Discovery Service)LDS (Listener Discovery Service) を試す.まず,CDS の設定となる cds.conf を作成する.既に作成した EDS と連携している.

{
  "version_info": "0",
  "resources": [{
    "@type": "type.googleapis.com/envoy.api.v2.Cluster",
    "name": "targetCluster",
    "connect_timeout": "0.25s",
    "lb_policy": "ROUND_ROBIN",
    "type": "EDS",
    "eds_cluster_config": {
      "service_name": "localservices",
      "eds_config": {
        "path": "/etc/envoy/eds.conf"
      }
    }
  }]
}

次に LDS の設定をする lds.conf を作成する.今まで envoy.yaml に設定していた filters を LDS に移したイメージとなる.

{
  "version_info": "0",
  "resources": [{
    "@type": "type.googleapis.com/envoy.api.v2.Listener",
    "name": "listener_0",
    "address": {
      "socket_address": {
        "address": "0.0.0.0",
        "port_value": 10000
      }
    },
    "filter_chains": [{
      "filters": [{
        "name": "envoy.http_connection_manager",
        "config": {
          "stat_prefix": "ingress_http",
          "codec_type": "AUTO",
          "route_config": {
            "name": "local_route",
            "virtual_hosts": [{
              "name": "local_service",
              "domains": [
                "*"
              ],
              "routes": [{
                "match": {
                  "prefix": "/"
                },
                "route": {
                  "cluster": "targetCluster"
                }
              }]
            }]
          },
          "http_filters": [{
            "name": "envoy.router"
          }]
        }
      }]
    }]
  }]
}

最後に新しく envoy1.yaml を作成し,cds.conflds.conf を設定している.全てを dynamic_resources の中に設定したため,とてもシンプルになった.おおおー!

node:
  id: id_1
  cluster: test

dynamic_resources:
  cds_config:
    path: "/etc/envoy/cds.conf"
  lds_config:
    path: "/etc/envoy/lds.conf"

既に Envoy を起動しているため,今回は 81 Port で新しい Envoy を起動する.特に挙動は変わらないけど,以下の構成図のようになる.

docker run --name=proxy-eds-cds-lds-filebased -d \
    -p 9902:9901 \
    -p 81:10000 \
    -v /root/:/etc/envoy \
    -v /root/envoy1.yaml:/etc/envoy/envoy.yaml \
    envoyproxy/envoy:latest

$ curl localhost:81
<h1>This request was processed by host: c9d6a2229f32</h1>

$ curl localhost:81
<h1>This request was processed by host: edd664e9d604</h1>

$ curl localhost:81
<h1>This request was processed by host: c9d6a2229f32</h1>

$ curl localhost:81
<h1>This request was processed by host: edd664e9d604</h1>

f:id:kakku22:20191217070613p:plain

Step.8 「CDS Apply Changes」

最後は cds.conflds.conf を修正して Envoy に反映する.まず,新しく cds.conf に Cluster newTargetCluster を追加する.

{
  "version_info": "0",
  "resources": [{
      "@type": "type.googleapis.com/envoy.api.v2.Cluster",
      "name": "targetCluster",
      "connect_timeout": "0.25s",
      "lb_policy": "ROUND_ROBIN",
      "type": "EDS",
      "eds_cluster_config": {
        "service_name": "localservices",
        "eds_config": {
          "path": "/etc/envoy/eds.conf"
        }
      }
    },
    {
      "@type": "type.googleapis.com/envoy.api.v2.Cluster",
      "name": "newTargetCluster",
      "connect_timeout": "0.25s",
      "lb_policy": "ROUND_ROBIN",
      "type": "EDS",
      "eds_cluster_config": {
        "service_name": "localservices",
        "eds_config": {
          "path": "/etc/envoy/eds1.conf"
        }
      }
    }
  ]
}

Cluster newTargetCluster から転送されるエンドポイントも新しく eds1.conf として設定する.

{
  "version_info": "0",
  "resources": [{
    "@type": "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment",
    "cluster_name": "localservices",
    "endpoints": [{
      "lb_endpoints": [{
          "endpoint": {
            "address": {
              "socket_address": {
                "address": "172.18.0.6",
                "port_value": 80
              }
            }
          }
        },
        {
          "endpoint": {
            "address": {
              "socket_address": {
                "address": "172.18.0.7",
                "port_value": 80
              }
            }
          }
        }
      ]
    }]
  }]
}

lds.conf は Cluster を newTargetCluster に変更しておく.

"route": {
  "cluster": "newTargetCluster"
}

修正した cds.conflds.confmv コマンドを使って差し替える.すると,自動的に新しく起動した katacoda/docker-http-server に接続できるようになった.

$ docker run -d katacoda/docker-http-server

$ docker run -d katacoda/docker-http-server

$ mv cds.conf tmp; mv tmp cds.conf; mv lds.conf tmp; mv tmp lds.conf

$ curl localhost:81
<h1>This request was processed by host: beac7d8bc5d3</h1>

$ curl localhost:81
<h1>This request was processed by host: 8d90f60e8a19</h1>

$ curl localhost:81
<h1>This request was processed by host: beac7d8bc5d3</h1>

$ curl localhost:81
<h1>This request was processed by host: 8d90f60e8a19</h1>

最終的に構成図は以下のようになる.

f:id:kakku22:20191217071552p:plain

まとめ

  • 「Try Envoy」のコンテンツ「File Based Dynamic Routing Configuration」を試した
  • Envoy でサポートされている「ディスカバリサービス (xDS)」の一部を学べた
    • EDS (Endpoint Discovery Service)
    • CDS (Cluster Discovery Service)
    • LDS (Listener Discovery Service)
  • 次は 「API Based」な xDS を試すために「API Based Dynamic Routing Configuration」をまとめる!

プルリクエスト

試しながら気付いた誤りを修正してプルリクエストを送っておいた!

github.com

Try Envoy 関連

今から Ansible に入門する初学者は読むべし /「Ansible 実践ガイド 第3版」を読んだ

2019年10月に発売された「Ansible 実践ガイド 第3版」を読んだ.実は今年の頭に「第2版」を購入していて,読もう読もうと積読をしていたら「第3版」が発売されたため,すぐに買い直して積読の優先順位を入れ替えた.個人的にプロダクション環境だと Chef の経験が長く,Ansible の経験が少ないこともあり,体系的に知識を整理しておこうという目的で読んだ.

読んだ感想としては,Ansible 初学者を中心に「Ansible で実現できることを知る」ことに適した良い本だと思う.Ansible の基礎から応用(徹底活用)まで幅広く学べる.逆に言うと,仕様を網羅したリファレンス本ではないし,ステップバイステップに写経をしながら試す本でもなく「どう読むと効果的なのか?」は気になるところ.今回は検証環境を Vagrant と Amazon EC2 に構築し,本書をリファレンスのように読みながら,知らなかった機能が出てきたら,実際にプレイブックを実装して試した.試しながら読み進めたことにより,学習効率は高かったと思う.

Ansible実践ガイド 第3版 (impress top gear)

Ansible実践ガイド 第3版 (impress top gear)

目次

  • 第1章 : Ansible の概要
  • 第2章 : Ansible の基礎
  • 第3章 : プレイブックとインベントリ
  • 第4章 : アプリケーションデプロイメント - Orchestration
  • 第5章 : システムの構成管理 - Configuration Management
  • 第6章 : ブートストラッピング - Bootstrapping
  • 第7章 : Ansible の徹底活用
  • 第8章 : 組織で実践する自動化

以下のサイトに正誤表は公開されているけど,特に報告はされていなさそうだった.実際に読むと数点誤植があるため,記事の最後にメモ程度に残しておく.

book.impress.co.jp

with_itemsloop

第3章「プレイブックとインベントリ」では,基本的なプレイブックの構文を学べる.その中に loop の解説があり,前から使える with_items との差を理解できてなく,実際に試しながら理解を整理した.loop は Ansible 2.5 で追加された構文で,ドキュメントを読むと「推奨 (recommend)」と書いてある.ただし,まだ完全に with_ の置き換えになるわけではなく,用途次第であるとも書かれている.

docs.ansible.com

まず,本書を参考に以下のプレイブックを実装した.loop の中にリスト(シーケンス)を実装し,{{ item }} で参照すると,順番に展開されて実行できる.今回は user モジュールを使って Linux にユーザーを3個追加する.

- hosts: all
  tasks:
    - name: Add users
      user:
        name: "{{ item }}"
        state: present
        groups: wheel
      loop:
        - kakakakakku1
        - kakakakakku2
        - kakakakakku3

実際に実行すると,正常に3個追加できた.なお,シーケンスに { name: 'kakakakakku1', groups: 'wheel' } のようなマッピングを設定し,{{ item.name }} とドット区切りにすると,複数の変数の値を展開することもできる.

$ getent passwd | grep kakakakakku | cut -d: -f1
kakakakakku1
kakakakakku2
kakakakakku3

なお,本書には with_itemsloop の差も解説されていた.簡単に言うと,with_items はシーケンスを flatten に展開し,loop は記載通りに展開する.変数の値を debug モジュールで標準出力する以下のプレイブックを実装した.シーケンスを変数にし,さらにリスト形式と文字列形式を混在させている.

- hosts: all
  vars:
    loop_test:
      - [kakakakakku1, kakakakakku2]
      - kakakakakku3
  tasks:
    - name: with_items
      debug:
        msg: "{{ item }}"
      with_items: "{{ loop_test }}"
    - name: loop
      debug:
        msg: "{{ item }}"
      loop: "{{ loop_test }}"

実際に実行すると,以下のようになる.with_items だと,リスト形式は無視されて,3回独立に実行されている.loop だと,2回実行されている.覚えておこう.

TASK [with_items] **************************************************************

ok: [localhost] => (item=kakakakakku1) => {
"msg": "kakakakakku1"
}

ok: [localhost] => (item=kakakakakku2) => {
"msg": "kakakakakku2"
}

ok: [localhost] => (item=kakakakakku3) => {
"msg": "kakakakakku3"
}

TASK [loop] ********************************************************************

ok: [localhost] => (item=[u'kakakakakku1', u'kakakakakku2']) => {
"msg": [
"kakakakakku1",
"kakakakakku2"
]
}

ok: [localhost] => (item=kakakakakku3) => {
"msg": "kakakakakku3"
}

serialmax_fail_percentage

第4章「アプリケーションデプロイメント」では,HAProxy / PHP / MariaDB / Keepalived などを組み合わせたフルスタックな WordPress 環境を Ansible で構築しながら,オーケストレーションに該当する Ansible の機能を学べる.管理するノード数が増えるため,Vagrant などを使って検証環境を作らないと,流し読みをして終わりになってしまう懸念もある.

その中に「nginx をローリングアップデートする」という内容がある.ノードにプレイブックを実行するときに,ミドルウェアの再起動を伴う場合などもあり,ロードバランサからノードを順番に切り離していく場面は多いと思う.ただし,デフォルトだと並列に実行されてしまうため,Ansible では serialmax_fail_percentage を使って,うまくローリングアップデートを実現できる.

docs.ansible.com

まず,プレイブックに serial を設定すると,任意の並行数を設定できる.例えば,以下のようにプレイブックを実装すると,1ノードごとにローリングアップデートとなる.

- name: Rolling Update
  hosts: apps
  serial: 1
  (中略)

ノード数が増えると,1ノードごとだとデプロイ時間が長時間化してしまう可能性もある.serial「割合 (%)」を設定することで,デプロイ時間を短縮しつつ,サービス影響のない範囲でローリングアップデートができる.

- name: Rolling Update
  hosts: apps
  serial: "30%"
  (中略)

さらに serial「並行数と割合 (%) を組み合わせたシーケンス」を設定することで,最初は1ノード,次に5ノード,残りを 20% ずつといったカナリア的なアップデートも実現できる.これは便利!

- name: Rolling Update
  hosts: apps
  serial:
  - 1
  - 5
  - "20%"
  (中略)

最後は max_fail_percentage で,プレイブックの実行を止める失敗率を定義することができる.ローリングアップデートによる全面障害を避けるためにも max_fail_percentage は設定しておくのが良さそう.

- name: Rolling Update
  hosts: apps
  serial: 1
  max_fail_percentage: 30
  (中略)

reboot モジュール

第5章「システムの構成管理」では,Linux と Windows の構成管理を学べる.その中に reboot モジュールの紹介があり,Ansible で再起動が必要なときに,SSH の接続を維持したまま再起動ができるようになる.Ansible 2.7 から使える機能となる.

- hosts: all
  tasks:
    - name: Reboot
      reboot:

docs.ansible.com

徹底活用

第7章「Ansible の徹底活用」は今後の参考になる実践的な内容だった.例えば,インベントリとプレイブックを管理するディレクトリ構成の紹介があったり,Ansible Galaxy の紹介もあった.

galaxy.ansible.com

また,パフォーマンス改善として「ファクト収集を無効化」したり,「ファクトキャッシュを有効化」したり,ansible.cfgforks パラメータを設定して並行数を上げたり,今まで知らなかったアプローチを知ることができた.実際に現場で使うときにもう1度読み直す.

誤植

  • 第1章 P.22 : Amazon Web ServiceAmazon Web Services
  • 第5章 P.202 : 疎結合しておくと疎結合にしておくと
  • 第7章 P.352 : ファクト収集ファクト取得 が表記揺れになっている

なお,誤植ではないけど「第4章」の基本構成で,PHP 実行環境として PHP 5.6 が前提になっている.PHP 5.6 は既に EOL になって1年となり,「第3版」として書き直すなら変えても良さそうだった(執筆の開始時期にもよるから判断は難しいけど).

まとめ

  • 「Ansible 実践ガイド 第3版」を読んだ
  • Ansible 初学者を中心に「Ansible で実現できることを知る」ことに適した1冊だった
  • ブログに載せた機能以外にも Ansible の機能で知らなかった部分を整理することができた

Ansible実践ガイド 第3版 (impress top gear)

Ansible実践ガイド 第3版 (impress top gear)

Envoy で HTTPS 接続をする設定を学べる「Securing traffic with HTTPS and SSL/TLS」を試した

どんどんと「Try Envoy」を進めていく.今回は Envoy で HTTPS 接続を試すコンテンツ「Securing traffic with HTTPS and SSL/TLS」を紹介する.今まで nginx を使う場合は ssl_certificatessl_certificate_key を設定したり,HTTP から HTTPS にリダイレクトをするために 301 を返していたけど,Envoy を使う場合はどのように envoy.yaml を設定するのか?を学べる.

Securing traffic with HTTPS and SSL/TLS

手順は以下の「計4種類」ある.

  • Step.1 「SSL Certificates」
  • Step.2 「Securing Traffic」
  • Step.3 「Redirecting HTTP Traffic」
  • Step.4 「Start Proxy」

www.envoyproxy.io

www.katacoda.com

Step.1 「SSL Certificates」

まず,検証のために example.com に対するオレオレ証明書を作成する.以下のコマンドを実行し,証明書 example-com.key と署名リクエスト example-com.crt を作成する.

$ openssl req -nodes -new -x509 \
  -keyout example-com.key -out example-com.crt \
  -days 365 \
  -subj '/CN=example.com/O=My Company Name LTD./C=US';

Step.2 「Securing Traffic」

Envoy で HTTPS 接続をする場合,filterstls_context を設定する.以下の envoy.yaml を読み解くと,8443 Port でリクエストを受けて,/service/1/service/2 のパスごとに異なる Cluster に転送する.そして tls_context に Step.1 で作成した example-com.keyexample-com.crt を設定することにより,HTTPS 接続となる.

- name: listener_https
  address:
    socket_address: { address: 0.0.0.0, port_value: 8443 }
  filter_chains:
  - filters:
    - name: envoy.http_connection_manager
      config:
        codec_type: auto
        stat_prefix: ingress_http
        route_config:
          name: local_route
          virtual_hosts:
          - name: backend
            domains:
            - "example.com"
            routes:
            - match:
                prefix: "/service/1"
              route:
                cluster: service1
            - match:
                prefix: "/service/2"
              route:
                cluster: service2
        http_filters:
        - name: envoy.router
          config: {}
    tls_context:
      common_tls_context:
        tls_certificates:
          - certificate_chain:
              filename: "/etc/envoy/certs/example-com.crt"
            private_key:
              filename: "/etc/envoy/certs/example-com.key"

Step.3 「Redirecting HTTP Traffic」

Envoy で HTTP から HTTPS にリダイレクトをする場合は routeredirect を設定する.

- name: listener_http
  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
        route_config:
          virtual_hosts:
          - name: backend
            domains:
            - "example.com"
            routes:
            - match:
                prefix: "/"
              redirect:
                path_redirect: "/"
                https_redirect: true
        http_filters:
        - name: envoy.router
          config: {}

実際にドキュメントを読むと,例えば response_code でレスポンスコードを 301 以外に設定したり,strip_query: true でクエリパラメータを削除したり,他のパラメータも用意されていた.

www.envoyproxy.io

Step.4 「Start Proxy」

最後は Envoy を起動する.今回は Envoy に対して「計3種類」のポートに接続できるようになっている.さらに,前回と同様にホスト名(コンテナ ID)を返す HTTP コンテナ katacoda/docker-http-server を2個起動し,最終的に以下の構成図のようになる.

  • 8080 (HTTP)
  • 8443 (HTTPS)
  • 8001 (管理画面)
$ docker run -it --name proxy1 -p 80:8080 -p 443:8443 -p 8001:8001 -v /root/:/etc/envoy/ envoyproxy/envoy
$ docker run -d katacoda/docker-http-server
$ docker run -d katacoda/docker-http-server

f:id:kakku22:20191206135928p:plain

さっそく HTTP にリクエストを送ると,期待通りに 301 となる(date は修正している).

$ curl -H "Host: example.com" http://localhost -i
HTTP/1.1 301 Moved Permanently
location: https://example.com/
date: Thu, 05 Dec 2019 00:00:00 GMT
server: envoy
content-length: 0

次に curl -k で HTTPS にリクエストを送ると,期待通りに 200 となり,パスごとに異なる HTTP コンテナからリクエストが返っている(date は修正している).

$ curl -k -H "Host: example.com" https://localhost/service/1 -i
HTTP/1.1 200 OK
date: Thu, 05 Dec 2019 00:00:00 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: eb8b87e0649a</h1>

$ curl -k -H "Host: example.com" https://localhost/service/2 -i
HTTP/1.1 200 OK
date: Thu, 05 Dec 2019 00:00:00 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: 3f660d280e19</h1>

まとめ

  • 「Try Envoy」のコンテンツ「Securing traffic with HTTPS and SSL/TLS」を試した
  • 今回も nginx で経験のある HTTPS 接続とリダイレクトを Envoy で試したため,イメージしやすかった

Try Envoy 関連

nginx と Envoy の設定を比較して学べる「Migrating from NGINX to Envoy Proxy」を試した

引き続き「Try Envoy」を使って Envoy を学ぶ.今回は nginx と Envoy を比較したコンテンツ「Migrating from NGINX to Envoy Proxy」を紹介する.nginx の nginx.conf を Envoy の envoy.yaml にどのようにマイグレーションするのか?を学べる.

Migrating from NGINX to Envoy Proxy

手順は以下の「計7種類」ある.今回もサイトの表記は「Estimated Time: 10 minutes」と書いてあるけど,気にせずに進める.

  • Step.1 「NGINX Example」
  • Step.2 「NGINX Configuration」
  • Step.3 「Server Configuration」
  • Step.4 「Location Configuration」
  • Step.5 「Proxy and Upstream Configuration」
  • Step.6 「Logging Access and Errors」
  • Step.7 「Launching」

www.envoyproxy.io

www.katacoda.com

Step.1 「NGINX Example」

nginx の設定には「ログ設定」「ポート設定」「ルーティング設定」などを書く.Envoy の場合は nginx と異なる仕組みとなり,大きく以下の4種類のコア設定から構成されている.詳しくは前回の記事にもまとめた.

  • Listeners
  • Filters
  • Routers
  • Clusters

Step.2 「NGINX Configuration」

まず nginx で「ワーカーコネクション」の設定をする場合,よく worker_processesworker_connections をチューニングする.worker_processes に CPU コア数を指定したり,auto を指定する.Envoy の場合はどのようにコネクションを管理するのだろうか?

worker_processes  2;

events {
  worker_connections   2000;
}

Envoy の場合は,ハードウェアスレッドに対してワーカースレッドを作成し,ワーカースレッドはノンブロッキング処理をすると書いてある.それ以上は書いてなく,詳しい解説は Envoy 公式ブログを参照する必要がある.例えば「Main / Worker / File Flush」という個別のスレッドがあったり,アクセスログを出力するときはロックを取得したり,スレッドをうまく活用する仕組みの名前が「Thread Local Storage (TLS)」だったり,Envoy のスレッドモデルに興味があれば読んでおくと良さそう.

blog.envoyproxy.io

Step.3 「Server Configuration」

以下の nginx.conf を読むと,server_name に指定されたドメインから 8080 Port でリクエストを受けることになる.

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

もし同じ設定を Envoy に設定する場合,envoy.yamllisteners にポート番号を設定する.ドメインはここには設定せず,次の filters に設定する.前回と似ているため,envoy.yaml は見慣れてきたような気がする.

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

Step.4 「Location Configuration」

引き続き nginx.conf を読み解いていく.以下の設定は / にリクエストをすると http://targetCluster/ に転送される.

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

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

もし同じ設定を Envoy に設定する場合,envoy.yamlfiltersenvoy.http_connection_manager を使う.これは前回も出てきた HTTP を制御するフィルタで,ここにドメインとリクエストを受けるプレフィックスを設定している.転送する設定は次の clusters に設定するため,ここでは Cluster 名を設定している.

filter_chains:
- filters:
  - name: envoy.http_connection_manager
    config:
      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

前回は特に気にしなかったパラメータとして codec_type がある.ドキュメントを読むと「AUTO / HTTP1 / HTTP2」を設定できる.基本的にはデフォルトの AUTO で良さそうだけど,例えば HTTP1 を設定すると,通信を HTTP/1.1 に強制できる.

www.envoyproxy.io

次に domains で,これは virtual_hosts の中で必須のパラメータになっている.one.example.com のように正確なドメイン名を設定しても良いし,* を使って部分一致を設定することもできる.

www.envoyproxy.io

Step.5 「Proxy and Upstream Configuration」

nginx.confupstream を設定すると,クラスタを表現できる.今回は IP アドレスを2個設定している.

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

転送する設定は 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 }}
  ]

まず,type にサービスディスカバリの種類を設定する.デフォルトのパラメータは STATIC で,リスト全てを対象とする.今回は STRICT_DNS が設定されているため,名前解決をする.正直今回の構成だと STATIC で良いのではないか?と思うけど,STRICT_DNS になっている理由や,もう1個類似したパラメータとして LOGICAL_DNS もあり,このあたりは別途調べないとクリアにならなそう.

lb_policy には負荷分散のタイプを指定する.今回はデフォルトのパラメータ ROUND_ROBIN でラウンドロビンになっているけど,他にも LEAST_REQUESTRING_HASH など,気になるタイプが多く使えるようになっている.

www.envoyproxy.io

Step.6 「Logging Access and Errors」

最後はログ設定となり,今回は envoy.file_access_log を使って stdout に出力する.なお,ログ設定は filters に設定するため,抜粋すると以下のようになる(access_log 以外は省略している).format を設定すると項目をカスタマイズできるし,json_format を設定すると JSON 形式で出力できる.

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

www.envoyproxy.io

www.envoyproxy.io

Step.7 「Launching」

作成した envoy.yaml を試す.今回は以下の構成図のように Envoy コンテナと HTTP コンテナを2個起動する.HTTP コンテナは katacoda/docker-http-server となり,これは Go の net/http パッケージを使って,ホスト名を表示するだけの実装になっている.

$ docker run --name proxy1 -p 80:8080 --user 1000:1000 -v /root/envoy.yaml:/etc/envoy/envoy.yaml envoyproxy/envoy
$ docker run -d katacoda/docker-http-server
$ docker run -d katacoda/docker-http-server

f:id:kakku22:20191201025819p:plain

実際に Envoy コンテナに Host Header 付きでリクエストをする.レスポンスのホスト名(コンテナ ID)を見ると,ラウンドロビンになっていることがわかる(date は修正している).

$ curl -H "Host: one.example.com" localhost -i
HTTP/1.1 200 OK
date: Sat, 30 Nov 2019 17:00:00 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: 1c7446d81170</h1>

$ curl -H "Host: one.example.com" localhost -i
HTTP/1.1 200 OK
date: Sat, 30 Nov 2019 17:00:00 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: 8c2ba95ccec2</h1>

さらに今回は envoy.yaml でドメインを設定しているため,Host Header なしでリクエストをすると,404 になった.

$ curl localhost -i
HTTP/1.1 404 Not Found
date: Sat, 30 Nov 2019 17:00:00 GMT
server: envoy
content-length: 0

まとめ

  • 「Try Envoy」のコンテンツ「Migrating from NGINX to Envoy Proxy」を試した
  • 個人的に慣れている nginx.conf の設定項目を envoy.yaml に書き換えていくことにより,理解を深められた
  • Envoy の設定はバリエーションが多く,ドキュメントを読みながらどんどん試したいと思う

Try Envoy 関連

nginx でアクセスログを JSON フォーマットにする場合は「escape=json」を設定する

nginx で nginx.confaccess_loglog_format を設定すると,ログフォーマットをカスタマイズできる.log_format のデフォルト設定は combined だけど,ログ集計などを考慮して,JSON フォーマットに変更する場面も多いと思う.例えば,以下のように log_format を設定すると,JSON フォーマットでアクセスログを出力できる.

log_format json '{'
    '"time": "$time_local",'
    '"remote_addr": "$remote_addr",'
    '"host": "$host",'
    '"remote_user": "$remote_user",'
    '"status": "$status",'
    '"server_protocol": "$server_protocol",'
    '"request_method": "$request_method",'
    '"request_uri": "$request_uri",'
    '"request": "$request",'
    '"body_bytes_sent": "$body_bytes_sent",'
    '"request_time": "$request_time",'
    '"upstream_response_time": "$upstream_response_time",'
    '"http_referer": "$http_referer", '
    '"http_user_agent": "$http_user_agent",'
    '"http_x_forwarded_for": "$http_x_forwarded_for",'
    '"http_x_forwarded_proto": "$http_x_forwarded_proto"'
'}';

access_log /var/log/nginx/access.log json;

アクセスログに " が含まれる場合

例えば,アクセスログに " が含まれる場合,自動的に \x22 に変換されてしまう.検証として,以下のように User Agent に kakaka"kakku と設定し,nginx にリクエストを送る.

$ curl -H 'User-Agent: kakaka"kakku' http://www.example.com/

すると,以下のように出力される(アクセスログを整形している).User Agent を http_user_agent の値で確認すると,確かに kakaka\x22kakku になっている.なお,今回は例として " で検証をしているけど,正確に言うと " 以外にも「RFC 4627」で定義された文字は変換されてしまう.

{
    "time": "22/Nov/2019:00:00:00 +0900",
    "remote_addr": "x.x.x.x",
    "host": "www.example.com",
    "remote_user": "-",
    "status": "200",
    "server_protocol": "HTTP/1.1",
    "request_method": "GET",
    "request_uri": "/",
    "request": "GET / HTTP/1.1",
    "body_bytes_sent": "0",
    "request_time": "0.000",
    "upstream_response_time": "-",
    "http_referer": "-",
    "http_user_agent": "kakaka\x22kakku",
    "http_x_forwarded_for": "x.x.x.x",
    "http_x_forwarded_proto": "http"
}

解決策 : escape=json を設定する

ドキュメントを読むと,「nginx 1.11.8(2016年12月リリース)」から使える escape パラメータを使って,escape=json を設定すると,今回の件は解決する.

nginx.org

escape=json を設定し,最終的な nginx.conf は以下のようになる.

log_format json escape=json '{'
    '"time": "$time_local",'
    '"remote_addr": "$remote_addr",'
    '"host": "$host",'
    '"remote_user": "$remote_user",'
    '"status": "$status",'
    '"server_protocol": "$server_protocol",'
    '"request_method": "$request_method",'
    '"request_uri": "$request_uri",'
    '"request": "$request",'
    '"body_bytes_sent": "$body_bytes_sent",'
    '"request_time": "$request_time",'
    '"upstream_response_time": "$upstream_response_time",'
    '"http_referer": "$http_referer", '
    '"http_user_agent": "$http_user_agent",'
    '"http_x_forwarded_for": "$http_x_forwarded_for",'
    '"http_x_forwarded_proto": "$http_x_forwarded_proto"'
'}';

access_log /var/log/nginx/access.log json;

もう1度,User Agent に kakaka"kakku と設定し,nginx にリクエストを送ると,http_user_agent の値が kakaka\"kakku となり,うまくエスケープされて出力できるようになった.なお,remote_user など,もともと値が - だった部分もブランクになっていて,ここはドキュメントには仕様が書かれてなく,理由までは追えなかった.

{
    "time": "22/Nov/2019:00:00:00 +0900",
    "remote_addr": "x.x.x.x",
    "host": "www.example.com",
    "remote_user": "",
    "status": "200",
    "server_protocol": "HTTP/1.1",
    "request_method": "GET",
    "request_uri": "/",
    "request": "GET / HTTP/1.1",
    "body_bytes_sent": "0",
    "request_time": "0.000",
    "upstream_response_time": "",
    "http_referer": "",
    "http_user_agent": "kakaka\"kakku",
    "http_x_forwarded_for": "x.x.x.x",
    "http_x_forwarded_proto": "http"
}

まとめ

nginx でアクセスログを JSON フォーマットにする場合,「nginx 1.11.8(2016年12月リリース)」から使える escape パラメータを使って,escape=json を設定する.数年前から使えるパラメータだとしても,無限に知らないことはあるし,引き続き学んだことをブログにアウトプットしていくぞ!