Rails.app.credsで50行のフィーチャーフラグシステムを構築

LaunchDarklyは必要ありません。Flipperも必要ありません。ほとんどのRailsアプリでは、バージョン管理され、開発環境で簡単にオーバーライドでき、別のサービスを管理する必要のないフィーチャーフラグが必要です。

Rails 8.2のRails.app.credsがまさにそれを提供します。50行未満のコードで完全なフィーチャーフラグシステムを構築できます。

コアとなる考え方

Rails.app.credsはまずENVをチェックし、次に暗号化されたクレデンシャルをチェックします。これは:

  1. フラグをcredentials.yml.encに保存(バージョン管理、PRでレビュー)
  2. いつでもENV変数でオーバーライド(テスト、デバッグ、段階的ロールアウト)

データベース不要。外部サービス不要。API呼び出し不要。

ステップ1:Credentialsにフラグを保存

bin/rails credentials:edit
# config/credentials.yml.enc
features:
  new_checkout: false
  dark_mode: false
  ai_summaries: false
  beta_dashboard: false

これらがデフォルト値です。暗号化され、バージョン管理され、変更にはPRが必要です。

ステップ2:フィーチャーフラグモジュールを作成

# app/models/concerns/feature_flags.rb
module FeatureFlags
  extend self

  def enabled?(flag_name)
    value = Rails.app.creds.option(:features, flag_name, default: false)
    ActiveModel::Type::Boolean.new.cast(value)
  end

  def disabled?(flag_name)
    !enabled?(flag_name)
  end

  def enable!(flag_name)
    Current.feature_overrides ||= {}
    Current.feature_overrides[flag_name] = true
  end

  def disable!(flag_name)
    Current.feature_overrides ||= {}
    Current.feature_overrides[flag_name] = false
  end

  def with(flag_name, value)
    old_value = Current.feature_overrides&.dig(flag_name)
    enable!(flag_name) if value
    disable!(flag_name) unless value
    yield
  ensure
    if old_value.nil?
      Current.feature_overrides&.delete(flag_name)
    else
      Current.feature_overrides[flag_name] = old_value
    end
  end
end

ステップ3:リクエストスコープのオーバーライドを追加

リクエストごとのフラグ制御(テスト、管理者オーバーライド)のためにCurrentを接続:

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

# lib/feature_override_configuration.rb
class FeatureOverrideConfiguration
  def require(*keys)
    option(*keys)
  end

  def option(*keys, default: nil)
    return default unless keys.first == :features && keys.length == 2
    Current.feature_overrides&.dig(keys.last)
  end

  def keys = []
  def reload = nil
end

クレデンシャルチェーンを更新:

# config/initializers/credentials.rb
Rails.app.creds = ActiveSupport::CombinedConfiguration.new(
  FeatureOverrideConfiguration.new,  # リクエストスコープが最初
  Rails.app.envs,                     # 次にENV
  Rails.app.credentials               # 次に暗号化ファイル
)

ステップ4:あらゆる場所で使用

コントローラーで

class CheckoutController < ApplicationController
  def show
    if FeatureFlags.enabled?(:new_checkout)
      render :new_checkout
    else
      render :legacy_checkout
    end
  end
end

ビューで

<% if FeatureFlags.enabled?(:dark_mode) %>
  <body class="dark">
<% else %>
  <body>
<% end %>

モデルで

class Order < ApplicationRecord
  def calculate_shipping
    if FeatureFlags.enabled?(:new_shipping_calculator)
      NewShippingCalculator.calculate(self)
    else
      legacy_shipping_calculation
    end
  end
end

テストで

class CheckoutTest < ActionDispatch::IntegrationTest
  test "新しいチェックアウトフロー" do
    FeatureFlags.with(:new_checkout, true) do
      get checkout_path
      assert_select ".new-checkout-form"
    end
  end

  test "レガシーチェックアウトフロー" do
    FeatureFlags.with(:new_checkout, false) do
      get checkout_path
      assert_select ".legacy-checkout-form"
    end
  end
end

フラグのオーバーライド

開発環境で

クレデンシャルを編集せずに任意のフラグをオーバーライド:

# 1つのフラグを有効化
FEATURES__NEW_CHECKOUT=true bin/rails server

# 複数を有効化
FEATURES__NEW_CHECKOUT=true FEATURES__DARK_MODE=true bin/rails server

ステージング/本番環境で

デプロイ設定でENV変数を設定:

# fly.toml, render.yaml, etc.
[env]
  FEATURES__BETA_DASHBOARD = "true"

リクエストごと(管理ツール)

class Admin::FeatureFlagsController < AdminController
  def toggle
    flag = params[:flag].to_sym

    if FeatureFlags.enabled?(flag)
      FeatureFlags.disable!(flag)
    else
      FeatureFlags.enable!(flag)
    end

    redirect_back fallback_location: admin_root_path
  end
end

応用:パーセンテージロールアウト

機能をユーザーの10%に有効にしたい?

# app/models/concerns/feature_flags.rb
module FeatureFlags
  def enabled_for?(flag_name, user)
    # 明示的に有効/無効が設定されているかまずチェック
    return enabled?(flag_name) if explicitly_set?(flag_name)

    # パーセンテージロールアウトをチェック
    percentage = Rails.app.creds.option(:features, :"#{flag_name}_percentage", default: nil)
    return enabled?(flag_name) unless percentage

    # ユーザーIDに基づく一貫したバケッティング
    bucket = Digest::MD5.hexdigest("#{flag_name}-#{user.id}").first(8).to_i(16) % 100
    bucket < percentage.to_i
  end

  private

  def explicitly_set?(flag_name)
    Current.feature_overrides&.key?(flag_name) ||
      ENV["FEATURES__#{flag_name.to_s.upcase}"].present?
  end
