redis-cluster の環境を docker-compose/circle ci 上に構築する
はじめに
redis-cluseter の環境を docker-compose
と circle ci
の両方の環境で用意する必要があったが、参考になるコードが少なかったので記事にした。
docker-compose up
をするだけで、ローカルに redis クラスターの環境ができて、circle ci 上でも同様の環境を扱えれるようにする。
コード
サンプルコードはこちら
docker-compose up で redis-cluster を利用する
いきなり、docker-compose.yamlの全体を貼り付け
version: "3.7"
services:
app:
build:
context: .
target: builder
command: go run main.go
environment:
REDIS_CLUSTER_1_NODES: "redis_cluster1:7000,redis_cluster1:7001,redis_cluster1:7002,redis_cluster1:7003,redis_cluster1:7004,redis_cluster1:7005"
REDIS_CLUSTER_2_NODES: "redis_cluster2:7010,redis_cluster2:7011,redis_cluster2:7012,redis_cluster2:7013,redis_cluster2:7014,redis_cluster2:7015"
depends_on:
- redis_cluster1
- redis_cluster2
container_name: app
redis_cluster1:
build:
context: .
dockerfile: DockerfileRedisCluster
ports:
- 7000-7005:7000-7005
environment:
CLUSTER_PORTS: "7000 7001 7002 7003 7004 7005"
SLAVES_PER_MASTER: 1
volumes:
- ./tmp/redis/redis_cluster1:/data
container_name: redis_cluster1
redis_cluster2:
build:
context: .
dockerfile: DockerfileRedisCluster
ports:
- 7010-7015:7010-7015
environment:
CLUSTER_PORTS: "7010 7011 7012 7013 7014 7015"
SLAVES_PER_MASTER: 1
volumes:
- ./tmp/redis/redis_cluster2:/data
container_name: redis_cluster2
redis_cluster1 と redis_cluster2 が参照している DockerfileRedisCluster
はこんなやつ
リポジトリ上にある redis-cluster の設定ファイルと、クラスター作成スクリプトを redis のコンテナへコピーし、スクリプトを実行している。
FROM redis:5.0.6
RUN apt-get update && apt-get install gettext-base
COPY ./docker/redis/redis.conf.tmpl /usr/local/etc/redis/redis.conf.tmpl
COPY ./docker/redis/init.sh /init.sh
COPY ./DockerfileRedisCluster ./tmp/redis/redis_cluster1/dump-* /usr/local/etc/redis/tmp/redis_cluster1/
CMD ["/bin/bash", "/init.sh"]
redis-cluster の設定ファイルはこんな感じでテンプレート化して使いまわせるようにしている。
port ${PORT}
pidfile "/var/run/redis_${PORT}.pid"
logfile "redis-${PORT}.log"
dbfilename "dump-${PORT}.rdb"
appendonly yes
appendfilename "appendonly-${PORT}.aof"
cluster-enabled yes
cluster-config-file nodes-${PORT}.conf
cluster-node-timeout 5000
daemonize yes
redisクラスターの作成スクリプトはこんな感じ。
#!/bin/bash
set -euxo pipefail
for port in $CLUSTER_PORTS; do
mkdir -p /usr/local/etc/redis/"${port}"
PORT=${port} envsubst < /usr/local/etc/redis/redis.conf.tmpl > /usr/local/etc/redis/"${port}"/redis.conf
done
chown -R redis.redis /usr/local/etc/redis
create_nodes_command=""
for port in $CLUSTER_PORTS; do
create_nodes_command="${create_nodes_command}redis-server /usr/local/etc/redis/${port}/redis.conf && "
done
nodes=""
for port in $CLUSTER_PORTS; do
IP=$(hostname -i)
nodes="$nodes ${IP}:${port}"
done
if ! ls /usr/local/etc/redis/tmp/redis_cluster1/dump-*.rdb >/dev/null 2>&1; then
bash -c "${create_nodes_command}yes yes | redis-cli --cluster create${nodes} --cluster-replicas ${SLAVES_PER_MASTER} && tail -f /dev/null"
else
bash -c "${create_nodes_command}tail -f /dev/null"
fi
環境変数の CLUSTER_PORTS
に渡された数だけ、 redis クラスターに参加させるノードを作成し、そのノードでクラスターを作成(redis-cli --cluster create
)している。
redis-cli --cluster create
のコマンド実行には redis ノードの IP が必要なので、 hostname -i
を実行して実行中のコンテナの IP を取得して動的にコマンド文字列を生成している。
クラスター作成後は tail -f /dev/null
をすることで、プロセス終了からのコンテナ終了とならないようにしている。
環境変数 SLAVES_PER_MASTER
で、マスターひとつに対して、スレーブをいくつ作るかを設定できる。
redis クラスターは最低マスターが三つ必要なので、 --cluster-replicas
を1にしたいなら、ひとつのクラスターにノードは6つ必要になってくる。
※マスターが三つより少ない状態で redis-cli --cluster create
するとこんなエラーでクラスタ作成に失敗する。
*** ERROR: Invalid configuration for cluster creation.
*** Redis Cluster requires at least 3 master nodes.
*** This is not possible with 4 nodes and 1 replicas per node.
*** At least 6 nodes are required.
クラスター作成済の状態での起動処理
上述した docker/redis/init.sh
のここの分岐が何をしているかというと、redis クラスターを一度でも構築済みかどうかを判定し、一度でも構築済みの場合は redis-cli --cluster create
は実行しないという制御をしている。
なぜ、こういう制御が必要かというと、 redis-cli --cluster create
でクラスターを構築済の状態で、再度 redis-cli --cluster create
を実行してしまうと Node xxx.xxx.xxx.xxx:xxxx is not empty. Either the node already knows other nodes (check with CLUSTER NODES) or contains some key in database 0.
のエラーが発生する。クラスターをすでに構築済の場合は redis-server
コマンドで redis を立ち上げるだけでいいため。
if ! ls /usr/local/etc/redis/tmp/redis_cluster1/dump-*.rdb >/dev/null 2>&1; then
bash -c "${create_nodes_command}yes yes | redis-cli --cluster create${nodes} --cluster-replicas ${SLAVES_PER_MASTER} && tail -f /dev/null"
else
bash -c "${create_nodes_command}tail -f /dev/null"
fi
上記の if 文に至るまでの流れを説明すると、 docker-compose.yml
のなかで redis のデータをこのように永続化している。
一度でも docker-compose up
をして、 redis を起動していると、ローカルの tmp 配下には redis の永続化のためのデータが保存されている。
volumes:
- ./tmp/redis/redis_cluster1:/data
volumes:
- ./tmp/redis/redis_cluster2:/data
tmp 配下に保存される redis のデータ永続化ファイル
tmp
└── redis
├── redis_cluster1
│ ├── appendonly-7000.aof
│ ├── appendonly-7001.aof
│ ├── appendonly-7002.aof
│ ├── appendonly-7003.aof
│ ├── appendonly-7004.aof
│ ├── appendonly-7005.aof
│ ├── dump-7000.rdb
│ ├── dump-7001.rdb
│ ├── dump-7002.rdb
│ ├── dump-7003.rdb
│ ├── dump-7004.rdb
│ ├── dump-7005.rdb
│ ├── nodes-7000.conf
│ ├── nodes-7001.conf
│ ├── nodes-7002.conf
│ ├── nodes-7003.conf
│ ├── nodes-7004.conf
│ ├── nodes-7005.conf
│ ├── redis-7000.log
│ ├── redis-7001.log
│ ├── redis-7002.log
│ ├── redis-7003.log
│ ├── redis-7004.log
│ └── redis-7005.log
└── redis_cluster2
├── appendonly-7010.aof
├── appendonly-7011.aof
├── appendonly-7012.aof
├── appendonly-7013.aof
├── appendonly-7014.aof
├── appendonly-7015.aof
├── dump-7010.rdb
├── dump-7011.rdb
├── dump-7012.rdb
├── dump-7013.rdb
├── dump-7014.rdb
├── dump-7015.rdb
├── nodes-7010.conf
├── nodes-7011.conf
├── nodes-7012.conf
├── nodes-7013.conf
├── nodes-7014.conf
├── nodes-7015.conf
├── redis-7010.log
├── redis-7011.log
├── redis-7012.log
├── redis-7013.log
├── redis-7014.log
└── redis-7015.log
なので、ローカルのtmp配下に 上記のファイルがある場合は Redis クラスターをすでに構築済という判定ができる。 tmp 配下のファイルであればなんでもいいので、今回は redis-cluster1 配下の rdb ファイルの存在有無をクラスター構築済みかどうかの判定に利用した。
ローカルのtmp配下に永続化された redis の rdb ファイルは 以下の記述で redis のコンテナ内にコピーしている。
COPY ./DockerfileRedisCluster ./tmp/redis/redis_cluster1/dump-* /usr/local/etc/redis/tmp/redis_cluster1/
COPY する際に注意するところが、 rdb ファイルはまだ一回も redis コンテナを起動していない場合は存在しないファイルなのでシンプルに、 COPY ./tmp/redis/redis_cluster1/dump-7000.rdb /usr/local/etc/redis/tmp/redis_cluster1/
という書き方をすると、ファイルが存在しない場合では COPY コマンドでエラーになってしまう。
なので以下のような、ちょっとハックっぽい書き方をしている。
COPY <必ず存在するファイル(ローカル)> <存在するかもしれないファイル(ローカル)> <コピー先(コンテナ内)>
こういう書き方をすることで、ファイルが存在する場合はコピーするし、存在しない場合はコピーしない、という COPY ができる。
こうして redis コンテナ内にコピーした rdb ファイルを冒頭の if 判定で利用している。
アプリケーション側
redis-cluster を利用するアプリケーション(golang)のコードはこんな感じ。
go-redis を使って接続している。
package main
import (
"context"
"fmt"
"log"
"os"
"strings"
"github.com/go-redis/redis/v8"
)
func main() {
ctx := context.Background()
if err := connectToRedis(ctx); err != nil {
log.Fatalf("main process: %v", err)
}
}
func connectToRedis(ctx context.Context) error {
cluster1Nodes := strings.Split(os.Getenv("REDIS_CLUSTER_1_NODES"), ",")
cluster2Nodes := strings.Split(os.Getenv("REDIS_CLUSTER_2_NODES"), ",")
cluster1Client := redis.NewClusterClient(&redis.ClusterOptions{
Addrs: cluster1Nodes,
})
cluster2Client := redis.NewClusterClient(&redis.ClusterOptions{
Addrs: cluster2Nodes,
})
pong, err := cluster1Client.Ping(ctx).Result()
if err != nil {
return fmt.Errorf("Could not connect to redis cluster1!: %w ", err)
} else {
fmt.Printf("redis cluster 1: %s\n", pong)
}
pong, err = cluster2Client.Ping(ctx).Result()
if err != nil {
return fmt.Errorf("Could not connect to redis cluster2!: %w ", err)
} else {
fmt.Printf("redis cluster 2: %s\n", pong)
}
return nil
}
redis.ClusterOptions.Addrs には、 host:port
の配列を渡してあげる必要がある。元となるデータは環境変数で渡すようにしている
REDIS_CLUSTER_1_NODES: "redis_cluster1:7000,redis_cluster1:7001,redis_cluster1:7002,redis_cluster1:7003,redis_cluster1:7004,redis_cluster1:7005"
REDIS_CLUSTER_2_NODES: "redis_cluster2:7010,redis_cluster2:7011,redis_cluster2:7012,redis_cluster2:7013,redis_cluster2:7014,redis_cluster2:7015"
名前解決後のホスト名を渡すようにしているので、 docker-compose.yaml の redis のコンテナの container_name
のところでクライアント側から接続する際のホスト名を設定している。
で、 redis クラスターへの接続後は Ping を使って接続確認をしている
pong, err := cluster1Client.Ping(ctx).Result()
if err != nil {
return fmt.Errorf("Could not connect to redis cluster1!: %w ", err)
} else {
fmt.Printf("redis cluster 1: %s\n", pong)
}
pong, err = cluster2Client.Ping(ctx).Result()
if err != nil {
return fmt.Errorf("Could not connect to redis cluster2!: %w ", err)
} else {
fmt.Printf("redis cluster 2: %s\n", pong)
}
docker-compose up
で起動後、 main.go を実行すれば、アプリケーションのコードから redisクラスターに接続できていることがわかる。
$ docker-compose run app go run main.go
Starting redis_cluster1 ... done
Starting redis_cluster2 ... done
Creating redis-cluster-sample_app_run ... done
redis cluster 1: PONG
redis cluster 2: PONG
こんな感じで cluster nodes
cluster info
コマンドを実行すれば、ちゃんとクラスターが作成されていることも確認できる。
$ docker-compose exec redis_cluster1 redis-cli -p 7000 -c cluster nodes
e3b2b3445800f58daddc6b83f4b5a7ee17243bd1 172.28.0.3:7003@17003 slave 5865a3c0ff8dc2e74b1051736e8d416b637faaed 0 1615115528937 4 connected
4b0da518d2f221162da6a110e76b89ba60a8006f 172.28.0.3:7002@17002 master - 0 1615115528000 3 connected 10923-16383
3857ca35c96a6b36cb31868b1601a105ac0c16ff 172.28.0.3:7005@17005 slave 078792f30dd6fd46cdcb1aacee7255d99388c1e1 0 1615115529943 6 connected
fe317e6850f2bb19bcf1a2ec4fb3cf7928745013 172.28.0.3:7004@17004 slave 4b0da518d2f221162da6a110e76b89ba60a8006f 0 1615115529000 5 connected
5865a3c0ff8dc2e74b1051736e8d416b637faaed 172.28.0.3:7001@17001 master - 0 1615115529000 2 connected 5461-10922
078792f30dd6fd46cdcb1aacee7255d99388c1e1 172.28.0.3:7000@17000 myself,master - 0 1615115529000 1 connected 0-5460
$ docker-compose exec redis_cluster1 redis-cli -p 7000 -c cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:1
cluster_stats_messages_ping_sent:11238
cluster_stats_messages_pong_sent:11182
cluster_stats_messages_sent:22420
cluster_stats_messages_ping_received:11177
cluster_stats_messages_pong_received:11238
cluster_stats_messages_meet_received:5
cluster_stats_messages_received:22420
$ docker-compose exec redis_cluster2 redis-cli -p 7010 -c cluster nodes
61f9b35aa9623fa991c4c0e455d034673f9beeea 172.28.0.2:7014@17014 slave acfe3198808b320f2ce7124eb9e53440c7a04331 0 1615115597000 5 connected
acfe3198808b320f2ce7124eb9e53440c7a04331 172.28.0.2:7012@17012 master - 0 1615115597000 3 connected 10923-16383
f2fd8a857168791d532a404ce1065b11301a7c7b 172.28.0.2:7010@17010 myself,master - 0 1615115596000 1 connected 0-5460
c9c587fc5118d1fed58fdb9e26d53c56920609ba 172.28.0.2:7013@17013 slave 411de9950f84e6ad7398e909df3eaac60dffd50d 0 1615115597530 4 connected
411de9950f84e6ad7398e909df3eaac60dffd50d 172.28.0.2:7011@17011 master - 0 1615115598538 2 connected 5461-10922
130cf369f6fe5f57bc6a4031f84c9f02125c5880 172.28.0.2:7015@17015 slave f2fd8a857168791d532a404ce1065b11301a7c7b 0 1615115598034 6 connected
$ docker-compose exec redis_cluster2 redis-cli -p 7010 -c cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:1
cluster_stats_messages_ping_sent:11412
cluster_stats_messages_pong_sent:11364
cluster_stats_messages_sent:22776
cluster_stats_messages_ping_received:11359
cluster_stats_messages_pong_received:11412
cluster_stats_messages_meet_received:5
cluster_stats_messages_received:22776
circle ci で redis-cluster を利用する
いきなり、 config.yaml の全体を貼り付け
version: 2.1
references:
working_directory: &working_directory /go/src/redis-cluster-sample
executors:
build-excutor:
working_directory: *working_directory
docker:
- image: circleci/golang:1.15.3
- image: pokotyan/redis-cluster-sample
name: "redis_cluster1"
environment:
CLUSTER_PORTS: "7000 7001 7002 7003 7004 7005"
SLAVES_PER_MASTER: 1
- image: pokotyan/redis-cluster-sample
name: "redis_cluster2"
environment:
CLUSTER_PORTS: "7010 7011 7012 7013 7014 7015"
SLAVES_PER_MASTER: 1
commands:
build_and_test:
steps:
- checkout
- restore_cache:
name: Restore go modules cache
keys:
- go-modules-cache-v1-{{ checksum "go.sum" }}
- run:
name: Set up dependent module
command: go mod download
- run:
name: Connection to redis-cluster
command: go run main.go
- save_cache:
name: Save go modules cache
key: go-modules-cache-v1-{{ checksum "go.sum" }}
paths:
- "/go/pkg/mod"
jobs:
build_and_test:
executor:
name: build-excutor
environment:
- REDIS_CLUSTER_1_NODES: "redis_cluster1:7000,redis_cluster1:7001,redis_cluster1:7002,redis_cluster1:7003,redis_cluster1:7004,redis_cluster1:7005"
- REDIS_CLUSTER_2_NODES: "redis_cluster2:7010,redis_cluster2:7011,redis_cluster2:7012,redis_cluster2:7013,redis_cluster2:7014,redis_cluster2:7015"
steps:
- build_and_test
workflows:
version: 2
build_and_test:
jobs:
- build_and_test
circle ci上で利用する redis クラスターのイメージは docker hub
に事前にあげておいたものを利用している。
こんな感じで、事前にイメージをレジストリにあげておく。
docker build -f DockerfileRedisCluster -t pokotyan/redis-cluster-sample .
docker push pokotyan/redis-cluster-sample:latest
circle ci の設定で特筆するところとしては、アプリケーション側は redis クラスターに対して
- REDIS_CLUSTER_1_NODES: "redis_cluster1:7000,redis_cluster1:7001,redis_cluster1:7002,redis_cluster1:7003,redis_cluster1:7004,redis_cluster1:7005"
- REDIS_CLUSTER_2_NODES: "redis_cluster2:7010,redis_cluster2:7011,redis_cluster2:7012,redis_cluster2:7013,redis_cluster2:7014,redis_cluster2:7015"
このホスト名で接続できるようにする必要があるため、 docker.name
のところでコンテナ名を明示してあげている。
これで、circle ci 上で立ち上げた reids クラスターにアプリケーション側から接続できるのが確認できる。
Discussion
とても参考になりました!ありがとうございます
1点
この部分が引数3つになっているので真ん中のはいらないかな?と思います