理系学生日記

おまえはいつまで学生気分なのか

Consulでservice meshを実現するConnectのSidecar Proxy/Intention

Consul Cluster を Service Mesh にする Connect という機能を備えた Consul 1.2.0 がリリースされました。ただし、Public Beta の位置づけで、正式版は 2018 年末とか。

記事によれば、Service Mesh は以下の 3 つ を提供すべきとされています(色々な意見があります)が、このうちの Segmentation が Connect で解決されるということになります。

  1. Discovery: サービスは互いに見付けあうことができる
  2. Configuration: ランタイム設定を Central Source から抽出できる
  3. Segmentation: サービスの通信は許可・暗号化されたものである

今日は、とりあえず Getting Started を読んだので、ちょっと実際に動かしてみようと思います。 作成した環境については、一番最後に。

Consul Connect とは

Connect を簡単に言うと、主として以下の 2 つの機能を備えた、Consul の新規機能です。

  • mTLS (mutual TLS) で暗号化、および相互認証されたサービス間通信
  • intension と呼ばれる、論理的なサービス名を用いた通信制御 (accept/deny)

これらを実現するため、Connect は Consul 組み込みの Sidecar Proxy を持ちます。この Proxy が mTLS 回りや通信制御の面倒を見てくれるので、既存のアプリケーションは変更不要という形になります。 この Proxy もかなり性能面には気を配られて設計・実装されているようですが、さらに性能を上げたい場合は、Consul Connect の API をアプリに組込む (Native Integration) ことも可能です。

上記のとおり、結構な部分を mTLS で行うため、証明書運用が絡んできます。

この証明書に関しても、かなり考慮されているようでして、Connect ではビルトインの CA を持っていますし、root 証明書のローテーションを(cross-sign を行うことで) 無停止で実現することもできたり、 Vault と連携することも可能です。

また、ここでの証明書は SPIFFE に準拠しているっぽくて、他の SPIFFE 準拠のシステムとも相互運用可能…なように読めます。

Connect を使ってみる

それでは、公式のConsul Dockerイメージ を使って、実際の Connect を使ってみようと思います。

Connect の有効化

Connect の機能は、-dev のコマンドラインオプションを付与しない限りデフォルトで無効化されています。 以下のような設定を Consul Server 側で行うことで、Connect を利用可能になります。

{
  "connect": { 
    "enabled": true 
  }
}

公式の consul イメージは、環境変数 CONSUL_LOCAL_CONFIG に設定を記述できるようになっています。このため、以下のような設定をしておけば、docker run 時に Connect を有効化できます。

export CONSUL_LOCAL_CONFIG='{"connect": { "enabled": true }}'

サーバ側で Sidecar Proxy を立ち上げる

まず最初に、以下のような consul server x 1、client x 2 のクラスタを構築しました。

# consul members
Node     Address          Status  Type    Build  Protocol  DC   Segment
server1  172.18.0.3:8301  alive   server  1.2.0  2         dc1  <all>
client1  172.18.0.4:8301  alive   client  1.2.0  2         dc1  <default>
client2  172.18.0.2:8301  alive   client  1.2.0  2         dc1  <default>

ここで、サーバ から service 設定を投入し、設定をリロードします。

[server]# cat <<EOF > /consul/config/socat.json
{
  "service": {
    "name": "socat",
    "port": 8181,
    "connect": { "proxy": {} }
  }
}
EOF
[server]# consul reload

ここの connect の設定が、Connect に関する設定です。ここでは、socat サービス用に Manged Proxy を立ち上げるように指示しています。

このタイミングで ps を確認すると、consul connect proxy が起動していることが分かります。これが sidecar proxy (Managed Proxy)ですね。

[server]# ps | grep [p]roxy
   46 consul     0:00 /bin/consul connect proxy

Sidecar Proxy を使ってみる

それでは早速、Sidecar Proxy を使ってみます。

まずは Proxy 先のサービスとして、server 上で Echo サーバを立ち上げます。 これで、server 上では Echo サーバと Sidecar Proxy の 2 つが立ち上がった状態です。

[server]$ socat -v tcp-l:8181,fork exec:"/bin/cat"

それでは、この Echo サーバに対して、client1 から接続を行ってみましょう。

クライアント側で、コマンドラインで Sidecar Proxy を作る

接続のためには、client1 上においても Sidecar Proxy を立ち上げる必要があります。これを簡易的に行うのが、consul connect proxy コマンドです。さっき、自動的に立ち上がっていたヤツですね。

[client1]$ consul connect proxy -service web -upstream socat:9191 &

上記の設定により、client1 にも web Service の元で、 Proxy 先として socat サービスが指定された Sidecar Proxy が 9191 ポートで立ち上がります。

