Rails.app.creds 的 5 种意想不到的用法
Rails 8.2 引入了 Rails.app.creds 作为从 ENV 和加密文件访问凭证的统一方式。但隐藏在实现中的是更强大的东西:CombinedConfiguration 是配置的责任链模式。每个后端要么返回一个值,要么传递给下一个。
这不仅仅是凭证查找。这是可组合的配置中间件。
以下是利用这种架构的五种模式。
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 变量即可。无需代码更改,无需编辑凭证,无需部署。
使其更易用
用 helper 封装它:
# 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. 开发者覆盖文件
每个团队都有这个问题:“如何在本地使用不同的凭证进行测试而不提交它们?”
创建一个被 git 忽略的本地覆盖文件:
# 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(被 git 忽略)
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
# 无操作,每个请求清除
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
模式
所有五个示例都共享同一个洞察:CombinedConfiguration 是配置的中间件。责任链模式让你能够:
- 分层行为而不修改现有代码
- 通过排序后端来组合功能
- 在存储、访问控制和业务逻辑之间分离关注点
顺序很重要:
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 了解实现详情。