consul connect, L7 traffic management, nomad consul connect を試す(2)

前回の続きから。

ここから行なっていく手順も前回の consul connect を構築した状態から再開です。

今回は、 consul 1.6 から追加された L7 traffic management と、 nomad 0.10 の consul connect integration を試すものになる。

consul connect は envoy の他に組み込みプロキシなども使用できるから、この先以降は現時点では envoy 限定の機能となってくる。 実現している機能を考えれば、これは確かに consul 内で実装するのは相当な手間だろうとは思う。

Consul L7 traffic management

概ねこの辺のガイドなどを参考にしている。

www.consul.io

www.consul.io

最初にいろいろ眺めていても全然理解できなかったのだけど、読んだり試したりしていてなんとなくどういうことをするものかがわかってきた。

L7 traffic management は config entry という機能を使って、サイドカーへ適用するする設定を与えることで実現していく。

config entry は Kind と Name 属性と Kind ごとに個別の情報を持つ、JSONまたはHCLファイルで、 Kindが設定の種類、 Nameが設定を適用するサービス名となる(全体の設定を示す、Globalもある)

service-defaults

まずは、Kind=service-defaults で、サービスごと proxy の設定を L4(TCP) から L7(http) に切り替える config を 各サービスごとに設定する。

# service_default.hcl
Kind = "service-defaults"
Name = "service-a"
Protocol = "http"
# service_default2.hcl
Kind = "service-defaults"
Name = "service-b"
Protocol = "http"

このファイルを、 config consul write コマンドで投入する。

consul config write l7/service_default.hcl
consul config write l7/service_default2.hcl

service-router

次が、L7の振り分けを設定する Kind, "service-router" の設定となる。

service-router を設定すると、 今まで単一のサービスにしか接続できなかった upstream のコネクションが、 URLやHTTPヘッダなどの内容に応じて、複数のサービスにルーティングできるようになる。

これの恩恵が受けやすいのが gRPCだろう。

gRPCは、コネクションを サーバーアドレス+ポートで生成して、PRC処理ごとにコネクションを使い回す。 その際に service-router を使うことで、サイドカーへの単一のコネクションだけで複数のサービスに接続可能にできる。 これが、L4レベルプロキシだとサービスごとに異なるポートで異なるコネクションが必要となってしまう。

今回のサンプルはこれを模したルーティングを行なってみる。

(この方法が正しいのかはわからないが) service-a の sidecar の upstream の接続先を自分自身に向けて再設定する。

{
    "ID": "service-a",
    "Name": "service-a",
    "Address": "127.0.0.1",
    "Port": 3000,
    "Meta": {
      "version": "v1"
    },
    "Check": {
      "DeregisterCriticalServiceAfter": "90m",
      "HTTP": "http://127.0.0.1:3000/health",
      "Interval": "10s"
    },
    "Connect": {
        "SidecarService":{
            "Proxy":{
                "upstreams": [
                  {
                    "destination_name": "service-a",
                    "local_bind_port": 9000
                  }
                ]
            }
        }
    }
  }
curl -X PUT http://localhost:8500/v1/agent/service/register -d @service_a_in_l7.json

次に、以下の内容で service-router を作成し config を適用する。

# service_a_router.hcl
kind = "service-router"
name = "service-a"
routes = [
  {
    match {
      http {
        path_prefix = "/hello_b"
      }
    }
    destination {
      service = "service-b"
    }
  },
  # NOTE: a default catch-all will send unmatched traffic to "service-a"
]
consul config write l7/service_a_router.hcl

この内容は、service-a のサイドカーに来た "/hello_b" へのリクエストは、service-b に向け、それ以外はデフォルト(service-a)に向けるという意味になる。

この状態で、service-a にアクセスしてみる。

curl localhost:3000/hello_a
{"message_b":"service_b(1) up at Tue, 17 Dec 2019 04:32:58 GMT","message_a":"service_a up at Tue, 17 Dec 2019 04:32:57 GMT"}

無事に、service-a に向かう upstream 出会っても、特定のパスなら service-bに向けれたことが確認できた。 gRPCでも同様に動くだろう。

