Rails.app.credsの5つの意外な使い方

Rails 8.2は、ENVと暗号化ファイルからクレデンシャルにアクセスする統一された方法としてRails.app.credsを導入しました。しかし、実装の中にはもっと強力なものが隠されています:CombinedConfigurationは設定のための責任連鎖パターンです。各バックエンドは値を返すか、次に渡します。

これは単なるクレデンシャル検索ではありません。コンポーザブルな設定ミドルウェアです。

このアーキテクチャを活用する5つのパターンを紹介します。

1. サービス不要のフィーチャーフラグ

ほとんどのアプリはLaunchDarklyを必要としません。必要なのは、バージョン管理され、開発環境で上書き可能な機能トグルの方法です。

# config/credentials.yml.enc
features:
  new_checkout: false
  dark_mode: true
  ai_summaries: false

他のクレデンシャルと同様にアクセスします:

if Rails.app.creds.option(:features, :new_checkout, default: false)
  render_new_checkout
else
  render_legacy_checkout
end

魔法:ENVがクレデンシャルを上書きします。暗号化ファイルを触らずにローカルで機能を有効にできます:

FEATURES__NEW_CHECKOUT=true bin/rails server

ステージングで機能をテストする必要がありますか?デプロイ設定でENV変数を設定するだけです。コード変更なし、クレデンシャル編集なし、デプロイなし。

使いやすくする

ヘルパーでラップします:

# app/helpers/feature_flags_helper.rb
module FeatureFlagsHelper
  def feature?(name)
    Rails.app.creds.option(:features, name, default: false).to_s == "true"
  end
end

# 使用方法
<% if feature?(:new_checkout) %>
  <%= render "checkout/new" %>
<% end %>

これでフィーチャーフラグは:

  • バージョン管理されている(credentialsで)
  • 環境ごとに上書き可能(ENV経由)
  • 外部依存なし
  • 無料

2. 開発者オーバーライドファイル

すべてのチームがこの問題を抱えています:「コミットせずにローカルで異なるクレデンシャルでテストするにはどうすればいい?」

gitignoreされたローカルオーバーライドファイルを作成します:

# config/initializers/developer_overrides.rb
class DeveloperOverrides
  def initialize
    path = Rails.root.join(".secrets.local.yml")
    @config = path.exist? ? YAML.load_file(path).deep_symbolize_keys : {}
  end

  def require(*keys)
    option(*keys) || raise(KeyError, "見つかりません: #{keys.join('.')}")
  end

  def option(*keys, default: nil)
    value = keys.reduce(@config) { |h, k| h.is_a?(Hash) ? h[k] : nil }
    value.nil? ? default : value
  end

  def keys
    flatten_keys(@config)
  end

  def reload
    @config = YAML.load_file(Rails.root.join(".secrets.local.yml")).deep_symbolize_keys
  rescue Errno::ENOENT
    @config = {}
  end

  private

  def flatten_keys(hash, prefix = [])
    hash.flat_map do |k, v|
      v.is_a?(Hash) ? flatten_keys(v, prefix + [k]) : [prefix + [k]]
    end
  end
end

# 最高優先度で挿入
if Rails.env.development?
  Rails.app.creds = ActiveSupport::CombinedConfiguration.new(
    DeveloperOverrides.new,
    Rails.app.envs,
    Rails.app.credentials
  )
end

.gitignoreに追加:

.secrets.local.yml

これで開発者は自分のオーバーライドファイルを作成できます:

# .secrets.local.yml(gitignoreされている)
stripe:
  secret_key: sk_test_my_personal_test_key
openai:
  api_key: sk-my-own-openai-key
features:
  experimental_ui: true

「うっかりAPIキーをコミットしてしまった」インシデントはもうありません。「一時的にcredentials.yml.encを編集させて」もありません。各開発者が自分のサンドボックスを持ちます。

3. リクエストスコープ設定

管理者がリクエストごとに設定を上書きできたらどうでしょう?デバッグ、カスタマーサポート、A/Bテストに便利です。

# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
  attribute :config_overrides
end

# lib/request_scoped_configuration.rb
class RequestScopedConfiguration
  def require(*keys)
    option(*keys) # 見つからない場合はnilを返してチェーンを続行
  end

  def option(*keys, default: nil)
    Current.config_overrides&.dig(*keys)
  end

  def keys
    Current.config_overrides&.keys || []
  end

  def reload
    # No-op、リクエストごとにクリア
  end
end

接続します:

# config/initializers/credentials.rb
Rails.app.creds = ActiveSupport::CombinedConfiguration.new(
  RequestScopedConfiguration.new,
  Rails.app.envs,
  Rails.app.credentials
)

コントローラーで:

class Admin::DebugController < ApplicationController
  before_action :require_admin

  def impersonate_config
    # この管理セッションで一時的にStripeテストモードを使用
    Current.config_overrides = {
      stripe: { test_mode: true },
      features: { debug_panel: true }
    }
    redirect_back fallback_location: root_path, notice: "デバッグモードが有効になりました"
  end
end

アプリの残りの部分は知りもせず、気にもしません。Rails.app.creds.option(:stripe, :test_mode)を呼び出すだけで、現在のリクエストに適した値を取得できます。

