さまざまなソースから設定を束ねる感じでいい具合に書けるgemを作ってみた

wtnabe/bindan: building single configuration object from various sources

動機

12factor 時代に育ったもので、「設定は環境変数に」と考えていたのだけれど、Google App Engine は

  • 環境変数のセット方法の基本が YAML のハードコーディング
  • なんとか Cloud Build にセットして YAML を生成することはできる(ただしログにダダ漏れ)
  • 単純な再起動では反映できず、どうにかビルドするしかない

という仕様になっていて、「秘密情報絶対許さねぇ」という姿勢の割には Secret Manager のリリースは後発で公式エミュレータもない、というほんとうにひどい仕様で「なんだこの言いっぱなしは」とずっと思っていた。

そこで「起動時にいろんなソースから情報を取得しつついい具合に一箇所に書ける方法があればなぁ」とずっと思っていたんだけど、意外とこういうものがない。ほな作るか。で、同じコンセプトの Node.js 版を実は 5年前に作ってたんだけど、Ruby で Google を使う機会が増えてきたので Ruby の方も作ることにした1

目指した書き味

中身の書き味は以下のような感じで考えた。

config = Bindan.configure do |c, pr|
  c.asset_host = pr.env["ASSET_HOST"] || "http://localhost:8080"
  c.credentials = pr.secret["CREDENTIALS"] || pr.env["CREDENTIALS"]
end
  • 複数のソースを pr (provider の意味) から取得できる
  • 値がなければ || でフォールバック
  • これを Ruby 標準の Hash のような書き味で

Node.js で作ってた時はどうしても async/await が顔を出してくるのでだいぶバタついた感じになっちゃうんだけど、Ruby だと同期的に書けるし [] をメソッド名として使えるので書き味はとてもスッキリする。やはりこういう DSL 的な働きと Ruby は非常に相性がいい。

実際の記述

ある程度動作するようになってから provider は全部決め打ちで自分で作る必要ないよなと思い始めたので、結果的に以下のような感じになった。

require "bindan"
require "google/client/storage"
require "google/client/firestore"

# Define your configuration providers
providers = {
  env: Bindan::Provider::Envvar.new,
  storage: Bindan::Provider::Storage.new(bucket: "my-config-bucket"),
  firestore: Bindan::Provider::Firestore.new(collection: "app-settings")
}

# Configure Bindan
config = Bindan.configure(providers: providers) do |c, pr|
  c.database_host = pr.env["DATABASE_HOST"] || "localhost"
  c.api_key = pr.storage["api_key"]
  c.feature_flags = pr.firestore["feature_flags_document"]
end

※ 0.1.1 で特定の class を extend してクラスに設定を保存する書き方もできるようになった。これでよくあるツールっぽく使えるはず。

改めて、providers の部分は、

  • Hash[Symbol, untyped] を準備すればよい
  • 各 Provider は [] メソッドを用意すればよい

とした。

逆に、自分の利用したいツールのクライアントライブラリは自分で用意すること。標準で動くのは環境変数だけ、同梱されている provider は今のところ Google Cloud Storage と Firestore だけです。

今のところ自動で解決せずに自分で定義を書かないといけないので、別に Bindan 名前空間の下に置く必要もないし、自由にしてよい。自分の都合でデフォルトで用意してあるのは Google だけだけど、基本は取ってくる処理しか必要ないはずなので、自分で使ってる何かに対して簡単な wrapper を書けば簡単に追加できる。

おわりに

これで既存の設定ツールを全部置き換えてやるぜ、みたいなことは考えていなくて、

  • 生々しく API を叩くコードを書きたくない
  • できるだけ純粋な設定に関するコードは固まっていた方が見通しがよい

を達成できたのでいったん満足。実際に使い始めたらまた不満が出てくるかもしれない(特に secret は対応しておきたい)ので、チマチマ盆栽していくかなぁと思ってる。

今回なにがしんどかったかって、エミュレータ、docker container の制御だったと思う。マジで面倒くさかったし、けっきょく思ったような動作は実現できてないし、テストがすごく重いし、GitHub Actions では思った通りに動いてない。これも見直していかないとなぁ…。

  1. 逆に Node.js を使う機会が激減して現在はこのライブラリはほぼ死んでる状態 

More