この時、service-a の envoy は次のように設定されている。 抜粋で示す。

    {
     "version_info": "00000009",
     "listener": {
      "name": "service-a:127.0.0.1:9000",
      "address": {
       "socket_address": {
        "address": "127.0.0.1",
        "port_value": 9000
       }
      },
      "filter_chains": [
       {
        "filters": [
         {
          "name": "envoy.http_connection_manager",
          "config": {
           "tracing": {
            "random_sampling": {},
            "operation_name": "EGRESS"
           },
           "http_filters": [
            {
             "name": "envoy.router"
            }
           ],
           "rds": {
            "route_config_name": "service-a",
            "config_source": {
             "ads": {}
            }
           },
           "stat_prefix": "upstream_service-a_http"
          }
         }
        ]
       }
      ]
     },
     "last_updated": "2019-12-17T05:11:49.438Z"
    },

  {
   "@type": "type.googleapis.com/envoy.admin.v2alpha.RoutesConfigDump",
   "dynamic_route_configs": [
    {
     "version_info": "00000009",
     "route_config": {
      "name": "service-a",
      "virtual_hosts": [
       {
        "name": "service-a",
        "domains": [
         "*"
        ],
        "routes": [
         {
          "match": {
           "prefix": "/hello_b"
          },
          "route": {
           "cluster": "service-b.default.dc1.internal.06e91f3c-fffd-1989-629f-d8b3edd7db5c.consul"
          }
         },
         {
          "match": {
           "prefix": "/"
          },
          "route": {
           "cluster": "service-a.default.dc1.internal.06e91f3c-fffd-1989-629f-d8b3edd7db5c.consul"
          }
         }
        ]
       }
      ],
      "validate_clusters": true
     },
     "last_updated": "2019-12-17T05:11:49.440Z"
    }
   ]
  }

まずは、リスナーについて HTTPモードでの接続を示す、 http-connection-managerが使われるようになった。 これは proxyの設定を http に変更したためだ(ただし、 service-routerの設定を行うまでは tcpのままだったので、必要となるまではなるべくTCPのままでやろうとするのだろう)

このリスナーには rds(envoyの動的なルーティング設定) が設定されていて、その設定先である route_config には、service-router で設定した通りのルートが設定されているのが見て取れる。

つまりは、 consul config の適用により、Kindに合わせて envoy の設定を xDSで変更していくのが、L7 traffic management の裏側だ。

service-splitter, service-resolver

service-router がリスナー側(入口側)の設定なら、 service-splitter, service-resolver はクラスター側(出口側, upstream側) の振り分け設定だ。 service-b の2つのサービスへの振り分けは均等だったが、これらを使うことで配分を変えることができる。

実は service-b2つのサービスにはそれぞれ、 versionという meta情報を付与していて、それぞれ v1, v2 という内容を設定してある。 (前回の記事の service-bの設定ファイルを見ると書いてある)

meta情報など様々な条件を使って同一のサービスであっても振り分け先ごとにグルーピングできる、これを subset と呼ぶようになっていて、 service-resolver は subset の設定を行う kind だ。

#service_b_resolver.hcl
kind           = "service-resolver"
name           = "service-b"
default_subset = "v1"
subsets = {
  "v1" = {
    filter = "Service.Meta.version == v1"
  }
  "v2" = {
    filter = "Service.Meta.version == v2"
  }
}

これは サービスの meta情報の値によって、v1, v2 という subset を設定している。

https://www.consul.io/docs/agent/config-entries/service-resolver.html を見ると、別サービスへの振り分けなどもできるようだし、色々な使い道がありそう。

service-splitter はサブセットごとに振り分ける量を設定するものになる。

#service_b_splitter.hcl

kind = "service-splitter"
name = "service-b"
splits = [
  {
    weight         = 90
    service_subset = "v1"
  },
  {
    weight         = 10
    service_subset = "v2"
  },
]

これは、90:10 で v1 と v2 の振り分け量を設定していることになる。

これらの config を設定してみる。

consul config write l7/service_b_resolver.hcl
consul config write l7/service_b_splitter.hcl

テストしてみる。