4. 監査ログラッパー

コンプライアンス要件では、機密クレデンシャルへのアクセスのログ記録が求められることがよくあります。CombinedConfigurationを使えば、任意のバックエンドをログでラップできます:

# lib/audited_configuration.rb
class AuditedConfiguration
  def initialize(backend, logger: Rails.logger)
    @backend = backend
    @logger = logger
  end

  def require(*keys)
    log_access("require", keys)
    @backend.require(*keys)
  end

  def option(*keys, default: nil)
    log_access("option", keys)
    @backend.option(*keys, default: default)
  end

  def keys
    @backend.keys
  end

  def reload
    @backend.reload
  end

  private

  def log_access(method, keys)
    location = caller_locations(2, 1).first
    @logger.info({
      event: "credential_access",
      method: method,
      keys: keys.join("."),
      file: location.path,
      line: location.lineno,
      timestamp: Time.current.iso8601
    }.to_json)
  end
end

本番クレデンシャルをラップします:

# config/initializers/credentials.rb
if Rails.env.production?
  audited_creds = AuditedConfiguration.new(Rails.app.credentials)

  Rails.app.creds = ActiveSupport::CombinedConfiguration.new(
    Rails.app.envs,
    audited_creds
  )
end

すべてのクレデンシャルアクセスがログに記録されます:

{"event":"credential_access","method":"require","keys":"stripe.secret_key","file":"app/services/payment_service.rb","line":42,"timestamp":"2026-01-05T10:30:00Z"}

これをSIEMに送信したり、異常なアクセスパターンのアラートを設定したり、アクセス制御の証拠を求める監査人を満足させることができます。

5. スケジュール設定

設定が時間に基づいて変更できたらどうでしょう?ブラックフライデーの価格。ホリデーテーマ。計画されたメンテナンスウィンドウ。

# lib/scheduled_configuration.rb
class ScheduledConfiguration
  Schedule = Data.define(:range, :config)

  SCHEDULES = [
    Schedule.new(
      range: Time.zone.parse("2026-11-27")..Time.zone.parse("2026-12-01"),
      config: {
        pricing: { discount_percent: 30 },
        features: { sale_banner: true },
        cache: { ttl: 60 } # 高トラフィック時は短いキャッシュ
      }
    ),
    Schedule.new(
      range: Time.zone.parse("2026-12-24")..Time.zone.parse("2026-12-26"),
      config: {
        features: { holiday_theme: true },
        support: { auto_reply: true }
      }
    )
  ]

  def require(*keys)
    option(*keys) || raise(KeyError, "見つかりません: #{keys.join('.')}")
  end

  def option(*keys, default: nil)
    active = SCHEDULES.find { |s| s.range.cover?(Time.current) }
    return default unless active

    keys.reduce(active.config) { |h, k| h.is_a?(Hash) ? h[k] : nil }
  end

  def keys
    SCHEDULES.flat_map { |s| s.config.keys }.uniq
  end

  def reload
    # データベースや外部ソースからリロードも可能
  end
end

チェーンに接続します:

Rails.app.creds = ActiveSupport::CombinedConfiguration.new(
  ScheduledConfiguration.new,  # 時間ベースのオーバーライドを最初に
  Rails.app.envs,
  Rails.app.credentials
)

ブラックフライデーには、Rails.app.creds.option(:pricing, :discount_percent)が自動的に30を返します。デプロイ不要。スケジュールが終了すると、デフォルトにフォールバックします。

価格設定コードはブラックフライデーを知る必要がありません:

def apply_discount(price)
  discount = Rails.app.creds.option(:pricing, :discount_percent, default: 0)
  price * (1 - discount / 100.0)
end

パターン

5つの例すべてが同じ洞察を共有しています:CombinedConfigurationは設定のためのミドルウェアです。責任連鎖パターンにより:

  1. 既存のコードを変更せずに振る舞いをレイヤー化
  2. バックエンドの順序付けで機能を合成
  3. ストレージ、アクセス制御、ビジネスロジック間で関心を分離

順序が重要です:

ActiveSupport::CombinedConfiguration.new(
  RequestScopedConfiguration.new,  # 最高優先度:リクエストごとのオーバーライド
  ScheduledConfiguration.new,       # 時間ベースのルール
  DeveloperOverrides.new,           # ローカル開発
  Rails.app.envs,                   # 環境変数
  AuditedConfiguration.new(         # ログ用にラップ
    Rails.app.credentials
  )
)

最初の非nil値が勝ちます。各バックエンドはリクエストを処理するか、チェーンを下に渡すことができます。

まとめ

Rails.app.credsはシンプルな便利APIのように見えます。しかしCombinedConfigurationは洗練された設定システムの構築ブロックです:

  • 外部サービス不要のフィーチャーフラグ
  • クレデンシャルの競合なしの開発者サンドボックス
  • リクエストスコープのデバッグ
  • コンプライアンス対応の監査ログ
  • デプロイなしの時間ベース設定

最大のメリットは?アプリケーションコードがクリーンなまま。Rails.app.creds.option(:whatever)を呼び出すだけで、設定レイヤーが複雑さを処理します。

実装についてはPR #56404を参照してください。