Rails8.1系で、Redisクラスターをセッションストアとして設定してみた

Railsではセッションの保存先(セッションストア)を指定できます。
3.2.58 config.session_store | Rails アプリケーションの設定項目 - Railsガイド

以前、セッションストアをRedisにしたときは、スタンドアロンRedisでの利用でした。
Rails7.0系で、キャッシュストアとセッションストアをRedisにしてみた - メモ的な思考的な

一方、RedisにはRedisクラスターという構成もあります。

そこで、Redisクラスターをセッションストアとして利用できるか試してみたことから、メモを残します。

 
目次

 

環境

 

スタンドアロンRedisをセッションストアにする

Redisクラスターをセッションストアにする前に、スタンドアロンRedisをRailsアプリのセッションストアにしてみます。

以前の記事でも試していますが、最新のRedis 8系であらためて試してみます。

 

rails new

今回は、必要最低限のRailsアプリで動作検証します。

セッションストアを利用することから、Cookieまわりのミドルウェアが必要です。 api オプションだとそれらのミドルウェアが含まれなくなるため、 minimal オプションを使います。

% bundle exec rails new . --minimal
Based on the specified options, the following options will also be activated:

  --skip-active-job [due to --minimal]
  --skip-action-mailer [due to --skip-active-job, --minimal]
  --skip-active-storage [due to --skip-active-job, --minimal]
  --skip-solid [due to --minimal]
  --skip-action-mailbox [due to --skip-active-storage, --minimal]
  --skip-action-text [due to --skip-active-storage, --minimal]
  --skip-javascript [due to --minimal]
  --skip-hotwire [due to --skip-javascript, --minimal]
  --skip-action-cable [due to --minimal]
  --skip-bootsnap [due to --minimal]
  --skip-brakeman [due to --minimal]
  --skip-bundler-audit [due to --minimal]
  --skip-ci [due to --minimal]
  --skip-dev-gems [due to --minimal]
  --skip-docker [due to --minimal]
  --skip-jbuilder [due to --minimal]
  --skip-kamal [due to --minimal]
  --skip-rubocop [due to --minimal]
  --skip-system-test [due to --minimal]
  --skip-thruster [due to --minimal]

 
コントローラもジェネレータで作成します。今回は

  • POSTでセッションストアへデータ登録
  • GETでセッションストアからデータを取得してレスポンス

を行いたいことから、 indexcreate を作ります。

% bin/rails generate controller Hellos index create
      create  app/controllers/hellos_controller.rb
       route  get "hellos/index"
      invoke  erb
      create    app/views/hellos
      create    app/views/hellos/index.html.erb
      invoke  test_unit
      create    test/controllers/hellos_controller_test.rb
      invoke  helper
      create    app/helpers/hellos_helper.rb
      invoke    test_unit

 
生成された hellos_controller.rbindexcreate の各アクションを実装します。

class HellosController < ApplicationController
  skip_before_action :verify_authenticity_token, raise: false

  def index
    puts session[:message]
    render json: { message: session[:message] }
  end

  def create
    puts params[:message]
    session[:message] = params[:message]
    head :ok
  end
end

 
routes.rb のルーティングも整理します。

Rails.application.routes.draw do  
  resources :hellos, only: [:index, :create]
end

 

スタンドアロンRedisをセットアップ

docker composeでスタンドアロンRedisを作成します。

Docker内でのネットワーク接続ができるよう、networks redis-network も作っておきます。

また、開発用途なので "--protected-mode", "no"も指定しておきます。

services:
  redis-single:
    image: redis:8.4.0-bookworm
    container_name: redis-single
    ports:
      - "17000:6379"
    command: ["redis-server", "--save", "", "--appendonly", "no", "--protected-mode", "no"]
    restart: unless-stopped
    networks:
      - redis-network

networks:
  redis-network:
    name: redis-network

 
準備ができたのでコンテナを起動します。

% docker compose up -d                        
[+] Running 2/2
 ✔ Network redis-network   Created
 ✔ Container redis-single  Started 

 

Railsのセッションストアを設定

最後に、スタンドアロンRedisへ接続できるよう、Railsのセッションストアを設定します。

RailsのセッションストアをRedisにする方法としては、まず目についたのは redis-rails です。
https://github.com/redis-store/redis-rails

redis-railsのREADMEを読むと

We are still actively maintaining all other gems in the redis-store family, such as redis-actionpack for session management

とありました。

今回はセッションストアしか利用しないため、 redis-actionpack を使うことにします。

 
次に redis-actionpackの依存関係について、Gemfile.lockを見てみると

