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


この記事は Speee Advent Calendar 2017 6日目の記事です。
5日目は @hatappi による Red Chainerをコードを変更せずに約2倍くらい早く処理させる でした。

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

今回は、その運用の中で学んだ、 Rails 5.1 の Encrypted secrets と config gem を併用するためのコツを書き留めておきたいと思います。

config gem (rails_config)

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

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

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

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

Encrypted secrets

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

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

Rails 5.2 で deprecated

Rails 5.2 では Encrypted secrets が deprecated となり、Credentials に置き換わる予定です。マイナーバージョン1つ分という、なんとも短い人生でした。

$ 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

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

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

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

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

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

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

config gem は #add_source! で、任意のハッシュを設定値として追加できます。なので、イニシャライザに下記のようなコードを追加することで、意図した事が実現できそうです。

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

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

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

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

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

このファイルは Config 自体の設定 と定義されていて、ここではまだ設定値 Settings は使用できません。そのため、異なるファイル名のイニシャライザを使用する必要があります(例:config_with_encrypted_secrets.rb)。

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

動作確認

config/secrets.yml.enc (復号時)
production:
  hoge: fuga
$ rails runner -e production 'puts Settings.hoge'
fuga

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

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

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

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

staging:
  <<: *default
  username: staging
  password: <%= Settings.database.password %>
config/secrets.yml.enc (復号時)
staging:
  database:
    password: pass

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

$ 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 を設定することで解消できます。

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

Rails 5.2 では...

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

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) - なるようになるブログ

おわりに

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

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

ボツアプローチ

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

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

などなど。

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

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

次回は弊社広報の @mogmog214 より、『自社テックブログの更新頻度を2年で6倍にした話』です!
自社テックブログの更新頻度を2年で6倍にした話 - mogmog2の日記