はじめに
これは ドリコム Advent Calendar 2021 の1日目です。
自己紹介
どうも、DRIP エンジニアの小川です。
DRIP は Drecom Invention Project の略称で、ドリコムが発明を産み続けるためのプロジェクトです。
今年は総合エンターテイメント企業を目指す中、その周辺領域にも視野を広げながら新規事業開発を進めています。
ちなみに去年の投稿で
水面下活動した一年でした。
と書きましたが、なんと今年、水面下から浮上しました。
去年からずっと Rooot というサービスに携わっております。
Rooot とは「ファンをよりファンにする」をコンセプトとした、ユーザー同士で繋がるキッカケを与えるファンコミュニティ促進サービスです。
導入タイトルは日々着々と増加しておりまして、導入いただいたゲームは40タイトルを突破いたしました!
様々なクライアント様にご協力いただき、大変感謝です。
詳細は上記リンク先や https://www.rooot.biz/ を御覧ください。
今やっている事
DRIP では更に今、Rooot とは別の水面下活動を平行しています。
とある新規サービス立ち上げの話があり、早速 Munimum Variable Product を作ろうという流れの中で久々に Rails 環境の構築をする事に。
SPA + backend な Web サービスを提供しようと考えているため、frontend との疎通は backend API のみで構成予定です。
であるならば Rails による API 専用アプリケーションとしっかり向き合おうと思い、API 専用モードをオンにした上で土台を作り始めました。
しかし土台構築中に様々な問題にぶち当たりまして。
その中の1つ、session_store の設定について今日はご紹介します。
よくある session_store redis の設定
session の保存先を Redis にしたい!
というわけで改めてそのやり方を調べていくと、
この辺の gem を1つ導入し、各種 README の通り
# redis-actionpack README から拝借
MyApplication::Application.config.session_store :redis_store,
servers: ["redis://localhost:6379/0/session"],
expire_after: 90.minutes,
key: "_#{Rails.application.class.parent_name.downcase}_session",
threadsafe: true,
secure: true
のようなコードを config/initializers/
に配置すればできそうな感じでした。
class RedisController < ApplicationController
def session_set
session[:test_time] = Time.current
render json: { status: "ok" }
end
def session_get
render json: { time: session[:test_time]&.iso8601 }
end
end
resource :redis, only: [] do
get "/session_set", to: "redis#session_set"
get "/session_get", to: "redis#session_get"
end
上記のような、session に値を入れたり参照するアクションを生やし routing してブラウザから http://127.0.0.1:3000/redis/session_set にアクセスし Chrome DevTools を確認したところ、なんと session 用の cookie が見当たりません。
本来であれば自分の session データにアクセスするための Redis key が cookie に保存されているはずなのに…
当然、http://127.0.0.1:3000/redis/session_get にアクセスしても {"time":null}
が返ってきます。
調査開始
rails session store redis not working
でググってみたところ、欲しい情報は得られず…
ひょっとして API 専用アプリケーションが要因かも?と思い rails session store redis not working api_only
でググってみたところ
- https://stackoverflow.com/questions/48263528/how-to-set-the-configuration-for-middleware-for-actiondispatchsessionredisst
- https://daido.hatenablog.jp/entry/2020/05/06/143145
- http://ggibucket.com/entries/7120ed0147f092c111ae
- https://stackoverflow.com/questions/27710896/rails-4-sessions-cookie-is-not-set
- etc…
色々な有益情報が出てきました。
要約すると、config.api_only = true
の場合は使われる middleware が限定されており、Redis を使った session_store 設定を有効化するには必要な middleware を使うようにしなければならない、とのこと。
https://stackoverflow.com/questions/27710896/rails-4-sessions-cookie-is-not-set の情報を見ると「config.api_only = false
にすれば直るよ!」とあり、確かに config.api_only = false
にすれば解消するのですが API 専用アプリケーションではなくなるので本末転倒です。
しかも、結構な確率で「config.api_only = false
にすれば直るよ!」を勧めてくるページがヒットします。
class ApplicationController < ActionController::API
のまま config.api_only = false
にして大丈夫なのだろうか…
だったら rails new
時に --api
しない、の方が安全な気がします。
cache_store を用いた session_store redis の設定方法
http://ggibucket.com/entries/7120ed0147f092c111ae にある通り
config.middleware.use ActionDispatch::Cookies
するconfig.middleware.use ActionDispatch::Session::CacheStore
するconfig.cache_store
に redis を宛てる
config.cache_store = :redis_store, {
host: "localhost",
port: 6379,
db: 0,
password: "mysecret",
namespace: "cache"
}, {
expires_in: 90.minutes
}
をすれば config.api_only = true
のままでも session_store にて(結果として)Redis が使われるようになります!
完
ブラウザアクセスしたらまさかのエラー
実は、プロダクトを作り始めた当初は config.api_only = false
で取り急ぎ対応しており、上記情報を手に入れたのは開発途中段階での事でした。
これらの情報を集めた時、これで config.api_only = true
に戻せる!とウキウキしながら修正していったのですが、rails s
後にブラウザアクセスしてみるとまさかのエラー。
え!?なんで!?
色々ググってみたところ https://stackoverflow.com/questions/54979970/rails-5-integration-test-fails-with-nomethoderror-undefined-method-for-ni を発見し、middleware.use
とは違う middleware 追加方法の存在に気付きました。
middleware.use
するという事
一旦 middleware.use
している箇所をコメントアウトし、bundle exec rails middleware
を使って現在の middleware およびロード順を調べます。
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::ActionableExceptions
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
run App::Application.routes
そして middleware.use
を復活させると、こうなります。
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::ActionableExceptions
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use ActionDispatch::Cookies
use ActionDispatch::Session::CacheStore
run App::Application.routes
middleware.use
すると、ロード順の最後に push される事がわかります。
config.api_only = false
するとロードする middleware が増えるとの事だったので、改めて middleware.use
している箇所をコメントアウトし、config.api_only = false
した上で bundle exec rails middleware
を確認します。
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
+ use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::ActionableExceptions
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use ActionDispatch::PermissionsPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
run App::Application.routes
config.api_only = true
との差分は以下の7行。
~~~
use Rack::MethodOverride
~~~
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use ActionDispatch::PermissionsPolicy::Middleware
~~~
use Rack::TempfileReaper
~~~
今回追加したい ActionDispatch::Cookies
は ActiveRecord::Migration::CheckPending
の直下にいました。
なので ActiveRecord::Migration::CheckPending
の直下に配置されるように、
config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CacheStore
を以下の通りに書き換えます。
config.middleware.insert_after ActiveRecord::Migration::CheckPending, ActionDispatch::Cookies
config.middleware.insert_after ActionDispatch::Cookies, ActionDispatch::Session::CacheStore
これでエラーは無くなりました。
cache_store に設定した redis を session_store として使う場合の注意点
ちなみに1点、API 専用アプリケーションじゃなかった頃の session_store redis 設定との違いがありました。
同じ db
を共有する事になります。
上記例で言うと 0
を共有しています。
例えばですが、session に入れた値だけ全部消したい!となった場合 flushdb
は使えないなど、若干選択肢が狭まります。
cache_store と session_store で別々の db
を指定したい!場合はどうするか、調べてみようと思い立ちました。
middleware に手動で登録
API 専用プロジェクトにしていない別の Rails プロジェクトでは redis-session-store を使っており、Rails.application.config.session_store
を呼び出して設定しています。
ひょっとしてこれを呼び出すと middleware として追加されるのでは?と思い bundle exec rails middleware
してみると
use ActiveRecord::Migration::CheckPending
use ActionDispatch::Cookies
use RedisSessionStore
見つけた!
RedisSessionStore
の実体は https://github.com/roidrage/redis-session-store/blob/d166f70d3ed781d7db21fa1da824c44e49f45d24/lib/redis-session-store.rb です。
つまり Rails.application.config.session_store
に入れているものの実体を middleware.insert_after
すればいけるのでは?
更に上記参考ページにある https://stackoverflow.com/questions/27710896/rails-4-sessions-cookie-is-not-set のコメント先 https://stackoverflow.com/a/61238872 を見てみると、middleware を渡す際に引数も渡せる事がわかりました。
おそらく Rails.application.config.session_store
時に渡す引数をそのまま渡せば、期待する設定を入れる事ができそうです。
というわけで以下のように書き換えました。
# redis-actionpack での例
Rails.application.config.middleware.insert_after ActiveRecord::Migration::CheckPending, ActionDispatch::Cookies
Rails.application.config.middleware.insert_after ActionDispatch::Cookies, ActionDispatch::Session::RedisStore,
servers: ["redis://#{ENV.fetch("REDIS_HOST") { "localhost" }}:6379/0"],
expire_after: 5.minutes,
key: "_redis_sample_session"
http://127.0.0.1:3000/redis/session_set にアクセスし Chrome DevTools を確認したところ、ちゃんと期待する key で cookie が作成されていました。
第二部 完
Rails.application.config.session_store
が使われる条件
気になるのは Rails.application.config.session_store
に入れた設定に対して、以下のように挙動が異なる点です。
config.api_only = true
の場合、無視されるconfig.api_only = false
の場合、有効になるbundle exec rails middleware
を見るとActionDispatch::Session::CookieStore
の箇所が session_store に設定したもので書き換わっている
config.api_only
を直接見ているためなのでしょうか?それとも別の原因?
該当コードを探ってみます。
https://github.com/rails/rails/blob/f132be462b957ea4cd8b72bf9e7be77a184a887b/railties/lib/rails/application/finisher.rb#L48-L54
# Setup default session store if not already set in config/application.rb
initializer :setup_default_session_store, before: :build_middleware_stack do |app|
unless app.config.session_store?
app_name = app.class.name ? app.railtie_name.chomp("_application") : ""
app.config.session_store :cookie_store, key: "_#{app_name}_session"
end
end
session_store が設定されていない場合は session_store に :cookie_store
が指定されるようになっているため、
bundle exec rails middleware
を見るとActionDispatch::Session::CookieStore
の箇所が session_store に設定したもので書き換わっている
書き換わっているのではなく、Rails::Application::Finisher
側で挿入する middleware を決定している、と見れます。
config.api_only = true
の場合は設定した session_store も ActionDispatch::Session::CookieStore
も挿入されない、という事は initializer :setup_default_session_store
が呼び出されていなさそうな雰囲気です。
bundle exec rails initializers
を使って確認してみるも、config.api_only
による差分は全くありませんでした。
という事は session_store に入れた値を middleware として設定する処理の部分に差分がある?
そして辿り着いたのが以下のコード。
https://github.com/rails/rails/blob/f132be462b957ea4cd8b72bf9e7be77a184a887b/railties/lib/rails/application/default_middleware_stack.rb#L64-L71
middleware.use ::ActionDispatch::Cookies unless config.api_only
if !config.api_only && config.session_store
if config.force_ssl && config.ssl_options.fetch(:secure_cookies, true) && !config.session_options.key?(:secure)
config.session_options[:secure] = true
end
middleware.use config.session_store, config.session_options
end
やはり config.api_only
が見られていました。
config.api_only = true
の場合は session_store が無視される事がわかったため、手動で middleware に挿入するのが正しいようです。
RSpec 実行時にエラー発生
その後プロジェクトに RSpec を導入して bundle exec rspec
したらエラーが発生。
RuntimeError:
No such middleware to insert after: ActiveRecord::Migration::CheckPending
ActiveRecord::Migration::CheckPending
の後に insert できない?
ひょっとしてテスト時は middleware が異なる?
手動で入れている middleware.insert_after
を一旦コメントアウトし、RAILS_ENV=test bundle exec rails middleware
して確かめます。
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::ActionableExceptions
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
run App::Application.routes
ActiveRecord::Migration::CheckPending
が無い!
というわけで insert_after ActiveRecord::Migration::CheckPending
から insert_before Rack::Head
に書き換えて対応しました。
まとめ
- API 専用アプリケーションにした Rails では今までの session_store 設定をしても有効にならない
config.api_only = true
の場合、使用される middleware が制限されておりそれが原因だった- かつ session_store の設定も無視されてしまう
- 必要な middleware を適切な場所に差し込む事で有効となる
ActionDispatch::Cookies
および使用したい Redis store が対象
網羅した記事が見つからなかったため、ならば自分がやろう!と衝動ドリブンで書き上げました。
結局コード全体はどうなってるの?と思った皆様、ご安心ください。
https://github.com/ogwmtnr/rails-api-session-redis に全て push してあります!
この記事に該当する PR は https://github.com/ogwmtnr/rails-api-session-redis/pull/3 となりますので、お急ぎの方はこちらを見ていただければ設定方法がわかるかと思います。
少しでも、Rails エンジニアの役に立てたら嬉しいです。
さいごに
明日は広井淳貴さんです。
ドリコムでは一緒に働くメンバーを募集しています!
募集一覧はコチラを御覧ください!