redis-actionpack (5.5.0)  
  actionpack (>= 5)  
  redis-rack (>= 2.1.0, < 4)  
  redis-store (>= 1.1.0, < 2)  
redis-store (1.11.0)  
  redis (>= 4, < 6)

となっていたことから、 redis gemの4系以降が使えそうでした。

 
redis gemのバージョンの違いとしては

  • 4系
  • 5系
    • redis gem単体では非対応で、 redis-clustering gemに分離された

ようです。
Plan for redis-rb 5.0 · Issue #1070 · redis/redis-rb

これより、Redisクラスターにおける redis 4系と5系の違いをためしてみたくなりました。

そこで、スタンドアロンRedisを使うときのgemとバージョンの組み合わせは次として、あとで4系と5系の違いを試しやすくします。

 
では、試していきます。bundle addします。

$ bundle add redis --version 4.8.1
...
Installing redis 4.8.1

$ bundle add redis-actionpack
...
Installing redis-store 1.11.0
Installing redis-rack 3.0.0
Installing redis-actionpack 5.5.0

 
次に config/initializers/session_store.rb にセッションストアの設定を行います。

redis-actionpack READMEのように文字列を指定できる他、servers に配列のハッシュを渡すこともできます。今回は後者で作ります。
https://github.com/redis-store/redis-actionpack?tab=readme-ov-file#usage

ここで、hostは redis-single というDockerコンテナを指定します。DevContainerからスタンドアロンRedisのDockerへ、Dockerのネットワークを使って接続するためです。

portも同様に、Dockerのネットワーク内から接続するため 6379 を指定します。

Rails.application.config.session_store :redis_store,
                                       servers: [
                                         {
                                           host: "redis-single",
                                           port: 6379,
                                           db: 0,
                                           namespace: "session"
                                         }
                                       ],
                                       expire_after: 90.minutes,
                                       key: "_redis_cluster_example_session"

 

DevContainerをセットアップ

RailsのDevContainerをセットアップするため、 .devcontainer/devcontainer.json を用意します。

Redis CLIだけをインストールするのは色々手間がかかりそうだったため、今回はサードパーティのFeatureでインストールしています。

また、何かあったとき用にAIエージェントをインストールできるよう、DevContainerのFeatureとしてNode.jsを追加します。
https://github.com/devcontainers/features/tree/main/src/node

 
これらを踏まえたRailsを動かすためのDevContainer向けの devcontainer.jsonは次の通りです。

{
  "name": "rails_local2devcontainer-example",
  // Rails公式のDevContainerを利用
  "image": "ghcr.io/rails/devcontainer/images/ruby:3.4.8",
  "mounts": [
    "source=${localEnv:HOME}/.agent/codex/.codex,target=/home/vscode/.codex,type=bind,consistency=cached",
  ],
  "features": {
    "ghcr.io/devcontainers/features/node:1": {},
    // curlなどのツールを導入しておく
    "ghcr.io/devcontainers/features/common-utils:2": {},
    // サードパーティのRedis CLIを入れる
    "ghcr.io/42atomys/devcontainers-features/redis-cli:1": {}
  },
  // 外部からコンテナのRailsへアクセスできるようポートフォワードする
  "forwardPorts": [3000],
  // Redisのネットワークへ接続できるようにする
  "runArgs": [
    "--network", "redis-network"
  ],
  // RubyMine向けのワークアラウンドやnpm installするため、postCreateCommandを用意する
  "postCreateCommand": ".devcontainer/postCreateCommand.sh",
  "customizations": {
    "jetbrains": {
      "plugins": [
        "com.github.thinkami.railroads",
        "com.intellij.plugins.mnemonicKeymap"
      ]
    }
  }
}

 
今回RubyMineでDevContainerを起動するため、 postCreateCommand.shも用意します。

#!/bin/sh  
set -eu  
  
# DevContainerを起動する前に、このファイルを `chmod +x postCreateCommand.sh` しておく  
  
# RubyMine 2025.3.1 の既知の不具合  
# https://youtrack.jetbrains.com/projects/RUBY/issues/RUBY-34350/Cannot-find-Ruby-interpreter-in-the-Rails-devcontainer  
# ただし、プライベートなgemのインストールがあるため、最後の `bin/setup --skip-server` は実行せず、コンテナ内で自分で実行する  
sudo mkdir -p /.jbdevcontainer/config /.jbdevcontainer/data && sudo chmod -R 777 /.jbdevcontainer && ln -s /home/vscode/.config/mise /.jbdevcontainer/config/mise && ln -s /home/vscode/.local/share/mise /.jbdevcontainer/data/mise  
  
