kakakakakku blog

Weekly Tech Blog: Keep on Learning!

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 を設定する.数年前から使えるパラメータだとしても,無限に知らないことはあるし,引き続き学んだことをブログにアウトプットしていくぞ!

Dockerfile に HEALTHCHECK を設定すると「ヘルスチェック機能」が使えるようになる

今回は Docker で使える「ヘルスチェック機能」を試す.Release Note を読むと,機能としては Docker 1.12 から使えるらしく,3年前からあったなんて...!仕組みとしては,Docker デーモンからコンテナに指定したコマンドを定期的に実行する.

Dockerfile 構文

「ヘルスチェック機能」を使う場合,まず DockerfileHEALTHCHECK を設定する.実行するコマンド以外に以下のオプションも設定できる.注意点として,--retries 以外は秒数を指定するため,表記は 5s のように単位も付ける.

  • --interval=DURATION (default: 30s)
  • --timeout=DURATION (default: 30s)
  • --start-period=DURATION (default: 0s)
  • --retries=N (default: 3)

以下にサンプルを載せる.

HEALTHCHECK --interval=5s --timeout=3s CMD curl -f http://localhost/ || exit 1

docs.docker.com

検証用コンテナイメージ

今回は検証用コンテナイメージを「計3種類」作成した.Alpine を使うため,事前に curl をインストールしておく必要がある.

  • kakakakakku/nginx:base
    • nginx:alpinecurl を追加した
  • kakakakakku/nginx:ok
    • nginx:alpinecurlHEALTHCHECK を追加した(ヘルスチェック成功
  • kakakakakku/nginx:ng
    • nginx:alpinecurlHEALTHCHECK を追加した(ヘルスチェック失敗

Dockerfile : kakakakakku/nginx:ok

nginx を使うため,DockerfileHEALTHCHECK を設定し,curl を実行する.今回は検証として,オプションは --interval=5s--timeout=3s にした.kakakakakku/nginx:ok は,デフォルトの index.html にアクセスできるため,ヘルスチェックに成功する.

FROM nginx:alpine

RUN apk add curl

HEALTHCHECK --interval=5s --timeout=3s CMD curl -f http://localhost/ || exit 1

Dockerfile : kakakakakku/nginx:ng

kakakakakku/nginx:ng は,誤った URL に curl を実行するため「終了コード 1」となり,ヘルスチェックに失敗する.

FROM nginx:alpine

RUN apk add curl

HEALTHCHECK --interval=5s --timeout=3s CMD curl -f http://localhost/ng || exit 1

詳細は GitHub を見てもらえればと!

github.com

コンテナを起動する

コンテナを起動し,docker ps コマンドを実行すると,以下のように STATUS「ヘルスチェック結果」を確認できる.通常は非表示だけど,今回は (healthy)(unhealthy) が表示されている.なお,起動直後は (health: starting) という表記になっていた.

$ docker run -d -p 8000:80 --name nginx01 kakakakakku/nginx:base
$ docker run -d -p 8001:80 --name nginx02 kakakakakku/nginx:ok
$ docker run -d -p 8002:80 --name nginx03 kakakakakku/nginx:ng

$ docker ps --format "table {{.Image}}\t{{.Status}}\t{{.Names}}"
IMAGE                    STATUS                     NAMES
kakakakakku/nginx:ng     Up 3 minutes (unhealthy)   nginx03
kakakakakku/nginx:ok     Up 3 minutes (healthy)     nginx02
kakakakakku/nginx:base   Up 3 minutes               nginx01

コンテナの起動時にヘルスチェックを設定する

実は docker run コマンドのオプションでヘルスチェックを指定することもできる.よって,DockerfileHEALTHCHECK が設定されていなくても「ヘルスチェック機能」は使える.オプションは以下のドキュメントに載っている.

  • --health-cmd
  • --health-interval
  • --health-retries
  • --health-start-period
  • --health-timeout

docs.docker.com

さっそく kakakakakku/nginx:base を使って,「ヘルスチェックに成功するコンテナ」「ヘルスチェックに失敗するコンテナ」を起動する.すると,DockerfileHEALTHCHECK を記載したときと同様に STATUS「ヘルスチェック結果」が表示された.オプションを柔軟に設定できるため,Dockerfile にベタ書きするよりも使えそう.

$ docker run -d -p 8003:80 --name nginx04 \
  --health-cmd 'curl -f http://localhost/ || exit 1' \
  --health-interval 5s \
  --health-timeout 3s \
  kakakakakku/nginx:base

$ docker run -d -p 8004:80 --name nginx05 \
  --health-cmd 'curl -f http://localhost/ng || exit 1' \
  --health-interval 5s \
  --health-timeout 3s \
  kakakakakku/nginx:base

$ docker ps --format "table {{.Image}}\t{{.Status}}\t{{.Names}}"
IMAGE                    STATUS                      NAMES
kakakakakku/nginx:base   Up 19 seconds (unhealthy)   nginx05
kakakakakku/nginx:base   Up 27 seconds (healthy)     nginx04
kakakakakku/nginx:ng     Up 11 minutes (unhealthy)   nginx03
kakakakakku/nginx:ok     Up 11 minutes (healthy)     nginx02
kakakakakku/nginx:base   Up 11 minutes               nginx01

ヘルスチェック結果でフィルタリングする

docker ps コマンドと --filter を組み合わせることにより,ヘルスチェック結果でコンテナをフィルタリングすることもできる.

$ docker ps --format "table {{.Image}}\t{{.Status}}\t{{.Names}}" --filter 'health = healthy'
IMAGE                    STATUS                    NAMES
kakakakakku/nginx:base   Up 2 minutes (healthy)    nginx04
kakakakakku/nginx:ok     Up 13 minutes (healthy)   nginx02

$ docker ps --format "table {{.Image}}\t{{.Status}}\t{{.Names}}" --filter 'health = unhealthy'
IMAGE                    STATUS                      NAMES
kakakakakku/nginx:base   Up 3 minutes (unhealthy)    nginx05
kakakakakku/nginx:ng     Up 14 minutes (unhealthy)   nginx03

docs.docker.com

ヘルスチェックログを確認する

docker inspect コマンドと --format を組み合わせることにより,ヘルスチェックログ(直近5件)を確認できる.以下はヘルスチェックに失敗した nginx03 のログとなり,curl の結果 404「終了コード 1」になっている(Output の結果が長く,横スクロールをする必要がある).

$ docker inspect --format='{{json .State.Health}}' nginx03 | jq .
{
  "Status": "unhealthy",
  "FailingStreak": 242,
  "Log": [
    {
      "Start": "2019-11-21T06:22:46.3344018Z",
      "End": "2019-11-21T06:22:46.611824Z",
      "ExitCode": 1,
      "Output": "  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current\n                                 Dload  Upload   Total   Spent    Left  Speed\n\r  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0\r  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0\ncurl: (22) The requested URL returned error: 404 Not Found\n"
    },
    {
      "Start": "2019-11-21T06:22:51.622651Z",
      "End": "2019-11-21T06:22:51.8756001Z",
      "ExitCode": 1,
      "Output": "  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current\n                                 Dload  Upload   Total   Spent    Left  Speed\n\r  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0\r  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0\ncurl: (22) The requested URL returned error: 404 Not Found\n"
    },
    {
      "Start": "2019-11-21T06:22:56.8871398Z",
      "End": "2019-11-21T06:22:57.0339383Z",
      "ExitCode": 1,
      "Output": "  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current\n                                 Dload  Upload   Total   Spent    Left  Speed\n\r  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0\r  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0\ncurl: (22) The requested URL returned error: 404 Not Found\n"
    },
    {
      "Start": "2019-11-21T06:23:02.046402Z",
      "End": "2019-11-21T06:23:02.4194353Z",
      "ExitCode": 1,
      "Output": "  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current\n                                 Dload  Upload   Total   Spent    Left  Speed\n\r  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0\r  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0\ncurl: (22) The requested URL returned error: 404 Not Found\n"
    },
    {
      "Start": "2019-11-21T06:23:07.4317862Z",
      "End": "2019-11-21T06:23:07.6609581Z",
      "ExitCode": 1,
      "Output": "  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current\n                                 Dload  Upload   Total   Spent    Left  Speed\n\r  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0\r  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0\ncurl: (22) The requested URL returned error: 404 Not Found\n"
    }
  ]
}

docs.docker.com

ヘルスチェック結果の変化を検知する

docker events コマンドと --filter を組み合わせることにより,Docker のイベント情報から「ヘルスチェック結果の変化」を検知できる.以下は nginx05 コンテナを起動し,unhealthy となったタイミングに取得したイベント情報となる.

$ docker events --filter 'type=container' --filter event=health_status
2019-11-21T15:44:18.141166300+09:00 container health_status: unhealthy xxxxx (image=kakakakakku/nginx:base, maintainer=NGINX Docker Maintainers <docker-maint@nginx.com>, name=nginx05)

docs.docker.com

まとめ

  • Docker で使える「ヘルスチェック機能」を試した
    • Dockerfile に設定することもできるし,docker run コマンドのオプションに設定することもできる
  • docker psdocker inspectdocker events コマンドを使うと「ヘルスチェック」に関する情報を取得できる
  • 注意点として,Docker 単体だとヘルスチェックに失敗して unhealthy になったとしても,コンテナ自体はそのまま残る

「Try Envoy」で Envoy を学ぼう!「Getting Started with Envoy」を試した

Envoy のサイトに「Try Envoy」という学習コンテンツがあり,現在は以下の「計11種類」のシナリオから選んで学べる.実際にはブラウザベースで進められる Katacoda の学習コンテンツが埋め込まれているため,特別な環境構築をせずに進められるのは便利.

  • Getting Started with Envoy(今回紹介する)
  • Migrating from NGINX to Envoy Proxy
  • Migrating from HAProxy to Envoy Proxy
  • Securing traffic with HTTPS and SSL/TLS
  • File Based Dynamic Routing Configuration
  • API Based Dynamic Routing Configuration
  • Detecting Down Services with Health Checks
  • Implementing Blue / Green Rollouts
  • Implementing Metrics and Tracing Capabilities
  • Debugging Envoy Proxy
  • Controlling load balancing policies

f:id:kakku22:20191117162057p:plain

学習コンテンツは「Try Envoy」からアクセスできるけど,当然ながら Katacoda にもある.個人的には画面レイアウトの広さという観点から Katacoda を使うと良いと思う.

www.envoyproxy.io

www.katacoda.com

「Try Envoy」のシナリオを数個試してみて,当然ながら手順通りには動くけど,Envoy 初学者には理解しにくそうな点もあった.そこで,学んだことを整理しつつ,イメージしにくそうな部分をまとめておこうと思う.

Getting Started with Envoy

今回は Envoy に入門するシナリオ「Getting Started with Envoy」を紹介する.Envoy を使ってリクエストを外部サービスに流したり,複数のアプリケーションにパスベースルーティングをする.

www.katacoda.com

手順は以下の「計4種類」ある.サイトに「Estimated Time: 10 minutes」と書いてあるけど,考えながら進めるとすぐに終わらないと思う.時間は参考レベルで見ておくと良さそう.

  • Step.1 「Create Proxy Config」
  • Step.2 「Start Proxy」
  • Step.3 「Admin View」
  • Step.4 「Route to Docker Containers」

Step.1 「Create Proxy Config」

最初は Envoy の挙動を設定する envoy.yaml を作成する.実際にエディタで envoy.yaml を書く場面はなく,手順上で Copy to Editor ボタンをクリックすると自動的にエディタに反映される仕組みになっている.Katacoda 便利!その前に基本的な用語を整理しておく.詳細はドキュメントを参照してもらえればと.

  • Listeners : リクエストを受ける設定
  • Filters : リクエストをフィルタする設定(複数のフィルタを設定できる)
  • Clusters : リクエストを転送する設定

そして envoy.yaml を読み解く前に構成図を整理しておく.コンテナとして起動した Envoy にリクエストを投げると,そのまま Google に転送される.Envoy を試す最も簡単な構成とも言える.

f:id:kakku22:20191117211922p:plain

envoy.yaml のポイントはザッと以下となる.

  • listeners
    • 10000 Port でリクエストを受ける
  • filter_chains
    • route_config にルーティング設定を書く
    • プレフィックスが / の場合に host_rewrite で HTTP Host Header 書き換える
    • service_google Cluster に転送する
static_resources:
  listeners:
  - 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
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: ["*"]
              routes:
              - match: { prefix: "/" }
                route: { host_rewrite: www.google.com, cluster: service_google }
          http_filters:
          - name: envoy.router

  clusters:
  - name: service_google
    connect_timeout: 0.25s
    type: LOGICAL_DNS
    dns_lookup_family: V4_ONLY
    lb_policy: ROUND_ROBIN
    hosts: [{ socket_address: { address: google.com, port_value: 443 }}]
    tls_context: { sni: www.google.com }

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

Envoy で HTTP を制御するフィルタ envoy.http_connection_manager の詳細は以下のドキュメントに書いてある.

www.envoyproxy.io

Step.2 「Start Proxy」

さっそく envoy.yaml を指定して Envoy コンテナを起動する.80 Port にリクエストをすると,Google に転送される.ホスト内だと curl localhost でも良いし,Katacoda だと https://xxx.environments.katacoda.com という一時的な URL も使えるため,どちらも使える.

$ docker run --name=proxy -d \
  -p 80:10000 \
  -v $(pwd)/envoy/envoy.yaml:/etc/envoy/envoy.yaml \
  envoyproxy/envoy:latest

Step.3 「Admin View」

次に Envoy の「Administration interface」機能を試す.UI は直感的に理解しにくく感じたけど,メトリクス/統計情報/ログなど,Envoy の状態を確認するときに使える.

$ docker run --name=proxy-with-admin -d \
    -p 9901:9901 \
    -p 10000:10000 \
    -v $(pwd)/envoy/envoy.yaml:/etc/envoy/envoy.yaml \
    envoyproxy/envoy:latest

f:id:kakku22:20191117181412p:plain

詳しくは以下のドキュメントに書いてある.

www.envoyproxy.io

Step.4 「Route to Docker Containers」

最後は Python アプリケーションと Envoy を組み合わせて使う.docker-compose.yml から構成を読み取ると以下となる.リクエストの URL を判断して service1service2 にパスベースルーティングをしていて,アプリケーション側は環境変数 SERVICE_NAME を表示する程度になっている.

f:id:kakku22:20191117211948p:plain

なお service1service2envoyproxy/envoy-alpine:latest イメージをベースに Python と Flask をインストールしているため,Envoy でよく使われる「サイドカーパターン」になっていない点に注意する.とは言え,今回のように Envoy を試すことが目的の場合は問題ないと言える.

version: '2'
services:

  front-envoy:
    build:
      context: .
      dockerfile: Dockerfile-frontenvoy
    volumes:
      - ./front-envoy.yaml:/etc/front-envoy.yaml
    networks:
      - envoymesh
    expose:
      - "80"
      - "8001"
    ports:
      - "8000:80"
      - "8001:8001"

  service1:
    build:
      context: .
      dockerfile: Dockerfile-service
    volumes:
      - ./service-envoy.yaml:/etc/service-envoy.yaml
    networks:
      envoymesh:
        aliases:
          - service1
    environment:
      - SERVICE_NAME=1
    expose:
      - "80"

  service2:
    build:
      context: .
      dockerfile: Dockerfile-service
    volumes:
      - ./service-envoy.yaml:/etc/service-envoy.yaml
    networks:
      envoymesh:
        aliases:
          - service2
    environment:
      - SERVICE_NAME=2
    expose:
      - "80"

networks:
  envoymesh: {}

front-envoyfront-envoy.yaml を以下に載せる.ポイントは filtersroutes に2種類の prefix が設定されている点で,URL によって転送する Cluster を変えている.

static_resources:
  listeners:
  - address:
      socket_address:
        address: 0.0.0.0
        port_value: 80
    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:
              - "*"
              routes:
              - match:
                  prefix: "/service/1"
                route:
                  cluster: service1
              - match:
                  prefix: "/service/2"
                route:
                  cluster: service2
          http_filters:
          - name: envoy.router
            config: {}
  clusters:
  - name: service1
    connect_timeout: 0.25s
    type: strict_dns
    lb_policy: round_robin
    http2_protocol_options: {}
    hosts:
    - socket_address:
        address: service1
        port_value: 80
  - name: service2
    connect_timeout: 0.25s
    type: strict_dns
    lb_policy: round_robin
    http2_protocol_options: {}
    hosts:
    - socket_address:
        address: service2
        port_value: 80
admin:
  access_log_path: "/dev/null"
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 8001

実際に起動すると,期待した通りにリクエストをルーティングできた.

$ docker-compose -f ~/envoy/examples/front-proxy/docker-compose.yml up -d

$ curl localhost:8000/service/1
Hello from behind Envoy (service 1)! hostname: 211d6aaf214c resolvedhostname: 172.19.0.3

$ curl localhost:8000/service/2
Hello from behind Envoy (service 2)! hostname: 30ce9b84004a resolvedhostname: 172.19.0.2

まとめ

  • Envoy のサイトに「Try Envoy」という学習コンテンツがある
    • 現在は「計11種類」のシナリオから選べる
  • 今回は「Getting Started with Envoy」を試した
    • リクエストを外部サービスに流したり,複数のアプリケーションにパスベースルーティングをしたり,基本的な動作確認はできる
  • まだ「Envoy の良さ」を感じられる内容ではなく,引き続き「Try Envoy」を進めていく