Railsでユーザごとにサブドメインが切られたユーザページを提供する手順


開発しているサービスの中で、ユーザにサブドメインが切られたページを提供したいなと思うことがあっていろいろ調べたけれど、あまり情報がなくて困ったので書いた。

よくある `dev.example.com` や `admin.example.com` のような事前に決めうちされたサブドメインにそれぞれ別の環境を用意するものではなく、例えば

user1.example.com
user2.example.com
user3.example.com ...

のように、ユーザの数だけサブドメインが発行されるものを想定している。
`example.com` の部分はご自身のドメインに置き換えてください。

## 前提
- Ruby 2.2.0
- Rails 4.2.0
- devise 3.4.1
- Unicorn 4.8.3
- Nginx 1.6.2
- EC2 Amazon Linux AMI 2015.09.1 (HVM)

## やること
- 1. サーバ側のレコード設定でサブドメインのAレコードを設定する
- 2. ローカル環境でサブドメインのページを表示させる
- 3. Rails側でサブドメインを受け取りuser#showにルーティングする
- 4. 補足：サブドメイン<->メインドメイン間でcookieを共有する
- 5. 補足：サブドメイン<->メインドメイン間をリンクで遷移する

## 1. サーバ側のレコード設定でサブドメインのAレコードを設定する

まずやらなければいけないのは `user1.example.com` で `example.com` で同じhostの同じページを表示できるようにすること。user1の部分はこの時点ではどんな文字列でも問題ない。

これを実現するためにサーバのAレコードを変更して、サブドメインでリクエストされた場合にメインドメインと同じhostを返してあげる。AWSで運用しているのでRoute53を使った。さくらVPSなどでももちろん大丈夫。

現在稼働しているHosted zonesを選択し、Record Setの追加を行う。Create Record Set より、以下のように設定する。

- Name: *.example.com.
- Type: A - IPv4 Address
- Alias: Yes or No
- TTL(Seconds): 300
- Value: xx.xx.xx.xx

Nameにワイルドカード使用してすべてのサブドメインに対してRecordをセットしておく。こうしておくことで `user1.example.com` でアクセスされた場合も `user2.example.com` でアクセスされた場合も同じRecord Setを適応できる。AliasはYesにしてELBをはさんだ場合もNoにして特定のインスタンスを指定した場合も、メインドメインのAレコードと合わせるようにする。TTLは3600とかに設定してしまうと反映に1時間かかったりするのでひとまず300とかにしておく。

5分ほど時間をおいた後、digコマンドでレコードが正常に反映されているか確認する。メインドメインとサブドメインのAレコードが同じになっていればok。さらに、ブラウザからも確認しておく。

```
$ dig example.com

;; ANSWER SECTION:
example.com.		300	IN	A	xx.xx.xx.xx
```
```
$ dig user1.example.com

;; ANSWER SECTION:
user1.example.com.		300	IN	A	xx.xx.xx.xx
```

## 2. ローカル環境でサブドメインのページを表示させる

ローカル環境にてサブドメインでアクセスされた場合にトップページ(root_path)を表示するようにする。とりあえず確認するだけであれば `/etc/hosts` にずらずらと書き連ねても良いが増えてくるととてもつらい。

```/etc/hosts
127.0.0.1 localhost example.com user1.example.com user2.example.com user3.example.com
```

メインドメインでアクセスしてもサブドメインでアクセスしても `127.0.0.1` に返してくれるドメイン(サービス)があるのでそれをつかう。ループバックドメインと呼ばれるもので、localtest.me / devd.io / loopback.jp などがあるが、比較的よく知られている、lvh.meをつかった。ちなみに、サブドメインでのアクセスを `127.0.0.1` に返してあげるだけのシンプル設計なので自作してもよい。

lvh.me:3000 / user1.lvh.me:3000 でroot_pathが表示されることを確認する。

## 3. Rails側でサブドメインを受け取りuser#showにルーティングする

ユーザページ(user#show)を `example.com/user1` -> `user1.example.com` で表示できるように変更する。Railsではサブドメインの値(request.subdomain)など、`require` でリクエストを送ってきたユーザのヘッダー情報や環境変数を受け取ることができる。

参考：[request - リファレンス - - Railsドキュメント](http://railsdoc.com/references/request)

`www.example.com` がサブドメインとして認識されてしまうため、libにwwwをはじくためのclassを用意しておく。

```ruby:lib/subdomain.rb
class Subdomain
  def self.matches?(request)
    # wwwがサブドメインとして認識されてしまうのを防ぐ
    request.subdomain.present? && request.subdomain != 'www'
  end
end
```

routesに以下の記述を追加し、`user1.example.com` でアクセスされた際にusers#showで処理するようにしておく。

```ruby:config/routes.rb
require Rails.root.join('lib', 'subdomain.rb')
constraints Subdomain do
　　get 'user', to: 'users#show', path: '/'
end
```

後は、controller側で request.subdomain の有無で分岐してあげればよい。

```ruby:app/assets/controllers/users_controller.rb
class UsersController < ApplicationController
  def show
    if request.subdomain.blank?
      @user = User.find_by!(username: params[:username])
    else
      @user = User.find_by!(username: request.subdomain)
    end
  end
end
```

lvh.me:3000 / user1.lvh.me:3000 でそれぞれ、user1のユーザページが呼ばれるようになっていればok。ここまでできれいれば、後はdeployして正しくルーティングされるか確認するだけ。

## 4. 補足：サブドメイン<->メインドメイン間でcookieを共有する

Rails側では別hostとして認識されているため、「サブドメインでuser1としてサインインしていても、メインドメインではサインインできていない」という場合がある。ユーザの認証管理にdeviseをつかっている場合は、解決しないとだめだった。具体的には、session_store.rb に以下の記述を追加しておく。`key:` には各自のアプリケーション名に応じて変える。

```ruby:config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store, key: app_key, domain: :all
```

cookieがキャッシュに残ったままになっていて反映されない場合は、ブラウザの履歴からcookieを削除してあげると解消するはず。

## 5. 補足：サブドメイン<->メインドメイン間をリンクで遷移する

サブドメインのページからメインドメインのページにリンクで遷移する際、baseタグに特に記述がなければ、サブドメインのページを起点とした相対パスになる。この状態でlink_toヘルパーなどをつかってroot_pathを指定したとしても、サブドメインのroot_pathに遷移されてしまうため少々不便。リンクに引数を持たせるなどしてサブドメインとメインドメインのパスを切り替えるheplerをつくってあげるか、手っ取り早くbaseタグに直接指定してあげると良さそう。