それでは、この 9191 番に対して接続してみます。

[client1]$ nc localhost 9191
hello world
hello world
^D

上記のように、client1 が localhost:9191 向けに接続すると、それが socat サービスに接続され、応答が返却されています。

ここでのポイントは、client1 は、server というホストの情報を指定していないということ。socat という論理的なサービス名のみを指定すると、自動的に Service Discovery が行われ、server 上で動作している socat サービスに接続できていることが分かります。client1・server の Sidebar Proxy 間の通信は TLS で暗号化されているはずです。

クライアント側で、設定ファイルで Sidecar Proxy を作る

上記では、コマンドラインで Sidecar Proxy を投入しました。 次に、設定ファイルで同様の設定を行いたいと思います。

これまでの consul と同様のサービス設定ですが、connect プロパティ以下が Connect 用の新規設定項目になります。 upstreams がプロキシ先ですね。今度は 10,000 ポートで Listen させてみます。

[client1]# cat <<EOF > /consul/config/web.json
{
  "service": {
    "name": "web",
    "port": 8080,
    "connect": {
      "proxy": {
        "config": {
          "upstreams": [{
             "destination_name": "socat",
             "local_bind_port": 10000
          }]
        }
      }
    }
  }
}
EOF

あとは reload すれば、server 上の socat サービスに接続できることがわかります。

[client1]# consul reload
[client1]# nc localhost 10000
hello consul connect!
hello consul connect!

intention での通信制御

consul connect での通信制御は intention と呼ばれるもので行われます。

intention による通信制御は、Sidecar Proxy から見たときの inbound で行われ、そのタイミングは、mTLS での認証後です。このタイミングで、authorize API が呼ばれ、コネクションの確立可否が判定されます。

デフォルトでは、任意のサービスから任意のサービスに通信が可能になっています。

しかし、セキュリティにおいてはデフォルトは deny にせよってことになっているのでそこはご注意くださいませ…

今日は、デフォルトであろう allow の状態から、deny のルールを追加する形で進めます。

まず最初に現状を確認してみます。client1 では、web サービスが動作しています。

f:id:kiririmode:20180627175305p:plain

そして、cluster 全体で intention は設定されていません。このため、すべての通信は許可 (allow) されます。

[client1]# curl http://localhost:8500/v1/connect/intentions
[]

このように、intention については、このように REST API でも操作できますし、CLI や UI でも操作可能です。

この時点で、client1 からは socat サービスに接続ができています。

[client1]# nc localhost 10000
hello
hello

ここで、web -> socat への通信を禁止してみます。

[client1]# consul intention create -deny web socat
Created: web => socat (deny)

UI を見ても、intention が追加されていることが分かります。

f:id:kiririmode:20180627180005p:plain

さて、実際の通信はどうでしょうか。

[client1]# nc localhost 10000

接続できないまま終了し、intention が効いていることが分かります。 では、web サービスを立ち上げていない、client2 はどうでしょうか。

[client2]$ consul connect proxy -service kiririmode -upstream socat:8000 &
[client2]$ nc localhost 8000
hello world
hello world

ここでは、kiririmode というサービスをでっちあげましたが、そうすると正しく socat サービスに接続できていることが分かります。 すごくおもしろいです。。。

まぁ、consul でセキュリティを語る上では、intention の前に ACL 設定があるので、そこから攻めた方が良さそうという話はあるんですが。

まとめ

というわけで、とりあえずは Sidecar Proxy と Intention を使ってみました。

ドキュメントを見る限りにおいては、証明書運用まわりや、性能面でもかなり面白そうなことになってるみたいです。

動作確認環境を作る

ここでは、以下のような Dockerfile を作成の上、それを利用した consul cluster を docker-compose で作成しました。

FROM consul:1.2.0

RUN set -ex \
    && apk update \
    && apk add --no-cache --virtual .deps \
       socat \
       bind-tools \
       tcpdump

docker-compose.yaml は以下なかんじ。

version: '3.2'
services:
  server: &agent
    build:
      context: .
      dockerfile: Dockerfile
    image: kiririmode/consul:1.2.0
    command: "agent -server -ui -bootstrap-expect 1 -retry-join consul_server_1 -client 0.0.0.0"
    environment:
      - 'CONSUL_LOCAL_CONFIG={"connect": { "enabled": true }}'
    hostname: server1
    ports:
      - "8500:8500"
    volumes:
      - type: bind
        source: ./work
        target: /work
  client1: &client
    <<: *agent
    command: "agent -ui -client 0.0.0.0 -retry-join consul_server_1"
    hostname: client1
    ports:
      - "8501:8500"
  client2:
    <<: *client
    hostname: client2
    ports:
      - "8502:8500"