Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Railsアプリで複数のサブドメインを扱う方法:ルーティング、セッション共有、テストまで徹底解説

本記事では、複数のサブドメインに対応したRailsアプリケーションの構築方法を解説します。例として、ゲームサイト funkygames.co を運営しており、単一のRailsアプリケーションで app.funkygames.coapi.funkygames.codev.funkygames.co といった複数のサブドメインをサポートしたいケースを想定します。すべてのサブドメインで適切な認証が行われ、重複するルートが存在しない状態を実現しましょう。

Railsが持つ強力なルーティング機能を活用すれば、複数サブドメインへの対応は驚くほどシンプルに行えます。さらに、ローカル環境でのサブドメイン設定方法や、複数サブドメインに対するテストの書き方についても取り上げます。

前提条件

本記事では、すべてのサブドメインがRailsアプリに向くようDNSレコードが適切に設定されていることを前提とします。ここで扱うのはRails側の設定のみです。

複数サブドメインへの対応

Railsは routes.rb ファイルを使って受信リクエストを処理し、特定のコントローラアクションへマッピングします。シンプルなアプリでは、routes.rb 内の各マッピングは次のように記述します。

get '/games/:id', to: 'games#show'

このアプローチでは、routes.rb に定義されたすべてのエンドポイントが全サブドメインに適用されます。つまり app.funkygames.co/games/1api.funkygames.co/games/1 も同じルートで処理されてしまいます。しかし本来は、このルートは app サブドメインからのリクエストだけを処理すべきです。api サブドメインはAPI用のルート専用にしたいところです。そこで、特定の条件を満たすリクエストのみを処理するよう、ルートにルールを追加します。

Railsのルーティングには constraints ヘルパーメソッドが用意されており、指定したルートに追加の制約を設けることができます。

get '/games/:id', to: 'games#show', constraints: { subdomain: 'app' }

こうすることで、app.funkygames.co/games/1 からのリクエストだけが GamesController の show アクションで処理され、app 以外のサブドメインからのリクエストはこのルートでは処理されなくなります。

ただし、すべてのルートに個別に constraints を定義するのは非常に冗長です。

get '/games/:id', to: 'games#show', constraints: { subdomain: 'app' }
get '/games/list', to: 'games#list', constraints: { subdomain: 'app' }
post '/games/start', to: 'games#start', constraints: { subdomain: 'app' }

そこで、constraints のブロック形式を使えば、単一のサブドメインに対して複数のルートをまとめて定義できます。

constraints subdomain: 'app' do
  get '/games/:id', to: 'games#show'
  get '/games/list', to: 'games#list'
  post '/games/start', to: 'games#start'
end

複数のサブドメイン向けにルートを定義するには、routes.rb に複数の constraints ブロックを追加するだけです。

constraints subdomain: 'app' do
  ...
end

constraints subdomain: 'api' do
  ...
end

constraints subdomain: 'dev' do
  ...
end

内部の仕組み

Railsのルーティングには「リクエスト制約(request constraints)」と「セグメント制約(segment constraints)」の2種類があります。セグメント制約はリクエストパスに対するルールを追加するもので、リクエスト制約は受信リクエスト自体に条件を課すものです。リクエスト制約のハッシュキーには、文字列を返す Request オブジェクトのメソッド名を指定し、値には期待される値を指定します。

constraints subdomain: 'app' do
  ...
end

上記の例では、Request オブジェクトの subdomain メソッドの戻り値を、appapidev といった文字列と照合しています。

詳細については、Railsのルーティングガイドを参照してください。

マルチレベルサブドメインへの対応

ステージング環境で app.staging.funkygames.co を使っているとしましょう。前述の設定のままだと、app サブドメインに届くはずのリクエストがすべて404を返すことに気づくでしょう。デバッグしてみると、サブドメインの制約判定が失敗していることが分かります。

request.subdomain #=> app.staging

