概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Anyway Config: Keep your Ruby configuration sane — Martian Chronicles, Evil Martians’ team blog
- 原文公開日: 2020/04/14
- 著者: Vladimir Dementyev
- サイト: Evil Martians -- ニューヨークやロシアを中心に拠点を構えるRuby on Rails開発会社です。良質のブログ記事を多数公開し、多くのgemのスポンサーでもあります。
日本語タイトルは内容に即したものにしました。
Rails: anyway_config gemでRubyの設定を正しく整理しよう(翻訳)
コンフィグは、コードベースの健康状態を最も如実に表すマーカーのひとつです。アプリケーションが成長して成熟すればするほど、APIキーだのenv
ファイルだのといった設定を扱うのが面倒になってきます。私がRailsConf 2019でお話しした「Terraforming legacy Rails applications」というセッションで、いわゆる「ENV地獄」についても触れました。大規模Rubyアプリを扱ってきた読者の皆さまのほとんどが、何らかの形でENV地獄の因縁にまとわりつかれていると確信しています。
⚓皆さんもENV地獄行きですか?
Railsアプリで最も広く使われている設定パターンは、すべての値を.env
に保存して、アプリケーションの起動時に(dotenv gemやdotenv-rails gemなどを用いて)プロセス環境に読み込み、ENV["KEY"]
でコードからアクセスできるようにするという手法です。
このパターン自身の由来は真っ当です。かの有名なTwelve Factorの原則にも『設定は環境に保存すべし』と述べられています。
しかしここでひとつシンプルな実験をしてみましょう。シェルを開いて.env
のあるプロジェクトディレクトリのルートディレクトリに移動し、以下のコマンドを打ってください。
cat .env | grep '[^\s]' | wc -l
52 # これは私たちが目にした成熟プロジェクトの平均の数値です
この数値はいったい何でしょう?あなたの数値が数十個のオーダーに達していたら、残念ながらあなたは今「ENV地獄」の真っ只中です。私たちもENV地獄にいたので、それはまあいいとしましょう。
ENV地獄にハマるとだいたい以下のようなことになります。
.env
ファイルが肥大化し、ほぼ理解不能になる.env.sample
ファイルの同期が取れなくなって、デバッグしづらい失敗がローカル開発中に発生するようになるENV
は外の世界を表すグローバルステートなので、デバッグ、特にテストがつらくなってくる
ENV
利用の治安を保つために、私たちはLint/Env
というRuboCopのcopを書きました。グローバルステートを設定ファイルの外に漏らさないためにお使いいただけます。こうした問題は、たいてい開発環境のセットアップ中に忍び寄ってきます。たとえばHerokuアプリのコンフィグは、数百個もの環境変数であふれがちです。以下を実行してクイックチェックできます。
$ heroku config -a legacy-project | wc -l
131
今度は、皆さんのRailsプロジェクトの現状を取り急ぎ調査しましょう。
- プロジェクトを(
bin/setup
などで)セットアップした直後にrails s
で起動できますか? - 続いてテストを実行するとパスしますか?
- アプリケーションのcredentialがない場合に、credentialの置き場所がすぐわかるようになっていますか?
- 設定に新しい値を追加するときのワークフローは明確になっていますか?
- サードパーティAPIトークンなどの固有の秘密情報は、アプリケーションのコードを変更せずに利用できるようになっていますか?
「いいえ」の数が多いほど、設定のアプローチを見直す作業をその分急ぐべきです。
調査の方法については次をご覧ください。
⚓設定パラメータは「秘密情報」と「設定」の2種類ある
大量の卵(設定パラメータ)を1つの籠(.env
ファイル)に押し込める方法には、ひとつ大きな欠点があります。つまり、その値がそもそも何であるかという「本質」の情報が失われてしまうのです。機密情報と機密でない情報や、ビジネスロジック情報とフレームワーク関連の情報がまぜこぜになってしまいます。
設定値のトラッキングを改善するために、「設定」と「秘密情報」を心の中で分けます。両者の具体的な違いについて見ていきましょう。
⚓設定は「内部に属する」
WEB_CONCURRENCY
、RAILS_MAX_THREADS
、RAILS_SERVE_STATIC_FILES
といった設定は、技術上の特性やフレームワーク設定を改変します。これらを「フレームワーク設定」と呼ぶことにします。ここで言うフレームワークはRailsに限らず、PumaやSidekiqなどスタック上のあらゆるものを指します。
もうひとつは「アプリケーション設定」です。これは、グローバルな機能をオンオフするCHAT_ENABLED=1
や、開発ツールを有効にするGRAPHIQL_ENABLED=1
フラグなどが該当します。
理想的な設定とは、以下の要素を満たすものです。
- development環境やtest環境の常識的なデフォルト設定は必ず完了していなければならない
- production環境では必要に応じてそれらの設定を環境変数で上書きできる(Herokuでは
RAILS_SERVE_STATIC_FILES
が自動的に設定されます) - 設定をプレーンテキストでリポジトリに保存できる
- 必要なら環境固有の設定ファイル(
config/environments/development.rb
)にハードコードすることも可能
⚓秘密情報は「外部に属する」
秘密情報は、他のシステムやサービスとのやりとりで必要な情報を指します。秘密情報はさらに以下のの2種類に分けられます。
- システム秘密情報
- サービス秘密情報
- システム秘密情報
- インフラの必要不可欠な部分にアクセスするためのcredential
(データベースDATABASE_URL
、キャッシュサーバーREDIS_URL
、その他)
たとえばHerokuのアドオンは、productionで使うそれらの値を設定するので、気にする必要があるのはdevelopment環境だけです(私たちの場合は、そうした設定をdocker-compose.ymlに保存しています)。
- サービス秘密情報
- サードパーティサービスのcredential
(APIキー、トークン、その他)
秘密情報に該当する情報であっても、必ずしも隠さなければならないとは限りません。APIの「ホスト名」や「上限値」のことを考えればおわかりでしょう。
「システム秘密情報」と「サービス秘密情報」には、技術的に重大な違いがひとつあります。
システム秘密情報がない場合や無効な場合、アプリケーションは必ず起動に失敗しなければなりません。
これは、サービス秘密情報の場合には必ずしも真ではありません。サービス秘密情報は、アプリケーションが起動するために必要不可欠な設定とは限らないからです。
⚓Active Storageの例
たとえば、ファイルアップロード機能を実現するのにActive StorageとAWS S3バックエンドを用いることに決めたとしましょう。以下はmasterブランチにマージされたstorage.yml
と設定ファイルです(なおコードは実際のコミットからの引用です)。
# config/application.rb
config.active_storage.service = :s3
# config/environments/test.rb
config.active_storage.service = :local
# config/storage.yml
local:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
s3:
service: S3
access_key_id:
secret_access_key:
region: us-east-1
bucket: ENV["AWS_BUCKET"]
こうすることで、AWS関連の環境変数が設定されてないチームメンバーは、たとえdevelopment環境であってもアプリの実行は不可能になります。言ってみれば、私たちは玄関に人工的なバリアーを構築しています(たとえAWS SDKをローカルで直接利用することが必須でないとしてもです)。Active Storageでは、:local
設定をtest環境のみならず、development環境でも利用できるようになっています。もし誰かが「自分のローカル環境をproductionになるべく近づけたいんです!」と言ったとしましょう。お気持ちはありがたいのですが、そんな厳しい要件が必要でしょうか?
ここで仮に、development環境で以下のように本物のAWSバケットを利用できる機能をオプトインとして導入したと想像してみましょう。
# config/application.rb
config.active_storage.service =
if AWSConfig.storage_configured?
$stdout.puts "Using :s3 service for Active Storage"
:s3
else
:local
end
おやおや、こんな見慣れない書き方をRailsで手軽に使えるものでしょうか。そもそもAWSConfig
が何なのか、それがどんなふうに設定されているのかをどうやって知ればよいのでしょう。そこで、本記事の冒頭からチラ見せしていたAnyway Config gemの出番です。
⚓Anyway Configの概要
今どきのRailsでは、環境変数に保存した設定データ以外にもさまざまな選択肢が使えます。config/initializers
のパラメータを直接編集する方法、昔ながらのyamlファイル編集による方法、そしてRails 5.2からは暗号化済みcredentialを用いてソースコード管理に安全にチェックインもできます。
Rails 6.0では、環境ごとにcredentialを使い分ける機能も追加されました。なお、このgistを使えばそれらをRails 5.2にバックポートできます。
私たちEvil Martiansが今試している、設定における鉄の規律は以下のとおりです。
- 重要な情報は、Railsのcredentialに保存する(環境ごとに
*.enc
ファイルを作る) - 重要でない情報は、名前を付けてyaml設定に保存する
- あらゆる値を環境変数で上書きできるようにする
- ローカル(development環境)の秘密情報や設定は、
*.local.yml
とcredentials/local.yml.enc
に保存する - 取り扱いに特別な注意を要するcredentialをチームメンバー全員で共有する必要がある場合は、暗号化されたストレージで集中管理する。Keybaseはこれを一手に引き受けます。
「選択肢が多すぎる問題」を避けるのは困難です。各開発者が思い思いの設定方法で、設定を片っ端からアプリにぶちこむと、開発者の認知能力にずしりと負担がのしかかります。そこで私たちは、ピュアなRubyインターフェイスを標準としてすべての設定に提供するツールを使うという手法にたどり着いたのです。
Anyway Config gemは、さまざまな場所に散在するデータを透過的に管理できます。さらに、コンフィグ専用のクラスを導入することで、設定をコードとは独立に保存します。
もうRails.credentials
だのRails.application.config_for
だのENV
を呼び出す必要はありません。必要なのはRubyのクラスを扱うことだけです。
このgemができるまでに長い話があります。当初は、私が最初に作ったInfluxerというgemから抽出しました。なおInfluxerはほとんどの場合ライブラリ(AnyCableなど)で使われてきました。最近リリースしたAnyway Config 2.0は、ここ数年Evil Martians社内のアプリケーション開発のユースケースから大いなるヒントを得ています。
それではActive Storageの例に戻って、Anyway Configでどんなふうに設定できるかを見ていきましょう。
RailsのGemfileにanyway_config
を追加すると、新しいコンフィグクラスを作成する手軽なジェネレータにアクセスできるようになります。
$ rails generate anyway:config aws access_key_id secret_access_key region storage_bucket
generate anyway:install
rails generate anyway:install
create config/configs/application_config.rb
append .gitignore
insert config/application.rb
create config/configs/aws_config.rb
Would you like to generate a aws.yml file? (Y/n) n
ジェネレータを実行すると、以下の2つのファイルがプロジェクトに追加されます。
config/configs/application_config.rb
: すべてのコンフィグクラスのベースクラス(存在しない場合にのみ作成される)
app/configs
ではなくconfig/configs
を使っている理由は、Railsのオートロードやオートリロードに関連します。詳しくはREADMEの"Organizing configs"をご覧ください。# ファイルを設定するための抽象ベースクラス
# 設定のデフォルトインスタンスを返す`instance`メソッドを提供する
#
# また、missingメソッドはすべてインスタンスに委譲されるので
# このクラス自身をシングルトンのコンフィグインターフェースとして使える
class ApplicationConfig < Anyway::Config
class << self
delegate_missing_to :instance
def instance
@instance ||= new
end
end
end
config/configs/aws_config.rb
: AWS設定用のクラス
class AWSConfig < ApplicationConfig
# attr_configはパラメータ設定用の
# リーダーとライターを定義する
attr_config :access_key_id, :secret_access_key,
:region, :storage_bucket
end
なお、以下のようにconfig/initializers/inflections.rb
に略語を追加することで、「AWS」という語をRailsのinflectorで認識できるよう設定する必要があります。
ActiveSupport::Inflector.inflections do |inflect|
# ...
inflect.acronym "AWS"
end
ジェネレータのプロンプトでyesを入力すると、config/aws.yml
ファイルも追加されます。今は情報をプレーンテキストに保存したくないので、credentialを使うことにします。
作成したコンフィグクラスを編集してregion:
にデフォルト値を追加し、#storage_configured?
メソッドも追加します。
class AWSConfig < ApplicationConfig
# ハッシュをひとつ渡すことでデフォルト値を指定できる
attr_config :access_key_id, :secret_access_key,
:storage_bucket, region: "us-east-1"
def storage_configured?
access_key_id.present? &&
secret_access_key.present? &&
storage_bucket.present?
end
end
続いて、production用の値も渡せるようにする必要があります。credentialファイルを開いて以下の値を定義しましょう。
$ RAILS_MASTER_KEY=<production key> rails credentials:edit -e production
aws:
access_key_id: "secret"
secret_access_key: "very-very-secret"
storage_bucket: "also-could-be-a-secret"
storage_bucket
は、ユースケースによっては重要な情報の一部とみなす必要はないかもしれません。その場合は値をconfig/aws.yml
に設定できます。
# config/aws.yml
production:
aws:
storage_bucket: my-public-bucket
対応する環境変数に値を設定すれば、いつでも値をオーバーライドできます。
AWS_STORAGE_BUCKET=another-bucket rails s
ところで、私たちのコードでは設定値がどこにあるかを気にする必要がないことにお気づきでしょうか?コードが認識しているのはAWSConfig
クラスだけです。このアプローチの主なメリットが、まさにこれです。
今後AWSをローカルで使うことが決まったら、config/aws.local.yml
に個人用のcredentialを書き込めば済みます。秘密情報を自分のコンピュータ上にプレーンテキストで保存するのがイヤな場合は、Railsのローカルcredentialを使えばよいのです。
rails credentials:edit -e local
Anyway Configの設定は、ローカルデータよりも優先して代入されます。
さらにうれしいオマケとして、あらゆる値の出どころをトラッキングできます。これは特にproduction環境で便利です。
AWSConfig.to_source_trace
# =>
# {
# "access_key_id" => {value: "XYZ", source: {type: :credentials, store: "config/credentials/production.yml.enc"}},
# "secret_access_key" => {value: "123KLM", source: {type: :credentials, store: "config/credentials/production.yml.enc"}},
# "region" => {value: "us-east-1", source: {type: :defaults}},
# "storage_bucket" => {value: "example-bucket", source: {type: :yml, path: "config/aws.yml"}}
# }
pp
で人間にとって読みやすい出力を得ることもできます。
pp AWSConfig.new
# =>
# #<AWSConfig
# config_name="aws"
# env_prefix="AWS"
# values:
# access_key_id => "XYZ" (type=credentials store=config/credentials/production.yml.enc)
# secret_access_key => "123KLM" (type=credentials store=config/credentials/production.yml.enc)
# region => "us-east-1" (type=defaults)
# storage_bucket => "my-public-bucket" (type=yml path=config/aws.yml)
まとめると、Anyway Configでは以下を行えます。
- さまざまなデータソースラッパーの代わりにコンフィグ用クラスを使える
- ローカルの秘密情報や設定もサポートされている
- 設定ファイルが分かれているので、単一の
.env
やapplication.yml
に設定をパンパンに押し込める必要がない
Rubyアプリケーションが成長するに連れて、設定の管理はたちまち悪夢と化します。設定ファイルの編成方法や、さまざまな種類の値の扱い方に注意することで、設定の治安を維持できるようになります。
あらゆる個別の設定データに関する知識をコードベースから切り離すことで、プロジェクトの健全性も保てます。Anyway Config gemは、あらゆるユースケースに合う共通の抽象化を提供しているので、開発者の認知能力に負担をかけることなく、さまざまなところからやって来るさまざまな設定値を自由自在にミックス、比較、オーバーライドできるようになります。
ENV
は責任を持って使いましょう!
ところで、皆さんのよく育ったRailsアプリケーションを「テラフォーミング」して、以下のような最新のベストプラクティスを導入することにご興味はおありですか?
- development環境のコンテナ化
- テストの高速化
- GraphQLの導入
- データベースクエリの最適化
- パフォーマンスの一般的なボトルネックの除去
私どもEvil Martiansが皆さまにお力添えいたします。
Evil Martiansでは、外宇宙流の製品開発およびご相談を承ります。
おたより発掘
設定はconfig gemによるYAML管理、機密情報はENVでずっと運営していて、あまり困ったことがないなあ
anyway_config はブラックボックスを排除した config って感じRails: anyway_config gemでRubyの設定を正しく整理しよう(翻訳)|TechRacho|BPS株式会社 - https://t.co/7ch5DZCmXB
— Jaga Apple (@jagaapple_tech) June 3, 2020