end

ロールアウトを設定:

# credentials.yml.enc
features:
  new_checkout: false
  new_checkout_percentage: 10  # ユーザーの10%

使用方法:

if FeatureFlags.enabled_for?(:new_checkout, current_user)
  # ユーザーの10%がこれを見る
end

同じユーザーは常に同じ結果を得ます(MD5ハッシュによる一貫したバケッティング)。

応用:ユーザーセグメントターゲティング

特定のユーザーセグメントに機能を有効化:

module FeatureFlags
  def enabled_for?(flag_name, user)
    return true if user_in_allowlist?(flag_name, user)
    return true if segment_enabled?(flag_name, user)
    return enabled_by_percentage?(flag_name, user)
  end

  private

  def user_in_allowlist?(flag_name, user)
    allowlist = Rails.app.creds.option(:features, :"#{flag_name}_users", default: [])
    allowlist.include?(user.id) || allowlist.include?(user.email)
  end

  def segment_enabled?(flag_name, user)
    segments = Rails.app.creds.option(:features, :"#{flag_name}_segments", default: [])
    segments.any? { |segment| user_in_segment?(user, segment) }
  end

  def user_in_segment?(user, segment)
    case segment.to_sym
    when :staff then user.staff?
    when :beta then user.beta_tester?
    when :premium then user.premium?
    else false
    end
  end
end
# credentials.yml.enc
features:
  new_checkout: false
  new_checkout_users:
    - 123  # ユーザーID
    - "[email protected]"  # またはメール
  new_checkout_segments:
    - staff
    - beta
  new_checkout_percentage: 5  # 他の全員の5%も追加

ロールアウト戦略:まずスタッフ、次にベータテスター、次に全員の5%、そして信頼度が上がるにつれてパーセンテージを増加。

完全な実装

すべてを1か所にまとめると:

# app/models/concerns/feature_flags.rb (46行)
module FeatureFlags
  extend self

  def enabled?(flag_name)
    cast_boolean(Rails.app.creds.option(:features, flag_name, default: false))
  end

  def disabled?(flag_name) = !enabled?(flag_name)

  def enabled_for?(flag_name, user)
    return true if allowlisted?(flag_name, user)
    return true if segment_match?(flag_name, user)
    return percentage_match?(flag_name, user) if rollout_percentage(flag_name)
    enabled?(flag_name)
  end

  def enable!(flag_name)
    (Current.feature_overrides ||= {})[flag_name] = true
  end

  def disable!(flag_name)
    (Current.feature_overrides ||= {})[flag_name] = false
  end

  def with(flag_name, value)
    old = Current.feature_overrides&.dig(flag_name)
    value ? enable!(flag_name) : disable!(flag_name)
    yield
  ensure
    old.nil? ? Current.feature_overrides&.delete(flag_name) : Current.feature_overrides[flag_name] = old
  end

  private

  def cast_boolean(value) = ActiveModel::Type::Boolean.new.cast(value)
  def creds_option(flag, suffix) = Rails.app.creds.option(:features, :"#{flag}#{suffix}", default: nil)
  def rollout_percentage(flag) = creds_option(flag, :_percentage)&.to_i
  def allowlist(flag) = creds_option(flag, :_users) || []
  def segments(flag) = creds_option(flag, :_segments) || []

  def allowlisted?(flag, user)
    list = allowlist(flag)
    list.include?(user.id) || list.include?(user.email)
  end

  def segment_match?(flag, user)
    segments(flag).any? { |s| user.try(:"#{s}?") }
  end

  def percentage_match?(flag, user)
    Digest::MD5.hexdigest("#{flag}-#{user.id}").first(8).to_i(16) % 100 < rollout_percentage(flag)
  end
end

SaaSソリューションに勝る理由

機能このアプローチSaaS
コスト無料$20-500/月
レイテンシゼロ(メモリ内)API呼び出し
バージョン管理あり(credentials)通常なし
コードレビューあり(PRs)場合による
オフライン動作ありなし
複雑さ中〜高
ユーザーターゲティング基本高度
アナリティクスDIY組み込み

ほとんどのアプリでは、リアルタイムのフラグ更新や複雑なターゲティングルールは必要ありません。管理が簡単で、レイテンシを追加せず、お金がかからないフラグが必要です。

本格的なフィーチャーフラグサービスを使うべき時

このアプローチには限界があります。以下が必要な場合は専用サービスを検討してください:

  • 非技術者がフラグを管理(プロダクトマネージャーなど)
  • 複雑なターゲティングルール(地域、デバイスタイプなど)
  • デプロイなしのリアルタイム更新
  • 組み込みの実験とA/Bテスト分析
  • 監査ログとコンプライアンス機能

しかし、ほとんどのRailsアプリでは?46行のコードとRails.app.credsで十分です。

まとめ

Rails 8.2のRails.app.credsCombinedConfigurationはフィーチャーフラグの完璧な基盤です:

  1. 暗号化されたクレデンシャルにデフォルトを保存(バージョン管理)
  2. ENVでオーバーライド(デプロイ不要)
  3. リクエストごとにオーバーライド(テスト、管理ツール)
  4. 必要に応じてパーセンテージロールアウトとユーザーターゲティングを追加

外部依存なし。APIレイテンシなし。月額料金なし。

Rails.app.credsの実装についてはPR #56404をご覧ください。