$ curl localhost:3000/hello_a
{"message_b":"service_b(1) up at Tue, 17 Dec 2019 04:32:58 GMT","message_a":"service_a up at Tue, 17 Dec 2019 04:32:57 GMT"}
$ curl localhost:3000/hello_a
{"message_b":"service_b(1) up at Tue, 17 Dec 2019 04:32:58 GMT","message_a":"service_a up at Tue, 17 Dec 2019 04:32:57 GMT"}
$ curl localhost:3000/hello_a
{"message_b":"service_b(1) up at Tue, 17 Dec 2019 04:32:58 GMT","message_a":"service_a up at Tue, 17 Dec 2019 04:32:57 GMT"}
$ curl localhost:3000/hello_a
{"message_b":"service_b(1) up at Tue, 17 Dec 2019 04:32:58 GMT","message_a":"service_a up at Tue, 17 Dec 2019 04:32:57 GMT"}
$ curl localhost:3000/hello_a
{"message_b":"service_b(1) up at Tue, 17 Dec 2019 04:32:58 GMT","message_a":"service_a up at Tue, 17 Dec 2019 04:32:57 GMT"}
$ curl localhost:3000/hello_a
{"message_b":"service_b(1) up at Tue, 17 Dec 2019 04:32:58 GMT","message_a":"service_a up at Tue, 17 Dec 2019 04:32:57 GMT"}
$ curl localhost:3000/hello_a
{"message_b":"service_b(1) up at Tue, 17 Dec 2019 04:32:58 GMT","message_a":"service_a up at Tue, 17 Dec 2019 04:32:57 GMT"}
$ curl localhost:3000/hello_a
{"message_b":"service_b(1) up at Tue, 17 Dec 2019 04:32:58 GMT","message_a":"service_a up at Tue, 17 Dec 2019 04:32:57 GMT"}
$ curl localhost:3000/hello_a
{"message_b":"service_b(2) up at Tue, 17 Dec 2019 04:32:58 GMT","message_a":"service_a up at Tue, 17 Dec 2019 04:32:57 GMT"}
$ curl localhost:3000/hello_a
{"message_b":"service_b(1) up at Tue, 17 Dec 2019 04:32:58 GMT","message_a":"service_a up at Tue, 17 Dec 2019 04:32:57 GMT"}

明らかに、service-b_2 にはつながりづらくなった。