期待していたのは app という値ですが、実際には app.staging が返ってきています。もちろん、環境ごとのコードを追加せずに解決したいものです。リクエストのサブドメイン解析は config.action_dispatch.tld_length オプションで制御されています。デフォルト値は1で、これは1階層のサブドメインのみをサポートすることを意味します。今回のように2階層のサブドメインがある場合は、config.action_dispatch.tld_length を2に設定する必要があります。

# config/application.rb
config.action_dispatch.tld_length = Integer(ENV['TLD_LENGTH'] || 1)

環境変数で設定できるようにしておけば、ステージング環境でも本番環境でも同じコードを使い回せます。これで app.staging.funkygames.co でもルーティング設定が正しく機能するようになります。

セッション管理

複数サブドメインからのリクエストを処理するルートが定義できたら、次は全サブドメインの認証に対応する必要があります。アプローチは2つあります。すべてのサブドメインで同一のユーザーセッションを共有するか、サブドメインごとに独立したセッションを持たせるかです。

認証の仕組みをおさらい

RailsはデフォルトでCookieを使ってユーザーのセッションキーを保存します。ユーザーがログインすると、セッション情報は選択したセッションストアに格納され、セッションキーがブラウザ上のCookieとして保存されます。次回ユーザーがサイトを訪れると、同じセッションCookieがブラウザからサーバーへ送信され、サーバーはそのセッションCookieに対応するセッションが存在するかどうかに基づいて、ログイン済みかどうかを判断します。

Railsアプリにおけるセッションのデフォルト設定は次のようになっています。

Rails.application.config.session_store :cookie_store, key: "_funkygames_session"

キー _funkygames_session がセッションCookieの名前として使われ、その値にはセッションIDが格納されます。

Cookieの基本

デフォルトでは、Cookieはリクエストのドメインに対してブラウザによって設定されます。つまり app.funkygames.co からアプリにアクセスした場合、セッションCookieは app.funkygames.co に対して設定されます。各サブドメインがそれぞれ独自のセッションCookieを設定するため、デフォルトのままではユーザーセッションはサブドメイン間で共有されません。

異なるサブドメイン間でのセッション共有

サブドメイン間でユーザーセッションを共有したい場合は、すべてのサブドメインからアクセスできるよう、セッションCookieを funkygames.co ドメイン自体に設定する必要があります。これは、セッションストアの設定に domain オプションを渡すことで実現できます。

Rails.application.config.session_store :cookie_store, key: "_funkygames_session", domain: :all

domain:all を渡すと、個々のサブドメインを含むリクエストホストではなく、funkygames.co のようなアプリケーションのトップレベルドメインに対してセッションCookieが設定されるようになります。こうすることで、異なるサブドメイン間でセッションを共有できるようになります。

なお、domain オプションに配列形式でドメインのリストを渡せば、複数ドメインにも対応できます。

すべてのサブドメイン向けにCookieを正しく設定するには、もう1つ設定すべきオプションがあります。それが tld_length オプションです。domain: :all を使う場合、このオプションでドメインのTLD(トップレベルドメイン)を解釈する際の解析方法を指定できます。今回の app.funkygames.co のケースでは、Cookie設定時にTLDを funkygames.co と認識させるため、tld_length を2に設定します。最終的なセッションストアの設定は次のようになります。

Rails.application.config.session_store :cookie_store,
                                       key: "_funkygames_session",
                                       domain: :all,
                                       tld_length: 2

セッションストアの tld_length オプションは、先ほど説明した config.action_dispatch.tld_length とは別物なので注意してください。

複数サブドメインのテストを書く

ルートがサブドメイン固有である以上、テストリクエストに適切なサブドメインが含まれていないと、リクエストスペックや統合テストは404エラーになります。Railsの統合テストには host! ヘルパーが用意されており、テストファイル内で行われるすべてのリクエストに対して適切なサブドメインを設定できます。

# Rails統合テストでのサブドメイン設定
setup do
  host! 'dev.example.com'
end

# RSpecリクエストスペックでのサブドメイン設定
before do
  host! 'dev.example.com'
end