npm install -g @openai/codex@latest

 
あとはRubyMineからDevContainerを、DevContainerの中でRailsをそれぞれ起動します。以前の記事同様のやり方で起動できます。
既存のRailsアプリを単体でDevContainer化し、RubyMineのDevContainerとして動かしてみた - メモ的な思考的な

 

動作確認

DevContainerで forwardPorts を定義していることから、macのターミナルで curl による動作確認が行えます。

まずはPOSTです。複数回のcurlでセッションを共有できるよう、 -c オプションでCookie情報をファイルへと保存します。

% curl -X POST http://localhost:3000/hellos -d "message=hello" -c cookies.txt

 
Railsのログを見ると、curlからのデータを受け取っていました。

Started POST "/hellos" for 127.0.0.1 at 2026-01-26 22:29:31 +0900
...
Processing by HellosController#create as */*
  Parameters: {"message" => "hello"}
hello

 
次に -b オプションで先ほど使ったCookieを利用して、セッションのデータを取得します。レスポンスを見る限り、セッションにデータが保存されているようでした。

% curl -b cookies.txt http://localhost:3000/hellos   
{"message":"hello"}

 
RubyMineでRedisの中身を確認します。スタンドアロンRedisへ接続するための設定は次の通りです。

 
Redisへ接続できたので確認すると、データが含まれていました。

 
最後に、DevContainerのRedis CLIでも中身を確認してみます。

scanでセッションのキーを取得後、getでセッションの中身を取得します。

$ redis-cli -c -h redis-single -p 6379 --scan
"session:2::36334d75885e1c87c38636160d0058aa8ef7b8e9a76727142c1727c102bdecc4"

$ redis-cli -c -h redis-single -p 6379 get "session:2::36334d75885e1c87c38636160d0058aa8ef7b8e9a76727142c1727c102bdecc4"
"\x04\b{\x06I\"\x0cmessage\x06:\x06EFI\"\nhello\x06;\x00T"

 
Rails Consoleで取得できた文字列をデコードしてみると、セッションに入れた値になっていました。良さそうです。

Loading development environment (Rails 8.1.2)
>> Marshal.load("\x04\b{\x06I\"\x0cmessage\x06:\x06EFI\"\nhello\x06;\x00T")
=> {"message" => "hello"}

 

Redisクラスターをセッションストアにする

スタンドアロンRedisをセッションストアにすることができたので、次はRedisクラスターをセッションストアにしてみます。

 

Redisクラスターをセットアップ

以前はbitnami版のRedisクラスターを使うのがお手軽だったようですが、現在では利用できなさそうです。そのため、RedisのDockerイメージをベースに作成します。
https://hub.docker.com/_/redis

今回は、compose.yml にRedisクラスター用コンテナとクラスタ構築用コンテナを追加します。なお、今回は動作確認が目的なので、次のような作りとしました。

  • マスター3台・レプリカ0台という最小構成
  • --appendonly no として永続化もしない
  • クラスタ構築用コンテナでは、クラスタ作成コマンドを実行
services:
  # ...
  # Redisクラスターノード1
  redis-cluster-node-1:
    image: redis:8.4-bookworm
    container_name: redis-cluster-node-1
    command: redis-server --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --appendonly no
    ports:
      - "17001:6379"
    networks:
      - redis-network

  # Redisクラスターノード2
  redis-cluster-node-2:
    image: redis:8.4-bookworm
    container_name: redis-cluster-node-2
    command: redis-server --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --appendonly no
    ports:
      - "17002:6379"
    networks:
      - redis-network

  # Redisクラスターノード3
  redis-cluster-node-3:
    image: redis:8.4-bookworm
    container_name: redis-cluster-node-3
    command: redis-server --cluster-enabled yes --cluster-config-file nodes.conf --cluster-node-timeout 5000 --appendonly no
    ports:
      - "17003:6379"
    networks:
      - redis-network

  # クラスタ初期化用コンテナ
  redis-cluster-init:
    image: redis:8.4-bookworm
    depends_on:
      - redis-cluster-node-1
      - redis-cluster-node-2
      - redis-cluster-node-3
    # クラスタ作成コマンドを実行
    command: >
      sh -c "sleep 5 &&
      echo 'yes' | redis-cli --cluster create 
      redis-cluster-node-1:6379
      redis-cluster-node-2:6379 
      redis-cluster-node-3:6379 
      --cluster-replicas 0"
    networks:
      - redis-network
