Rails 5.1 Encrypted secrets を config gem と併用する

この記事は [Speee Advent Calendar 2017](https://qiita.com/advent-calendar/2017/speee) 6日目の記事です。
5日目は @hatappi による [Red Chainerをコードを変更せずに約2倍くらい早く処理させる](http://hatappi.hateblo.jp/entry/2017/12/05/090625) でした。

数ヶ月前に、弊社の福利厚生制度でもある [Speee Library](https://speee.jp/about/office/) のシステム全面リニューアルが行われ、アプリケーションが Rails 5.1 でリライトされました。

今回は、その運用の中で学んだ、 Rails 5.1 の Encrypted secrets と [config](https://github.com/railsconfig/config) gem を併用するためのコツを書き留めておきたいと思います。

## [config](https://github.com/railsconfig/config) gem (rails_config)

YAML (`config/settings.yml`) でアプリケーション設定を一元管理できる、定数管理 gem です。

環境別の設定 (`config/settings/{environment}.yml`) 、各個人の開発環境毎に異なる設定 (`config/settings.local.yml`) 、環境変数による設定 (`use_env` オプション) などの機能を備えています。

- [Railsで定数を環境ごとに管理するrails_config（現 config） - Qiita](https://qiita.com/yumiyon/items/32c6afb5e2e5b7ff369e)

設定値の管理を明瞭に行えるため、私は好んでこの gem を使用しています。

一方、production 環境の API キーのような、リポジトリに含めるべきでない秘匿情報はどう管理しようか？ という課題もありました。

## Encrypted secrets

Rails 5.1 で導入された仕組みで、API キーやインフラ設定などの秘匿情報を、`config/secrets.yml.enc` で暗号化して管理できるようになります。

- [Encrypted Rails Secrets on Rails 5.1](https://www.engineyard.com/blog/encrypted-rails-secrets-on-rails-5.1) (Engine Yard)
- [Rails 5.1のencrypted secretsの運用について考える - Qiita](https://qiita.com/kikunantoka/items/dd22cf51071b93ba5b62)

秘匿情報の管理については [yaml_vault](https://github.com/joker1007/yaml_vault) などの先行例もありましたが、今回のプロジェクトではせっかく Rails 5.1 を使える状況なので、Rails way な方法で管理してみることにしました。

### :warning: Rails 5.2 で deprecated

Rails 5.2 では Encrypted secrets が **deprecated** となり、**[Credentials](https://github.com/rails/rails/pull/30067)** に置き換わる予定です。マイナーバージョン1つ分という、なんとも短い人生でした。:innocent: 

```bash
$ bin/rails secrets:edit

Encrypted secrets is deprecated in favor of credentials. Run:
bin/rails credentials --help
```

> *[Rails 5.2.0 beta: Active Storage, Redis Cache Store, HTTP/2 Early Hints, CSP, Credentials | Riding Rails](http://weblog.rubyonrails.org/2017/11/27/Rails-5-2-Active-Storage-Redis-Cache-Store-HTTP2-Early-Hints-Credentials/)*

Encrypted secrets と異なり、Credentials は 動作環境の設定が (beta2 時点では) ないため、環境毎の設定値を扱う場合は自分で工夫する必要があります（値取得時に `Rails.env` をキーにするなど）。

# Encrpyted secrets + config 併用のアプローチ

自身のプロジェクトでは、全面的に config gem に乗っかっていたので、以下のようなアプローチを取りました。

- Encrypted secrets で設定した値を config gem の設定にマージする
- Rails アプリ内では config gem の値を使う (＝`Settings` を参照する)
- :arrow_right: config gem で Encrypted secrets の値を透過的に参照する

基本的には config gem を使い、production 環境の設定値は Encrypted secrets で管理するといった具合です。

## イニシャライザで設定値をマージする

config gem は `#add_source!` で、[任意のハッシュを設定値として追加](https://github.com/railsconfig/config#adding-sources-at-runtime)できます。なので、イニシャライザに下記のようなコードを追加することで、意図した事が実現できそうです。

```rb
Settings.add_source!(Rails.application.secrets.deep_stringify_keys)
Settings.reload!
```

`Settings` は内部で文字列キーを使用しているため、`#deep_stringify_keys` で変換して正しくマージできるようにしています。

### イニシャライザのファイル名

この処理を `config/initializers/config.rb`（`rails g config:install` を実行すると作られる）に書きたくなるのですが、そうすると `NameError` が返ってきます。

```
/Users/yukihattori/rails51/config/initializers/config.rb:1:
in `<top (required)>': uninitialized constant Settings (NameError)
Did you mean?  String
```

このファイルは [`Config` 自体の設定](https://github.com/railsconfig/config/blob/master/lib/config/integrations/rails/railtie.rb#L6) と定義されていて、ここではまだ設定値 `Settings` は使用できません。そのため、異なるファイル名のイニシャライザを使用する必要があります（例：`config_with_encrypted_secrets.rb`）。

```rb:config/initializers/config_with_encrypted_secrets.rb
Settings.add_source!(Rails.application.secrets.deep_stringify_keys)
Settings.reload!
```

### 動作確認

```yaml:config/secrets.yml.enc&nbsp;(復号時)
production:
  hoge: fuga
```

```bash
$ rails runner -e production 'puts Settings.hoge'
fuga
```

Encrypted secrets の値を透過的に扱えてますね！

## データベース設定を Encrypted secrets で管理する時の注意

例えば、staging 環境のデータベース認証情報をリポジトリ管理したいとします。

```yaml:config/database.yml
default: &default
  adapter: mysql2
  database: application
  host: localhost

staging:
  <<: *default
  username: staging
  password: <%= Settings.database.password %>
```

```yaml:config/secrets.yml.enc&nbsp;(復号時)
staging:
  database:
    password: pass
```

ところが、`db:create` を実行するとエラーになってしまいます。

```bash
$ RAILS_ENV=staging bundle exec rake db:create
rake aborted!
NoMethodError: Cannot load `Rails.application.database_configuration`:
undefined method `database' for nil:NilClass
```

`db:create` は、Rails の環境を事前に読み込んでいないため、Encrypted secrets の有効化設定 (`config.read_encrypted_secrets = true`) を認識していないのが原因です。

この問題は `db:load_config` タスクの依存関係に `environment` を設定することで解消できます。

```rb:lib/tasks/db/load_config.rake
namespace :db do
  task load_config: :environment
end
```

### Rails 5.2 では...

Rails 5.2 では、下記 PR にて `environment` が依存関係に追加されているので、この対策は必要ありません。

- [Load environment fle in db tasks by y-yagi · Pull Request #31135 · rails/rails](https://github.com/rails/rails/pull/31135)

> `load_config` taskのdependency に`environment` taskを追加しています。
>
> 元々一部taskでは`environment` taskが実行されていなかった(environmentファイルがロードされてなかった)のですが、それだと、例えば`database.yml`にencrypted secretsを使用している場合に問題になる(environmentファイルがロードされない、という事は`read_encrypted_secrets`がtrueになる事が無い)為、一通りのtaskでenvironmentファイルのロード処理が行われるようにする為に、`load_config` taskのdependency に`environment` taskを追加しています。
>
> ─ *[rails commit log流し読み(2017/11/14) - なるようになるブログ](http://y-yagi.hatenablog.com/entry/2017/11/15/061306)*

# おわりに

config gem での秘匿情報の管理が Encrypted secrets で少し楽になりました。やはりリポジトリで管理できるのは嬉しいですね。

Encrypted secrets 自体は Rails 5.2 での deprecated により賞味期限が迫ってしまったものの、Credentials になってもこの原理自体は変わらないので、同様のアプローチで透過的に秘匿情報を扱えると思われます。

### ボツアプローチ

実は、以前から他のアプローチもいろいろと試しておりました。例えば...

- `settings.yml` 内で ERB を使用し、`<%= Rails.application.secrets.hoge %>` で読み込む
- `config/boot.rb` に `require 'config'` を追加して config gem の Rails インテグレーションを読み込まないようにした上で、自前で `Config.load_and_set_settings` で設定を読み込む

などなど。

実際のプロジェクトでは、ここに至るまで諸々のトラブルがありました。`Config` の読み込みタイミングに起因する問題や、Sidekiq との併用など…

さまざまなトラップを踏んだ末、ここに挙げた後者の方法で長らく運用していて、今回もその話をしようかと思っておりました。
が、この投稿を書くために改めて調査した結果、『一周回ってシンプルに修正できるじゃん！』と気づいた次第です。何事も書き留める事は大切ですね... :pencil:
&nbsp;

次回は弊社広報の [@mogmog214](https://qiita.com/mogmog214) より、『[自社テックブログ](http://tech.speee.jp/)の更新頻度を2年で6倍にした話』です！
:arrow_right: *[自社テックブログの更新頻度を2年で6倍にした話 - mogmog2の日記](http://mogmog2.hatenablog.com/entry/2017/12/07/133429)*