RAILS_ENVの数は増やさず、環境ごとの差異をymlでいい感じに管理する方法

本番環境とローカルの開発環境の他に、ステージング環境やQA環境などと言った環境が複数あるのが一般的です。
こういった環境ごとの異なる設定値（例えばデータベースの設定や、連携しているサービスのURLやIDなど）をどうやって管理するかというのはRailsアプリを運用する上で悩むポイントかと思います。
本記事では、RAILS_ENVを増やすことなく、Railsが用意するRails::Application.config_forを用いて、環境ごとの設定値をymlでいい感じに管理する方法を紹介します。

https://speakerdeck.com/spring_mt/deep-environment-parity-cdnt-2019
複数環境があるという前提で、その環境感の差異を無くして一致させようという話については、このスライドに綺麗にまとまっているので参考にしてください。

## Rails::Application.config_forのおさらい

```rb
module MyApp
  class Application < Rails::Application
    config.application= config_for(:application)
  end
end
```

このように記述すると、configディレクトリ以下にある/application.ymlを読み込み、RAILS_ENVに一致する設定値を読み込んでくれます。

```rb
Rails.configuration.x.application[:hoge]
```

読み込んだあとは、使用したい箇所で上記のように呼び出すことで、yml内にあるhogeというkeyの値が取得できます。
この機能は非常に便利かつRailsが推奨しているものなので、積極的に使っていきたいところです。


## 環境ごとにRAILS_ENVを増やせば良いのでは？

Rails::Application::config_forをそのまま使うには、RAILS_ENVを増やせば単純です。
確かにぐぐるとconfig/environments/staging.rb などを用意して環境変数を増やすという話がよく出てきます。
ただし、この方法はあまり良いとは思いません。
なぜかというと、RAILS_ENVはRailsというフレームワークの挙動を変更するための環境変数であるため、今回のようにアプリケーションの設定値を変更するために用いることはRailsの思想から外れていると私は考えるからです。

## 環境ごとに異なる変数なのだから、環境変数を使えば良いのでは？

データベースのパスワードのような秘匿情報は環境変数を通して渡すことが多いので、それと同様にあらゆる設定値を環境変数にするというのも自然といえば自然です。
ただし、環境変数はインフラ側で設定することになりますし、アプリケーションの設定値なのでアプリケーションのリポジトリ内で管理したいということもあり、環境変数での管理は避けます。

環境変数がカジュアルに追加でき、いい感じにリポジトリで管理されてる場合はこの方法も良いと思います。弊プロジェクトでは、CloudFormationを用いてインフラを管理していますが、CloudFormationは別リポジトリで管理していて、デプロイのタイミングも異なることから環境変数での管理はやめています。

## じゃあどうするんだってばよ

https://speakerdeck.com/spring_mt/deep-environment-parity-cdnt-2019?slide=47
先ほど紹介したスライドの、47〜51ページにあるような感じでやります。ただしEntrykitのprehookを使ってやる部分だけはいじります。
一応詳しく説明していきます。
前述の通り、config_forのためにymlで管理したいので以下のようなディレクトリ構成にしてymlを配置します。
ymlの中身は省略しますが、ymlの先頭のkeyはRAILS_ENVと同一のkeyにしておく必要があります（development, test or production)

```
config/stage_settings
├── circleci
│   ├── application.yml
│   ├── database.yml
│   ├── secrets.yml
│   └── sentry.yml
├── local
│   ├── application.yml
│   ├── database.yml
│   ├── secrets.yml
│   └── sentry.yml
├── production
│   ├── application.yml
│   ├── database.yml
│   ├── secrets.yml
│   └── sentry.yml
└── staging
    ├── application.yml
    ├── database.yml
    ├── secrets.yml
    └── sentry.yml
```

Rails::Application.config_forは、config以下にあるymlファイルを読むことができるメソッドなので、stage_settingsディレクトリ以下に置かれたymlをいい感じにconfig以下に移す必要があります。

先ほど紹介したスライドでは、Entrykitのprehookを使って実現していましたが、Rails起動時に必ず行う処理であれば、config/boot.rbに記述するほうが楽であろうと考え変更しています。（config/boot.rbはRailsの起動前に処理を挟める唯一の場所のはず…）

```rb
# config/boot.rb

ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)

require 'bundler/setup' # Set up gems listed in the Gemfile.
require 'bootsnap/setup' # Speed up boot time by caching expensive operations.

# ↑はRailsが自動生成したコード。ここから独自の処理

stage = ENV.fetch('APP_STAGE', 'local')
path = File.join('config', 'stage_settings', stage)
raise "#{path} is not a directory." unless File.exist?(path)

FileUtils.cp_r(Dir.glob("#{path}/*"), File.join('config'))
```

こんな感じに記述します。APP_STAGEという新しい環境変数が追加されていますが、これはstagingやqa環境などの環境を表すための環境変数です。

これで毎回起動時にconfigディレクトリ以下にstage_settings以下のファイルがすべてコピーされるようになりました。
起動時にどのymlを使うかを定めるため、configディレクトリ直下にはymlをコミットしないように.gitignoreに追加するなどしておきましょう。

なぜこうしたかというと、チーム内にDockerで開発している人もいればそうでない人もいたことと、ローカルの開発環境のセットアップのためにあれこれしなければならないという手間を増やしたくないということから、なにか良い方法は無いかと考えてこれに至りました。

## まとめ

- 環境ごとに設定値を変えたくても、RAILS_ENVは増やさない
- 環境ごとの設定値はymlで管理したい
- boot.rbをに処理を差し込んでいい感じにした