任意のサブドメインを利用するRailsアプリをHerokuで

r7kamura.hatenablog.com や r7kamura.tumblr.com のように任意のサブドメインを利用するアプリをRailsで開発しているときに、Herokuにデプロイして独自ドメインを設定することなく何としてでも *.herokuapp.com ドメイン下で動作させるという、極めてニッチな情報について説明します。

背景

*.*.herokuapp.com は使えない

まず前提として、Herokuで利用する *.herokuapp.com のドメイン下では、r7kamura.myapp.herokuapp.com のようにサブドメインを2つ重ねるような利用方法は許可されていません。そのため、独自ドメインを設定するなど、何らかの対策を講じる必要があります。

雑な検証環境としてHerokuを利用したい

本番環境では流石に独自ドメインを設定して運用すると思いますが、検証用の環境についても独自ドメインを設定するとなると、少し面倒になってくると思います。プロトタイプ段階やリリース前の段階では、本番環境の用意やドメイン取得より前に検証環境が必要になることもあるだろうし、そういう場合に独自ドメインを設定せずともHerokuを利用して簡単に環境を用意できると便利な訳です。

逐一独自ドメインを設定するのはだるい

また、検証環境でも一度くらいなら独自ドメインを設定する手間を掛けても良いかもしれませんが、Pull Requestを作成するたびに新しいアプリを作成するような無駄に豪華な使い方をするようになると、その度に独自ドメインを割り当てたりする訳にもいかなくなってきます。

本番用のドメインを使い回すのも面倒

検証環境用に別途ドメインを取得したりするのは勿体無いので、大抵の場合は本番環境用のドメインに staging. のようなサブドメインを付けて済ませたりするものですが、前述の通りサブドメインを利用するアプリであるという性質上この部分が衝突して面倒なことになるため、やはりサブドメインを設定せずに何とかしてアプリを動かす方法があると嬉しい訳です。

対策

要点としては以下の通りで、サブドメイン相当の情報をURLクエリに付与させてアプリに認識させるという方法が取れます。

  1. サブドメインの代わりにURLクエリでも認識できるようにする
  2. サブドメインの付いたURLを生成する代わりにクエリの付いたURLを生成する
  3. cookieのドメインを変更する

サブドメインの認識

Herokuで動かす場合はHerokuのアプリ名がサブドメインの末尾に付与されるため、アプリ上ではこれを取り除いたものを本来のサブドメインだと認識する必要があります。request.subdomainをwrapしたメソッドをつくり、アプリ上でサブドメインを参照したいときは必ずこのメソッド経由で参照するようにします。

class ApplicationController < ActionController::BAse
  def subdomain
    if heroku?
      request.subdomain.split(".")[1..-1].join(".")
    else
      request.subdomain
    end
  end
end

URL生成

polymorphic_urlとurl_forをoverrideして、subdomainオプションが指定されていたときはURLクエリを付け、また相対パス指定だった場合にはURLクエリを引き継ぐようにします。以下の例ではサブドメインの代わりに ?username=r7kamura のようなクエリが使われるようにしています。注意点として、users_pathのようにnamed route由来のメソッドを利用しているとここでoverrideしたメソッドが利用されないので、url_for(:users)link_to [:login, subdomain: nil] do のようにsymbol等を利用する方法で統一しておく必要があります。下記のmoduleをcontrollerとviewにincludeしておけば、URL生成部分の対応は完了です。

module SubdomainUrlHelper
  if heroku?
    def polymorphic_path(*args)
      override_subdomain!(args.last)
      super
    end

    def polymorphic_url(*args)
      override_subdomain!(args.last)
      super
    end

    def url_for(*args)
      override_subdomain!(args.last)
      super
    end

    private

    def override_subdomain!(options)
      case options
      when Hash
        case
        when options.has_key?(:subdomain)
          if options[:subdomain].nil?
            options.delete(:subdomain)
          else
            options[:username] = options.delete(:subdomain)
          end
        when options[:only_path] != false && current_blog
          options[:username] = current_blog.username
        end
      end
    end
  end
end

class ApplicationController < ActionController::Base
  include SubdomainUrlHelper
end

cookie

任意のサブドメインを利用するアプリでsessionを保存するのにcookieを利用している場合、大抵の場合はdomainオプションとして:allが指定されることになると思います。これにより、cookieに .example.com のようなドメインが指定されることになります。しかしHerokuでこれをやると、cookieに指定されるドメインが .herokuapp.com になってしまうため、ドメインを指定しておく必要があります。

Rails.application.config.session_store :cookie_store, key: '_myapp_all_domain_session', domain: ".myapp.herokuapp.com"

注釈

上記のサンプルコードでは heroku? と適当に書いて条件分岐させましたが、実際には環境変数とSettingslogicを併用する等して条件分岐させるのが良いと思います (デフォルトでは普通にサブドメインを利用するが、特定の環境変数が指定されていた場合はURLクエリを利用するモードに切り替える)。

おわり

Herokuでは100個までアプリが作れるので無数の検証環境を作っていきましょう。