Docker ComposeとRedisクラスタ4.0の新機能「port-forwarding」を使いRedisクラスタを作成する方法（どんなサイズにも対応可）

#どんなサイズにも対応可。Docker ComposeとRedisクラスタ4.0「port-forwarding」を使って、Redisクラスタを作成しよう！
**Redis4.0の一般向け提供が先日開始されたことにより、Dockerユーザー待望のRedisクラスタ機能、「NAT/port-forwarding」のサポートが始まりました。そのおかげで、Dockerのエフェメラルポートを使ってRedisクラスタを実行させることが可能となりました。これで「host-mode networking」もしくは「静的に割り当てられたポート」を使わずに、Docker上でRedisクラスタを実行できるようになりました。**

*English original available [here](https://goo.gl/At7P3W).*

:point_up: **要約**
Redis 4.0 より 「Dockerネットワークのカプセル化」のサポートが始まり、よりダイナミックなデプロイが可能となった。

    $ git clone https://github.com/aprice-/redisclustercompose
    $ cd redisclustercompose
    $ docker-compose up -d --scale redis=9
   
## Github

:computer: Github参照: [https://github.com/aprice-/redisclustercompose](https://github.com/aprice-/redisclustercompose)

## 4.0では何が変わった？

設定パラメーターが新しく３つ加わりました。その結果、Redisによりクラスタの他メンバーに告知する「IPアドレス」「データポート（例:6379）」「クラスタポート（例:16379）」がコントロール可能に！最も注目すべき点は、ポート単位での設定が可能になったことです。これでポート毎に10,000番地飛ばして設定する縛りが無くなりました。

* `cluster-announce-ip`: The IP address to announce.
* `cluster-announce-port`: The data port to announce.
* `cluster-announce-bus-port`: The cluster bus port to announce.

## どうしてそれが重要なの？

Dockerの魅力はダイナミックに、且つ摩擦なくサービスが組み立てられるその柔軟性にありますが、Redisクラスタ3.0ではそれを可能にする主要な２つの機能「NAT/port-forwarding」と「ネットワークのカプセル化」が使用不可であるため。

### NATの背景としてのRedisクラスタ3.0（うまくいかないバージョン）

Redis 3.0はデフォルトで、自動認識したローカルIPアドレスと設定ポートを通知します。そのため、デフォルトの`bridge`ネットワークでRedisを実行すると、「内部プライベートIPアドレス」と「Internal-Onlyポート」がピアとクライアントに通知されます。

![Redis Cluster (3.0) behind NAT on bridge network](https://get-reddie.com/assets/img/blog/redis4/redis3_nat.png)

この際、NATの背後のRedisノードがクライアントに接続不可能な「MOVED redirection」で応答してしまう可能性があります。

### Redisクラスタ3.0におけるHost-mode networkingを使った回避策（なんとかうまくいくバージョン）
  
Docker上でRedisクラスタ3.0にできることは、「host-modeネットワーク」を使ってそれぞれのノードのポートを静的に定義することだけです。Host-modeネットワークでは、ホストのネットワーク・スタックを使い、コンテナがホストの「パブリックIPアドレス」を通知します。Dockerはポート転送を行わないため、ポート・コリジョンを管理するのはユーザー次第となります。

![Redis Cluster (3.0) using host-mode networking](https://get-reddie.com/assets/img/blog/redis4/redis3_host.png)

たとえば任意のDockerホストで *n*個のRedisノードを実行するとすると、それぞれのノードはポートが選択され且つ`redis.conf`内で定義されている必要があります。６ノード以上でのクラスタ構成が推奨されていることを考え合わせると、これではすぐに扱いが厄介になると想像されます。それと同時に、Docker SwarmとDocker設定が冗長になってしまいます。

### NATの背景としてのRedis クラスタ 4.0

Redisクラスタ 4.0では新しいオプションが利用可能です。Redisが任意の「IPアドレス」「ポート」「クラスタポート」を通知できるため、NAT状態に対応した設定が可能です。もうhost-modeネットワークを使う必要はありません。ただしRedisプロセスを始める前に、「IPアドレス」「ポート」「クラスタポート」をノードの `redis.conf`に設定しなければいけません。

![Redis Cluster (4.0) behind NAT on bridge network](https://get-reddie.com/assets/img/blog/redis4/redis4_nat.png)

これでMOVED redirectionはどれも正しいIPとポートを指すようになりました。静的設定でRedisクラスタを実行しているユーザーには、これで十分かと思います。しかしクラスタのスケジューラーを使うのであれば、事前にこれらの設定を行わなければならないのは障害です。残りのブログでは、動的にRedisクラスタのノードをプロビジョニングできる「ディスカバリー」のメカニズムを追求していきます。

## NATのディスカバリー

クラスタの各ノードは、コンテナ毎にNAT設定が行え、且つ対応する`redis.conf`値が設定できて初めて、単独の共有コンテナ・設定を有することができます。「Docker compose」「Docker swarm」および他の「クラスタリング・ソリューション」のサービス定義は、たった一行のRedisコンテナ・テンプレートに集約できます。たとえば`docker-compose --scale`のたった一行で、Redisサービスはスケール可能になったのです！ 

Dockerがコンテナ起動時にランダムに割り当てられたポートを制御するので、どのポートが割り当てられたかを確認するには、「Docker API」をクエリーする必要があります。いくつかのアプローチが考えられますが、ここでは公式の「Redis Docker Image」をカスタマイズし、簡単なHTTPディカバリー・エージェントに統合させたやり方を探っていきます。

![Redis Cluster (4.0) behind NAT on bridge network](https://get-reddie.com/assets/img/blog/redis4/redis_discover.png)

### 1. エージェント

フル機能のDockerソケットで不必要に全Redisコンテナをさらしてしまうよりも、Docker APIをHTTPの薄いラッパーで包む方が理にかなっていると考えます。その他にも利用できるサービス・ディスカバリのオプションがたくさんあります。ここでは非常にシンプルでカスタムな例を紹介します。

エージェント・コンテナは公式な「nodejs alpine docker image」に基づいて作られた「nodejs express application」です。利用できる唯一のルートは、コンテナIDでパラメタ化されたGETのみです。 目標は、Docker APIを使ってリクエストされたコンテナIDをもとに、動的に割り当てられたポートを探すことです。

エージェントは[request](https://www.npmjs.com/package/request)を使って、Docker Unix Socket経由でDocker APIをクエリーします。Dockerの「unix socket」はDocker volumeとして起動時にコンテナに渡されます。Docker APIからコンテナ情報を得るのは極めて簡単です：

    request({
        method: 'GET',
        url: `http://unix:/var/run/docker.sock:/containers/${id}/json`,
        headers: {host: 'localhost'},
        json: true
    }, (error, res, body) => {
        if (error) {
            callback(error, null);
            return;
        }
        if (res.statusCode >= 200 && res.statusCode <= 299) {
            callback(null, body);
        } else {
            callback(new Error(body.message));
        }
    })

Docker経由でどのコンテナの情報も入手可能になったので、あとはexpressルートを定義して、リターンされたオブジェクトからポート情報を読み出します。

    app.get('/:id', (req, res) => {  
        dockerInspect(req.params.id, (error, container) => {
            if (error) {
                res.status(400);
                res.send(error);
            } else {
                let portInfo = container.NetworkSettings.Ports["6379/tcp"];
                let cportInfo = container.NetworkSettings.Ports["16379/tcp"];
    
                if (portInfo && cportInfo) {
                    let port = portInfo[0].HostPort;
                    let cport = cportInfo[0].HostPort;
    
                    res.send(`${clusterAnnounceIp}:${port}@${cport}`);
                } else {
                    res.send("");
                }
            }
        });
    });

Redis4.0の`CLUSTER NODES`と同じフォーマットで結果が返ってきます： `<IP>:<port>@<cluster-bus-port>`.

    $ curl http://192.168.50.5:3000/2bddf88b0904
    
    192.168.50.5:34176@34175

ここではRedisコンテナを調整して、この情報を用いてNAT設定を行います。

:bulb: 必要なら環境変数`INTERFACE` を用い、告知するネットワーク・インタフェースを選んでください。

### 2. Redisコンテナ

公式な[library/redis](https://hub.docker.com/_/redis/)コンテナのalpine-variantに基づき、Redisコンテナは`ENTRYPOINT`を上書きし、起動時にディスカバリー・サービスのクエリーを行って、NAT設定を見つけます。

ディスカバリー・エージェントはhost-mode networkingで正しい「announce IPアドレス」を取得するので、コンテナ内からDockerホストにクエリーするだけでいいのです：

    gateway=$(/sbin/ip route|awk '/default/ { print $3 }')          # Find the gateway IP address
    
    network_info=$(curl -s http://${gateway}:3000/$(hostname))      # Query the discovery agent 
                                                                    # returns: 192.168.50.5:34176@34175
    
    cluster_announce_ip=$(echo ${network_info} | cut -d ':' -f 1)   # Cut out the different parts of <IP>:<port>@<cluster-bus-port>`
    ports=$(echo ${network_info} | cut -d ':' -f 2)
    cluster_announce_port=$(echo ${ports} | cut -d '@' -f 1)
    cluster_announce_bus_port=$(echo ${ports} | cut -d '@' -f 2)

そして新しい`cluster-announce-ip`、`cluster-announce-port`そして`cluster-announce-bus-port`設定を`redis-server`実行に加えてください。

    set -- redis-server "$@" "--cluster-announce-port ${cluster_announce_port}" "--cluster-announce-bus-port ${cluster_announce_bus_port}" "--cluster-announce-ip ${cluster_announce_ip}"
    
### 3. クラスタ作成

発見したNAT設定により、ひとたびクラスタの全メンバーがオンラインになれば、クラスタ作成可能になります。公式なRedisクラスタのRubyアドミニストレーション・スクリプトは`redis-trib.rb` で、クラスタの作成例は以下です。

    ruby redis-trib.rb create --replicas 1 <ip>:<port>[] 

Docker cliを併用すれば、どんなサイズのクラスタも作成できます。

    docker ps -q -f label=redis |                                   # loop over each container labeled 'redis'
    {
      while read x; do                                              # inspect each container for its private IP address
        private_ip=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $x) 
        cluster_hosts=\"$cluster_hosts $private_ip:6379\"           # Append it to the list of cluster hosts
      done
      ruby redis-trib.rb create --replicas 1 $cluster_hosts         # Form the cluster
    }

これで各メンバーが引き合わされ、初期ハッシュ・スロット・オーナーシップの設定が行われます。これでクラスタは使用可能になりました！

## Docker Composeを使って

Docker Composeを使えば、個々のサービスを設定できます。コンテナに追加インスタンスを立ち上げることで、スケールも自在です。上に記した静的設定の制限のために、Redisクラスタ3.0では、単独サービスのもとで複数ノードを実行することは不可能です。Redis4.0では、このディスカバリー方法で各コンテナが同一のテンプレートを共有することができるようになったので、可能となったのです。

    redis:
      image: redis:discover
      build: ./redis
      command: --cluster-enabled yes --bind 0.0.0.0 
      ports: 
       - 6379
       - 16379
      labels:
       - redis
      depends_on:
       - discover
       
これでサービスに`docker-compose up --scale redis=<num>`を使って、お望みの数だけコンテナ・インスタンスを立ち上げることが可能になりました。

    $ docker-compose up -d --scale redis=6
    
    $ docker exec redisclustercompose_redis_1 exec redis-cli cluster nodes
    
    48836a86da5d54d56393f866befde47efacc256d 192.168.50.5:34238@34237 master - 0 1504307943992 2 connected 5461-10922
    53eb5604cf752e3811b5db01bddb1eb5d580d142 192.168.50.5:34244@34243 slave 48836a86da5d54d56393f866befde47efacc256d 0 1504307944996 5 connected
    8b2b5e4eec8d25eab59bdedd0eb356465f3a4102 192.168.50.5:34246@34245 master - 0 1504307942000 1 connected 0-5460
    dc71aa602eb6ab6f0d4c71208061b7230f53dc50 192.168.50.5:34240@34239 slave 8b2b5e4eec8d25eab59bdedd0eb356465f3a4102 0 1504307943000 4 connected
    e1ab6ad9c5b806f564486b1bb8be2567244d9f5c 192.168.50.5:34242@34241 master - 0 1504307944000 3 connected 10923-16383
    3b061c92f30154046c2eba2f858a81ff0c95d2c1 192.168.50.5:34236@34235 myself,slave e1ab6ad9c5b806f564486b1bb8be2567244d9f5c 0 1504307943000 6 connected
   
### Reddieバンドル

GithubのDocker Composeコードに、Reddieはあらかじめ含まれています。また、あなたが作成する新しいクラスターに繋がるようにも設定してあります。Docker Composeがコンテナをビルドし終わったら、`https://localhost` までナビゲートしてできたての作品に見惚れましょう！

## まとめ

Docker上の多くのRedisクラスタ・ディプロイメントにとって、新機能の「NAT/port-forwarding」は、ただ単にhost-mode networkingから`bridge`や`overlay` networkに変えられるということを意味するのでしょう。

しかし、このブログでみてきたようにSwarm・Kubernetes・Nomadなどのクラスタスケジューラーを用いてRedisクラスタのノードをスケジュールすると、Redis4.0の新機能をうまく融合させた形でRedisクラスタを扱うことが可能となります。   

実行時にディスカバリー・メカニズムを使ってRedisを再設定しなければならないのは、残念なことに非常に複雑なオーバーヘッドであります。もし（例えばインバウンドのコネクション情報を使ったり、クライアントコネクションにてクラスタのバスポートを提供したりして）Redisが動的に自身を再設定できるようになれば、Redisクラスタの動的スケジューリングはもっと簡単になるのではないでしょうか。

もちろん欠けている部分もまだまだあります。永続記憶装置、新しくスケジュールされたノードをクラスターに結合させたり、ハッシュスロットをリシャーディングしたりする、などが例として挙げられます。