Rails.app.credsで50行のフィーチャーフラグシステムを構築
LaunchDarklyは必要ありません。Flipperも必要ありません。ほとんどのRailsアプリでは、バージョン管理され、開発環境で簡単にオーバーライドでき、別のサービスを管理する必要のないフィーチャーフラグが必要です。
Rails 8.2のRails.app.credsがまさにそれを提供します。50行未満のコードで完全なフィーチャーフラグシステムを構築できます。
コアとなる考え方
Rails.app.credsはまずENVをチェックし、次に暗号化されたクレデンシャルをチェックします。これは:
- フラグを
credentials.yml.encに保存(バージョン管理、PRでレビュー) - いつでも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.credsとCombinedConfigurationはフィーチャーフラグの完璧な基盤です:
- 暗号化されたクレデンシャルにデフォルトを保存(バージョン管理)
- ENVでオーバーライド(デプロイ不要)
- リクエストごとにオーバーライド(テスト、管理ツール)
- 必要に応じてパーセンテージロールアウトとユーザーターゲティングを追加
外部依存なし。APIレイテンシなし。月額料金なし。
Rails.app.credsの実装についてはPR #56404をご覧ください。