A Day In The Life

とあるプログラマの備忘録

Kubernetes 上に Redis Sentinel を構築する

GCP環境でRedisを使う

GCP には Cloud Memorystore という Redis 互換のマネージドサービスがあるのですが、対応している Redis のバージョンが古い(4系)ので自前で GKE 上に Redis を立てることにしました。どうせ立てるなら master/slave 構成のほうが良いだろうということで Redis Sentinel にしました。

環境

  • GKE 1.13.6
  • Redis 5

Redis Sentinelって何?

Redis Sentinel は master/slave 構成の Redis の監視を行ってくれるサーバソフトウェアです。自動フェイルオーバー機能を持っています。

Redis Sentinelの構成

構成は以下のようになります。

sequence dialog

上の図では省略しましたが Sentinel は Sentinel 同士で繋がっていてすべての Redis を監視しています。

sequence dialog

Redisの設定

Redis の Config ファイルとコンテナ起動時に必要なスクリプトを作成します。

RedisのConfigファイルを作成する

Redis の master/slave 用の Config ファイルと Sentinel 用の Config ファイルを作成します。 はじめにマスター用の Config ファイル master.conf を作成します。

bind 0.0.0.0
port 6379

dir /redis-data

次にスレーブ用の Config ファイル slave.conf を作成します。

bind 0.0.0.0
port 6379

dir /redis-data

slaveof redis-0.redis 6379

最後に Sentinel 用の Config ファイル sentinel.conf を作成します。

bind 0.0.0.0
port 26379

sentinel monitor master redis-0.redis 6379 2
sentinel parallel-syncs master 1
sentinel down-after-milliseconds master 10000
sentinel failover-timeout master 20000

redis-0.redis をマスターに設定し sentinel でマスターを監視するように config に指定しています。 sentinel.confの設定内容については以下の記事が参考になります。

コンテナ起動時に実行するスクリプトを作成します

Redis コンテナ起動時に実行するスクリプト init.sh を作成します。どのコンテナをマスターにするか決定するためのものです。

#!/bin/bash
if [[ ${HOSTNAME} == 'redis-0' ]]; then
  redis-server /redis-config/master.conf
else
  redis-server /redis-config/slave.conf
fi

Sentinel 用のスクリプト sentinel.sh を作成します。マスターが立ち上がるのを待ってからredis-sentinelコマンドを実行します。

#!/bin/bash
while ! ping -c 1 redis-0.redis; do
    echo 'Waiting for server'
    sleep 1
done

redis-sentinel /redis-config/sentinel.conf

Redis 関連のファイルは以上で作成完了です。ファイル構成は以下のようになります。

|-- master.conf
|-- slave.conf
|-- sentinel.conf
|-- init.sh
`-- sentinel.sh

Redisの設定をConfigMapにまとめる

RedisのConfigファイルとコンテナ起動時に使うスクリプトをまとめて redis-config という名前の ConfigMap を作成します。

$ kubectl create configmap \
> --from-file=slave.conf=./slave.conf \
> --from-file=master.conf=./master.conf \
> --from-file=sentinel.conf=./sentinel.conf \
> --from-file=init.sh=./init.sh \
> --from-file=sentinel.sh=./sentinel.sh \
> redis-config

ワークロードの作成

次に Redis コンテナを定義する redis.yaml を作成します。デプロイメント(Deployment)ではなくステーフルセットを使うとポッドに ID が採番されポッド名-インデックスの形式でポッドにアクセスできます。replicas に3を指定しているので redis-0, redis-1, redis-2 という名前のポッド(Pod)が生成されます。Redis Sentinel 構成にする場合は Redis のコンテナが最低でも3つ以上必要なのとPodのIPアドレスをPod名から取得したかったのでこのような構成にしています。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis
spec:
  selector:
    matchLabels:
      app: redis
  replicas: 3
  serviceName: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      initContainers:
      - image: busybox:latest
        name: redis-init
        command: [sh, -c, cp -L /tmp/* /config]
        volumeMounts:
        - mountPath: /tmp
          name: tmp
        - mountPath: /config
          name: config
      containers:
      - command: [sh, -c, source /config/init.sh]
        image: redis:5-alpine
        name: redis
        ports:
        - containerPort: 6379
          name: redis
        volumeMounts:
        - mountPath: /config
          name: config
        - mountPath: /redis-data
          name: data
      - command: [sh, -c, source /config/sentinel.sh]
        image: redis:5-alpine
        name: sentinel
        ports:
        - containerPort: 26379
          name: sentinel
        volumeMounts:
        - mountPath: /config
          name: config
      volumes:
      - configMap:
          name: redis-config
        name: tmp
      - emptyDir:
        name: config
      - emptyDir:
        name: data

サービスの作成

Redis にアクセスするためのサービスを作成します。clusterIP に None を設定することで Pod の IPアドレスを直接取得することができます。

apiVersion: v1
kind: Service
metadata:
  name: redis
spec:
  ports:
  - port: 6379
    name: port-redis
  - port: 26379
    name: port-sentinel
  clusterIP: None
  selector:
    app: redis

IP アドレスを設定しないサービスのことを Headless サービスと呼びます。今回のように Headless Service + StatefulSet の組み合わせで使うことが多いようです。Headless サービスの説明は以下を参照してください。

k8s 1.13以降はConfigMapが読み取り専用になったので注意が必要です

Redis Sentinel は Config ファイルにアクセスして内容を更新します。ConfigMap のままでは正常に動作しません。なのでボリュームに ConfigMap を読み込んでから書き込み可能な emptyDir なボリュームにファイルをコピーする必要があります。

# ファイルコピー部分の抜粋
initContainers:
- image: busybox:latest
  name: redis-init
  command: [sh, -c, cp -L /tmp/* /config]
  volumeMounts:
  - mountPath: /tmp #読み取り専用
    name: tmp
  - mountPath: /config #書き込み可能
    name: config

ConfigMap はボリューム作成時にシンボリックリンクとして作成されリンク先が読み取り専用に設定されます。なのでdefaultModeで書き込み権限を付与しても書き込みできるようにはなりません。

Redisのデプロイ

サービスをデプロイします。

$ kubectl apply -f service.yaml

ステートフルセットをデプロイします。

$ kubectl apply -f redis.yaml

確認

redis-2 にアクセスして IP アドレスが返ってくるか確認します。

$ kubectl exec redis-2 -c redis \
-- redis-cli -p 26379 sentinel get-master-addr-by-name master

10.0.9.6
6379

参考書籍

こちらの「入門 Kubernetes」の14章を参考にしました。

参考

ConfigMap の読み取り専用を回避する方法

sentinel.conf の設定