さて、同じく envoy の設定を覗いてみよう。

     "route_config": {
      "name": "service-b",
      "virtual_hosts": [
       {
        "name": "service-b",
        "domains": [
         "*"
        ],
        "routes": [
         {
          "match": {
           "prefix": "/"
          },
          "route": {
           "weighted_clusters": {
            "clusters": [
             {
              "name": "v1.service-b.default.dc1.internal.06e91f3c-fffd-1989-629f-d8b3edd7db5c.consul",
              "weight": 8500
             },
             {
              "name": "v2.service-b.default.dc1.internal.06e91f3c-fffd-1989-629f-d8b3edd7db5c.consul",
              "weight": 1500
             }
            ],
            "total_weight": 10000
           }
          }
         }
        ]
       }

service-b のルート設定に、 weighted_clusters という設定が増えていて、service-splitter で設定した重みが設定されていることがわかる。

振り分け先のクラスタも、v1, v2 で分かれている。 subset は、envoy上では単純なクラスタ定義となっていることがわかる。

splitter によりconsul上で1つのサービスも、envoy上では分かれている。これはとても参考になる実装だと思う。

というわけで、L7 traffic management を試した。 envoy の xDS コントロールプレーンを実装した自分にとって、consul の xDS 実装は非常に参考になる部分が多い。

簡易的な isito として使える。

nomad

続いて、 nomadnomad の consul connect 連携を試す。

これを使うと、docker container を1つずつあげて、手動で consul に登録する必要はないし、 サイドカーの立ち上げも不要になる。
さらに、 host ネットワークではなく、 nomad が管理しているコンテナネットワーク内でコンテナ間通信を行うようにもできる。

今までの手順で起動した Docker コンテナは全て落とし、 consul からもサービスを全て削除する。

consul services deregister --id=service-a
consul services deregister --id=service-a-sidecar-proxy
consul services deregister --id=service-b
consul services deregister --id=service-b_2
consul services deregister --id=service-b-sidecar-proxy
consul services deregister --id=service-b_2-sidecar-proxy

docker stop service_a service_b service_b_2 sidecar_a sidecar_b sidecar_b_2

これから行う手順は概ね、次のガイドに従って行ったものになる。

www.nomadproject.io

さらりと、 consul へのパスが通っていることや、 nomad は root 権限で起動することなどハマりポイントが書いてあるので注意が必要だ。

CNI (Container Network Interface, https://github.com/containernetworking/cni ) plugin のインストールが必要となり、 またカーネル設定の一部変更も必要となる。 nomad は CNI経由で iptable を操作して、サイドカーと docker コンテナを localhost で通信可能にしている。

( CNI についてはいまいち理解不足だが、 k8s のネットワーキングでも使っているコンテナ間のネットワーク設定のための仕様であり、 network namespace をいい感じに作って、コンテナにアタッチするみたいなものだろうか)

# CNI plugin
curl -L -o cni-plugins.tgz https://github.com/containernetworking/plugins/releases/download/v0.8.1/cni-plugins-linux-amd64-v0.8.1.tgz
sudo mkdir -p /opt/cni/bin
sudo tar -C /opt/cni/bin -xzf cni-plugins.tgz

以下の設定ファイルを、"/etc/sysctl.d/99-sysctl.conf" などに作って、 sudo sysctl -p

net.bridge.bridge-nf-call-arptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1

この状態で、 nomadルート権限で起動する。

nomad では docker コンテナを、 ジョブ定義ファイルを作って投入する。k8sでいう所の、deploymentSet だ。

今までに作ってきた、service-a, service-b 2つのジョブ定義ファイルを作ってみる。

#service_a.job
job "service_a_job" {
  datacenters = ["dc1"]
  group "service-a" {
    count = 1
    network {
      mode = "bridge"
      # this setting host network 3000 forward to bridge network 3000.
      port "http" {
        static = 3000
        to     = 3000
      }
    }
    service {
      name = "service-a"
      port = "3000"
      connect {
        sidecar_service {
            proxy {
                upstreams {
                    destination_name = "service-a"
                    local_bind_port = 9901
                }
          }
        }
      }
    }
    task "service-a" {
      driver = "docker"
      env {
          #. Note that dashes (-) are converted to underscores (_) in environment variable
        SIDECAR_URL = "http://${NOMAD_UPSTREAM_ADDR_service_a}"
      }
      config {
        image = "<your_registory>/service_a"
      }
      resources {
            memory = 100
      }
    }
  }
}

nomad 0.10 から、最上位のgroup 節 に network 節が書けるようになった。 以前では、nomad にはコンテナネットワークを管理する機能はなく、事前に自分で docker network や vxlan を構成しておく必要があったが、 今回からは nomad 独自のコンテナネットワークが暗黙的にあるようだ。 このネットワークに外部からブリッジするポートとして静的に3000ポートを指定している。

また、service 節に connect 節も書けるようになった。 consul connet と同様の upstream の設定を行なっている。 前述の L7 traffic manager とも併用可能なので、自分自身に upstream を向けている。
また、service節では他に、 network で設定した 3000版ポートを、service-a に流すように port を設定している。

task 節の env でコンテナの環境変数を与えている。 http://${NOMAD_UPSTREAM_ADDR_service_a} のように、nomad が管理している属性値を実行時に与えることができる。ちなみに ${NOMAD_UPSTREAM_ADDR_service_a} は実際には、 localhost:9901 であり、 nomad が CNI を使って独自のコンテナネットワークを行なっていることがわかった。

というわけで、同様に service-b のジョブ定義ファイルも作成する。

# service-b.job
job "service_b_job" {
  datacenters = ["dc1"]
  group "service-b" {
    count = 2
    network {
      mode = "bridge"
    }
    service {
      name = "service-b"
      port = "3001"
      connect {
        sidecar_service {}
      }
    }

    task "service-b" {
      driver = "docker"
      env {
        APP_ID = "${NOMAD_ALLOC_ID}"
      }
      config {
        image = "<your-registory>/service_b"
      }
      resources {
          memory = 100
      }
    }
  }
}

ちなみに、docker イメージは Docker レジストリにpush しておく必要があり、ローカルの docker image は使用できない。 ただし、一度 tar に出力することでtar からロードも可能となっている。 その辺の方法は、github の全量のソースを参照してほしい。

こららのファイルを nomad に適用すると、 noamd は (今回はサーバーは1つしかないが) nomad エージェントを導入している各サーバーから、 docker コンテナを起動するサーバーを探して割り当てる。

nomad job run nomad/service_a.job
nomad job run nomad/service_b.job

consul を見てみると、 nomad アイコンのついた、service-a, service-bとそのサイドカーがサービスとして登録されていることがわかる。 このように、nomad は consul に 自身が起動したコンテナの情報を登録し、ヘルスチェックなどを移譲している。

f:id:kencharos:20191217200537p:plain

nomad を見てみると、サービスを3つ起動していることがわかる。

f:id:kencharos:20191217200932p:plain

service-a ジョブの内容を見てみると、サイドカーコンテナも一緒に起動し、 3000ポートが service-aに割り当ててあることがわかり、

f:id:kencharos:20191217201123p:plain

service-bには ポートが割り当てられてない(外部からアクセスできない) ことがわかる。

f:id:kencharos:20191217201331p:plain

サイドカーのポートは公開されているように見えるが、サイドカーへのアクセスは mTLSのため、クライアント証明書が必要なので、実質的に外部からアクセスできない。

では、疎通できるか試してみる。

$ curl localhost:3000/hello_a
{"message_b":"service_b(a7885ab8-cef6-5aed-7c25-b7c5978bcd3f) up at Tue, 17 Dec 2019 11:42:00 GMT","message_a":"service_a up at Tue, 17 Dec 2019 11:43:11 GMT"}
$ curl localhost:3000/hello_a
{"message_b":"service_b(89984200-a56b-5696-2ee3-c5fa19512145) up at Tue, 17 Dec 2019 11:42:00 GMT","message_a":"service_a up at Tue, 17 Dec 2019 11:43:11 GMT"}

service_b のIDには、nomad のコンテナごとにユニークに振られる allocation id を設定していて、ちゃんと分散してアクセスしていることがわかる。

(なお、consul からの続きでこの手順を試してたらうまく動かなかなったので、一度 consulのtmpファイルを破棄してから再起動し、 サイド L7 traffic manager の config を再設定した。 そのため、nomad の手順を実行する際に、サービスだけでなく、L7 traffic manager の設定も全て消したほうがよかったと思われる。)

というわけで、 nomad と consul connect を使うことで、サイドカーの設定を設定だけで追加でき、独自ネットワークでの動作やポート管理からも解放された。

簡易的な k8s, istio として使えそうだ。

ちなみに、 docker ps した結果は次のようになった。

CONTAINER ID        IMAGE                                      COMMAND                  CREATED             STATUS              PORTS                    NAMES
a915b780f123        service_a                                  "docker-entrypoint.s…"   8 minutes ago       Up 8 minutes                                 service-a-c4cd69c1-50ac-981f-2748-2b53a176d106
cf072b8f461c        envoyproxy/envoy:v1.11.2                   "/docker-entrypoint.…"   8 minutes ago       Up 8 minutes                                 connect-proxy-service-a-c4cd69c1-50ac-981f-2748-2b53a176d106
9d51d11c526f        gcr.io/google_containers/pause-amd64:3.0   "/pause"                 8 minutes ago       Up 8 minutes                                 nomad_init_c4cd69c1-50ac-981f-2748-2b53a176d106
662baf7eab4a        service_b                                  "docker-entrypoint.s…"   10 minutes ago      Up 10 minutes                                service-b-89984200-a56b-5696-2ee3-c5fa19512145
265d8defce94        service_b                                  "docker-entrypoint.s…"   10 minutes ago      Up 10 minutes                                service-b-a7885ab8-cef6-5aed-7c25-b7c5978bcd3f
53481e6f6282        envoyproxy/envoy:v1.11.2                   "/docker-entrypoint.…"   10 minutes ago      Up 10 minutes                                connect-proxy-service-b-a7885ab8-cef6-5aed-7c25-b7c5978bcd3f
befc5e30e23d        envoyproxy/envoy:v1.11.2                   "/docker-entrypoint.…"   10 minutes ago      Up 10 minutes                                connect-proxy-service-b-89984200-a56b-5696-2ee3-c5fa19512145
f798256ae7e7        gcr.io/google_containers/pause-amd64:3.0   "/pause"                 10 minutes ago      Up 10 minutes                                nomad_init_89984200-a56b-5696-2ee3-c5fa19512145
565e75715f36        gcr.io/google_containers/pause-amd64:3.0   "/pause"                 10 minutes ago      Up 10 minutes                                nomad_init_a7885ab8-cef6-5aed-7c25-b7c5978bcd3f

自分のイメージと サイドカーenvoy 以外に、謎の pause-amd64 とかいうコンテナも立っていたり、CNI経由のためか コンテナがどのネットワークにも属してないなど、 nomad のシンプルさが若干失われたような気がした。

まとめ

L7 traffic management はいい感じだった。 mesh gateway を使うことで、マルチクラウドやオンプレ、k8sなんかともいい感じに連携できそうだ。 適用したconfigの内容が UI とかで参照できるともっといいんだけどね。

nomad の consul connect は可能性は感じるが今の所まだ荒削りだなと思った。 CNIによるネットワーキングは便利だが、果たしてそれは nomad に求めていたことなのかと思ったり、 ルート権限いるの? とか、envoy をもう少し自由にいじりたいとか、まだ洗練される余地があるような気がしている。

とはいえ、オンプレでも使えるサービスメッシュ、コンテナクラスタとしては魅力的なので、今後もアップデートを見守っていきたいと思う。