これにより、リクエストは routes.rb のサブドメインルーティングに従って、正しくコントローラアクションへ振り分けられます。

なお、ここではドメイン名自体は重要ではなく、テスト対象のコードに応じた適切なサブドメインを指定することが重要です。

開発環境で複数サブドメインをローカルにセットアップする

ローカル環境でサブドメインをセットアップする方法はいくつかあります。最も簡単なのは /etc/hosts ファイルを編集する方法です。

127.0.0.1 dev.funkygames.local
127.0.0.1 app.funkygames.local
127.0.0.1 api.funkygames.local

これで、ローカル環境でもサブドメインの設定が機能するようになります。また、powのようなツールを使ってローカルのサブドメインを管理することも可能です。

制約ベースのサブドメインルーティングにおける注意点

制約ベースのサブドメインルーティングはほとんどのケースで問題なく動作しますが、状況によっては厄介な問題になることもあります。

外部APIとの連携

サードパーティAPIと連携機能を構築する際、.local.dev のようなローカル開発用TLDは許可されません。そのためngrokなどのツールを使う必要がありますが、このような場合、サブドメインベースのルーティングは機能しないため、ngrok経由でもアクセスできるよう一部のルートをホワイトリスト化する必要があります。

サブドメイン制約の外側にあるべきルート

サブドメイン制約の中に置けないルートも存在します。典型的な例が healthcheckping エンドポイントです。Railsアプリの前面にロードバランサーを配置している場合、ロードバランサーはアプリが稼働しているかどうかを定期的に確認する必要があります。この用途で使われる healthcheck エンドポイントは、ロードバランサーがリクエストホストを把握していないことが多いため、サブドメイン制約の外に置かなければなりません。

rootルートが存在しない問題

Railsには特別な root ルートがあり、これはアプリケーションのデフォルトルートとして機能します。他のどのルートともリクエストが一致しない場合に root ルートが使われます。すべてのルートをいずれかのサブドメイン配下に置いていると、root ルートが一切定義されていない状況が発生しえます。一部のgemは root ルートの存在に依存している可能性があるため、必要に応じてチェックやフォールバックを追加しておきましょう。

まとめ

本記事では、わずかな設定行数で複数サブドメインに対応したRailsアプリを構築しました。ローカル環境や異なる環境でのサブドメイン設定方法に加え、複数サブドメイン向けの効果的なテストの書き方についても紹介しました。Railsが提供する仕組みを活用すれば、複数サブドメインを持つRailsアプリのセットアップとテストは簡単に行えます。

P.S. Ruby Magicの最新記事をいち早く読みたい方は、Ruby Magicニュースレターを購読してください。見逃すことはありません!

  1. Rails5でのAngularの使用

    あなたは前にその話を聞いたことがあります。分散型で完全に機能するバックエンドAPIと、通常のツールセットで作成されたフロントエンドで実行されているアプリケーションがすでにあります。 次に、Angularに移動します。または、AngularをRailsプロジェクトと統合する方法を探しているだけかもしれません。これは、この方法を好むためです。私たちはあなたを責めません。 このようなアプローチを使用すると、両方の世界を活用して、たとえばRailsとAngularのどちらの機能を使用してフォーマットするかを決定できます。 構築するもの 心配する必要はありません。このチュートリアルは、この目的のた

  2. Photo Organizerアプリで複数の画像名を一括変更する方法

    デジタル画像は、気づけばハードドライブを散らかしてしまう存在です。大切な思い出の写真ほど、整理や分類が難しく、場合によってはほぼ不可能に感じられることもあります。こうした画像関連の悩みを解決するのが「Systweak Photo Organizer」です。このアプリを使えば、画像の整理、重複ファイルの削除、写真の一括リネームが簡単に行えます。本記事では、Systweak Photo Organizerの概要と、複数の画像名を一度に変更する方法を詳しく解説します。 Photo Organizerアプリで複数の画像名を変更する手順 ステップ1:Microsoft Storeまたは下のボタンから、S