読者です 読者をやめる 読者になる 読者になる

設定きりわけ大作戦

Misoca開発チームの黒曜(@kokuyouwind)です。

先日秋葉原UDXで行われた技術書典2にサークル参加して情報ガールを頒布してきました。*1

本気で原稿を落としそうになりましたが、なんとか入稿できてよかったです。

f:id:kokuyouwind:20170419175003j:plain

「原稿は早めに書こう」という教訓を得た結果、この開発ブログはそこそこ早めに書き始めています。*2

🔧 環境設定について

今回は設定ファイルの話をします。

MisocaではSettingsLogicを使い、RAILS_ENVに応じて設定を切り替えられるようにしています。*3

しかしアプリケーションの成長に伴い設定ファイルが肥大化し、以下のような設定ファイルができあがっていました。

default: &default
  application: &default_application
    host_with_port: "localhost:3000"
    protocol: http
  service_a:
    host: "<%= ENV['SERVICE_A_HOST']%>"
    token: "<%= ENV['SERVICE_A_TOKEN']%>"
    user_path: /api/v1/user
    issue_path: /api/v2/issue
  # こんな感じで設定が100行くらい続く

development:
  <<: *defaults
  service_b:
    host: dev.example.com
    application_id: misoca_dev
    secret: secret_dev_b
  service_c:
    type: mock

production:
  <<: *defaults
  application: <<: *defaults_application
    host_with_port: "<%= ENV['APPLICATION_HOST_WITH_PORT']%>"
    protocol: https
  service_b:
    host: prod.example.com
    application_id: misoca
    secret: secret_prod_b
  service_c:
    type: api
    application_id: misoca
    secret: secret_prod_c
  # こんな感じで設定が50行くらい続く

こうなると、設定を追加するのも確認するのも大変になってきます。

特にしんどいのは外部サービスの設定で、

  • RAILS_ENVに関わらず、環境変数で設定を変える(service_a)
  • RAILS_ENVで切り替える(service_b)
  • 開発環境ではモックする(service_c)

など設定方法がそれぞれで変わってしまい、設定の見通しを更に悪くしています。

🔪 設定の分割

設定ファイルの肥大化を解決するため、外部サービスの設定は別ファイルに切り出していくことにしました。

service_bの設定であれば、以下のように専用の設定ファイルを読み込むコードを書いておきます。

class ServiceBSettings < Settingslogic
  source "#{Rails.root}/config/service_b.yml"
  namespace Rails.env
end

こうすると、service_b.ymlは下記のようにとてもシンプルになります。

development:
  host: dev.example.com
  application_id: misoca_dev
  secret: secret_dev_b
production:
  host: prod.example.com
  application_id: misoca
  secret: secret_prod_b

同様に、service_cの設定は以下のようになります。

development:
  type: mock
production:
  type: api
  application_id: misoca
  secret: secret_prod_c

元の設定と比べて、環境ごとの設定がわかりやすくなりましたね。

🚩 設定切り替え条件の変更

ステージングサーバと本番サーバで外部サービスの向き先を変えたい場合は、service_aのように環境変数でサーバを切り替えることになります。*4

このような設定は、SettingsLogicのnamespaceを利用して、1つの環境変数で設定項目をまとめて切り替えるようにしました。

class ServiceASettings < Settingslogic
  source "#{Rails.root}/config/service_a.yml"
  namespace ENV['SERVICE_A_ENV'] || 'development'
end

設定ファイルであるservice_a.ymlは以下のようになります。

default: &default
  user_path: /api/v1/user
  issue_path: /api/v2/issue
development:
  <<: *defaults
  host: dev.servicea.example.com
  token: token_dev
production:
  <<: *defaults
  host: servicea.example.com
  token: token_prod

こうすることで環境変数の設定が簡潔になり、各環境での設定内容をきちんと設定ファイルに記述できるようになりました。

🏗 URLの構築

上述の設定ファイルのようにホスト名とパスをそれぞれ定義した場合、URLを使う箇所で"https://#{ServiceASettings.host}#{ServiceASettings.user_path}"のようにURLを組み立てる必要が出てきます。

これは使いづらかったので、以下のように設定クラスを拡張し、*_urlに対応する設定が見つからない場合には*_pathからURLを生成して返すようにしました。

class ServiceASettings < Settingslogic
  source "#{Rails.root}/config/service_a.yml"
  namespace ENV['SERVICE_A_ENV'] || 'development'

  # *_urlが見つからず、対応する*_pathがある場合はhostを含めたフルURLを生成して返す
  def method_missing(name)
    if name =~ /_url\Z/ && host
      path_key = name.to_s.gsub(/_url\Z/, '_path').to_sym
      url = URI.join("https://#{host}/", send(path_key)).to_s
      create_accessor_for(name, url)
      url
    else
      super
    end
  end
end

これで、ServiceASettings.user_urlのようにアクセスすることができるようになります。

設定ファイルを切り分けることで、こういった各設定ごとのヘルパーも生やすことができるようになり、なかなか便利です。*5

📢 宣伝

Misocaでは設定ファイルを綺麗にしたいエンジニアを募集しています。

*1:リンク先はおためし版になってます。全文PDFはいろいろ手直しがあり、現在頒布準備中です…

*2:結局、あまり余裕のない時間に書き上がりました

*3:類似のGemにconfigなどがあります。最近だとこちらのほうが主流かもしれません。

*4:RAILS_ENVとしてstaging環境を追加する方法もありますが、環境を増やすとそれだけ複雑になるためMisocaでは行っていません。

*5:設定クラスでやるのは邪道かもしれませんが、このためだけに別のラッパークラスを作るのもなぁ… と迷った末、今回はこうしました。