# ...

 
今までのコンテナを落としてから、再度コンテナを起動します。

% docker compose down -v
% docker compose up -d 

 

redis 4系 + redis_actionpackによる接続

まずはスタンドアロンRedisへの接続でも使用した redis 4系 + redis_actionpackにて接続します。

スタンドアロンRedis向けの設定ではRedisクラスターへ接続できないことから、次の記事を参考に session_store.rbの設定を変更します。
RubyのRedis Client LibraryをCluster Modeに対応させた話 - LIVESENSE ENGINEER BLOG

cluster 引数には、 compose.ymlで作成したRedisクラスターのノードとポートを指定します。

Rails.application.config.session_store :redis_store,
                                       redis_store: Redis::Store.new(
                                         cluster: %w[
                                           redis://redis-cluster-node-1:6379
                                           redis://redis-cluster-node-2:6379
                                           redis://redis-cluster-node-3:6379
                                         ],
                                         namespace: "session"
                                       ),
                                       expire_after: 90.minutes,
                                       key: "_redis_cluster_example_session"

 
続いて、curlで動作確認をします。curlに渡す値はスタンドアロンRedisと同様です。

% curl -X POST http://localhost:3000/hellos -d "message=hello" -c cookies.txt

% curl -b cookies.txt http://localhost:3000/hellos   
{"message":"hello"}

 
処理が成功したため、RubyMineでRedisクラスターの様子を確認します。

RubyMineの公式ドキュメントを参考に、Redisクラスター向けに接続情報を変更します。

今回は次のようになります。

項目
Connectiontype cluster
Authentication No auth
URL jdbc:redis:cluster://redis-cluster-node-1:6379,redis-cluster-node-2:6379,redis-cluster-node-3:6379

 
確認したところ、Redisクラスターにデータが保存されていました。

 
続いて、Redis CLIでも確認してみます。

Redisクラスターのどのノードにデータが保存されているかわからないため、ノードごとに --scan します。今回は redis-cluster-node-3 へ保存されていたため、そこに対して get で値を取得します。

スタンドアロンRedisと同じ値が設定されていました。

$ redis-cli -c -h redis-cluster-node-3 -p 6379 --scan
"session:2::c77a2927c20f6d6a47ae38aa05675eda1e3f56a370e1a6c47ddb8eac8f95dbbd"

$ redis-cli -c -h redis-cluster-node-3 -p 6379 get "session:2::c77a2927c20f6d6a47ae38aa05675eda1e3f56a370e1a6c47ddb8eac8f95dbbd"
"\x04\b{\x06I\"\x0cmessage\x06:\x06EFI\"\nhello\x06;\x00T"

 
続いて、 redis 5系 + redis_actionpack だとどうなるかを試してみます。

bundle update で更新します。

$ bundle update redis
...
Installing redis-client 0.26.4
Installing redis 5.4.1 (was 4.8.1)
Bundle updated!

 
Railsを起動しようとすると、エラーになりました。redis gemのREADMEにある通り、redis gemの5系では redis-clusteringとともに使う必要がありそうです。
https://github.com/redis/redis-rb/tree/master?tab=readme-ov-file#cluster-support

=> Run `bin/rails server --help` for more startup options
Exiting
/home/vscode/.local/share/mise/installs/ruby/3.4.8/lib/ruby/gems/3.4.0/gems/redis-5.4.1/lib/redis.rb:138:in 'Redis#initialize_client': Redis Cluster support was moved to the `redis-clustering` gem. (RuntimeError)

 

redis 5系 + redis-clusteringによる接続

では、redis 5系 + redis-clustering 構成での接続を試してみます。

まず、redis-clustering gemを追加します。

一緒に追加された redis-cluster-client については次の作者の記事が参考になります。

$ bundle add redis-clustering
...
Installing redis-cluster-client 0.13.7
Installing redis-clustering 5.4.1
Bundle complete! 9 Gemfile dependencies, 78 gems now installed.

 
再びRailsを起動しようとしたところ、同じエラーになりました。

Redis Cluster support was moved to the `redis-clustering` gem. (RuntimeError)

 
redis gemと redis-clustering gemを組み合わせて使う方法がないかを調べたところ、次のissueが見つかりました。
Proper documentation on how to setup rails to connect to a redis cluster for session management · Issue #1280 · redis/redis-rb

その中で、Railsのコアコミッターであるbyrootさんが :cache_storeを使う旨をコメントしていました。
https://github.com/redis/redis-rb/issues/1280#issuecomment-2111358279

 
そこで、session_store.rb を差し替えてみました。

Rails.application.config.session_store :cache_store,
                                       cache: ActiveSupport::Cache::RedisCacheStore.new(
                                         redis: Redis::Cluster.new(nodes: %w[
                                           redis://redis-cluster-node-1:6379
                                           redis://redis-cluster-node-2:6379
                                           redis://redis-cluster-node-3:6379
                                         ]),
                                         namespace: "session_redis5"
                                       ),
                                       expire_after: 90.minutes,
                                       key: "_redis_cluster_example_session"

 
この設定にしたことで、Railsが問題なく起動しました。

そこで、 curlを使って動作確認をすると、想定通りの結果が返ってきていました。

% curl -X POST http://localhost:3000/hellos -d "message=hello" -c cookies.txt

% curl -b cookies.txt http://localhost:3000/hellos
{"message":"hello"}

 
RubyMineでRedisクラスターの中身を確認したところ、保存されているようでした。

 
Redis CLIでも確認してみたところ、今度は redis-cluster-node-2へ保存されていました。

ただ、redis gem4系の :redis_store とは格納されている値が異なってそうでした。

$ redis-cli -c -h redis-cluster-node-2 -p 6379 --scan
"session_redis5:_session_id:2::1d434ac6474f679214bde8d3e19480c0bb75439f622049ac1a0af1a08238737d"

$ redis-cli -c -h redis-cluster-node-2 -p 6379 get "session_redis5:_session_id:2::1d434ac6474f679214bde8d3e19480c0bb75439f622049ac1a0af1a08238737d"
"\x00\x11\x01\xd8\xac\x9c\xdf\xd7^\xdaA\xff\xff\xff\xff\x04\b{\x06I\"\x0cmessage\x06:\x06EFI\"\nhello\x06;\x00T"

 
そこで、中身を確認できるようなRubyスクリプトをAIに作ってもらいました。

# Redisクラスターから取得した値を貼り付け
s = "\x00\x11\x01\xd8\xac\x9c\xdf\xd7^\xdaA\xff\xff\xff\xff\x04\b{\x06I\"\x0cmessage\x06:\x06EFI\"\nhello\x06;\x00T".b  

sig = s[0,2] # => "\x00\x11"  
type, expires_at, version_len = s.byteslice(2, 13).unpack("CEl>")  
offset = 2 + 13  
version = (version_len >= 0) ? s.byteslice(offset, version_len) : nil  
offset += (version_len >= 0 ? version_len : 0)  
payload = s.byteslice(offset, s.bytesize - offset)  
puts Marshal.load(payload)

 
このスクリプトを実行したところ、次の結果が得られました。想定通りの値でした。

$ ruby decode.rb 
{"message" => "hello"}

 
ところで、 redis-clusteringを使う場合は redis-actionpackは不要そうでした。

そこで、Gemfileから redis-actionpackを削除し、動作確認をします。今回は最初のPOSTのデータを少し変えて message=world としています。

すると、良さそうな結果が返ってきました。

% curl -X POST http://localhost:3000/hellos -d "message=world" -c cookies.txt

% curl -b cookies.txt http://localhost:3000/hellos
{"message":"world"}

 
Redis CLIで確認したところ、 redis-cluster-node-1 に保存されていました。また、get したときに world という文字列が見えました。

$ redis-cli -c -h redis-cluster-node-1 -p 6379 --scan
"session_redis5:_session_id:2::dc555d52881bdde8959fc8dcf44c48b1bf474339cc2e217dff83f0cfbec7af31"

$ redis-cli -c -h redis-cluster-node-1 -p 6379 get "session_redis5:_session_id:2::dc555d52881bdde8959fc8dcf44c48b1bf474339cc2e217dff83f0cfbec7af31"
"\x00\x11\x01_\xac\x1fg\xd8^\xdaA\xff\xff\xff\xff\x04\b{\x06I\"\x0cmessage\x06:\x06EFI\"\nworld\x06;\x00T"

 
これより、 redis-actionpack なしで、 redis 5系 + redis-clustering で使えると分かりました。

 

ソースコード

GitHubに上げました。
https://github.com/thinkAmi-sandbox/rails_session_with_redis_cluster-example

redis 4系 + redis-actionpackのプルリクはこちら。
https://github.com/thinkAmi-sandbox/rails_session_with_redis_cluster-example/pull/1

redis 5系 + redis-clusteringのプルリクはこちら。
https://github.com/thinkAmi-sandbox/rails_session_with_redis_cluster-example/pull/2