5 Façons Inattendues d'Utiliser Rails.app.creds

Rails 8.2 a introduit Rails.app.creds comme moyen unifié d’accéder aux credentials depuis ENV et les fichiers chiffrés. Mais caché dans l’implémentation se trouve quelque chose de plus puissant : CombinedConfiguration est une chaîne de responsabilité pour la configuration. Chaque backend retourne une valeur ou passe au suivant.

Ce n’est pas juste une recherche de credentials. C’est un middleware de configuration composable.

Voici cinq patterns qui exploitent cette architecture.

1. Feature Flags Sans Service

La plupart des apps n’ont pas besoin de LaunchDarkly. Vous avez besoin d’un moyen de basculer les fonctionnalités qui soit versionné et modifiable en développement.

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

Accédez-y comme n’importe quel credential :

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

La magie : ENV remplace les credentials. Activez une fonctionnalité localement sans toucher au fichier chiffré :

FEATURES__NEW_CHECKOUT=true bin/rails server

Besoin de tester une fonctionnalité en staging ? Définissez la variable ENV dans votre config de déploiement. Pas de changement de code, pas d’édition de credentials, pas de deploy.

Le rendre ergonomique

Enveloppez-le dans un 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

# Utilisation
<% if feature?(:new_checkout) %>
  <%= render "checkout/new" %>
<% end %>

Vous avez maintenant des feature flags qui sont :

  • Versionnés (dans credentials)
  • Modifiables par environnement (via ENV)
  • Sans dépendances externes
  • Gratuits

2. Fichiers de Surcharge pour Développeurs

Chaque équipe a ce problème : “Comment tester avec des credentials différents localement sans les commiter ?”

Créez un fichier local de surcharge ignoré par 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, "Manquant : #{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

# Insérer avec la plus haute priorité
if Rails.env.development?
  Rails.app.creds = ActiveSupport::CombinedConfiguration.new(
    DeveloperOverrides.new,
    Rails.app.envs,
    Rails.app.credentials
  )
end

Ajoutez à .gitignore :

.secrets.local.yml

Maintenant n’importe quel développeur peut créer son propre fichier de surcharge :

# .secrets.local.yml (ignoré par git)
stripe:
  secret_key: sk_test_ma_cle_de_test_personnelle
openai:
  api_key: sk-ma-propre-cle-openai
features:
  experimental_ui: true

Plus d’incidents “j’ai accidentellement commité ma clé API”. Plus de “laissez-moi éditer temporairement credentials.yml.enc”. Chaque développeur a son propre sandbox.

3. Configuration Scopée par Requête

Et si les admins pouvaient surcharger la configuration par requête ? Utile pour le debugging, le support client ou les tests 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) # Retourne toujours nil pour continuer la chaîne si non trouvé
  end

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

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

  def reload
    # No-op, nettoyé par requête
  end
end

Connectez-le :

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

Maintenant dans un contrôleur :

class Admin::DebugController < ApplicationController
  before_action :require_admin

  def impersonate_config
    # Utiliser temporairement le mode test Stripe pour cette session admin
    Current.config_overrides = {
      stripe: { test_mode: true },
      features: { debug_panel: true }
    }
    redirect_back fallback_location: root_path, notice: "Mode debug activé"
  end
end

Le reste de votre app ne sait pas et ne s’en soucie pas. Elle appelle juste Rails.app.creds.option(:stripe, :test_mode) et obtient la bonne valeur pour la requête actuelle.

4. Wrapper de Journal d’Audit

Les exigences de conformité mandatent souvent la journalisation des accès aux credentials sensibles. Avec CombinedConfiguration, vous pouvez envelopper n’importe quel backend avec du logging :

# 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

Enveloppez vos credentials de production :

# 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

Chaque accès aux credentials est maintenant journalisé :

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

Vous pouvez alimenter cela vers votre SIEM, configurer des alertes pour des patterns d’accès inhabituels, ou satisfaire les auditeurs qui veulent une preuve des contrôles d’accès.

5. Configuration Programmée

Et si la configuration pouvait changer selon le temps ? Prix du Black Friday. Thèmes de fêtes. Fenêtres de maintenance programmées.

# 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 } # Cache plus court pendant le trafic élevé
      }
    ),
    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, "Manquant : #{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
    # Pourrait recharger depuis la base de données ou une source externe
  end
end

Connectez-le à la chaîne :

Rails.app.creds = ActiveSupport::CombinedConfiguration.new(
  ScheduledConfiguration.new,  # Surcharges basées sur le temps d'abord
  Rails.app.envs,
  Rails.app.credentials
)

Le Black Friday, Rails.app.creds.option(:pricing, :discount_percent) retourne automatiquement 30. Pas de deploy nécessaire. Quand la période se termine, ça retombe sur la valeur par défaut.

Votre code de tarification n’a pas besoin de connaître le Black Friday :

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

Le Pattern

Les cinq exemples partagent la même insight : CombinedConfiguration est un middleware pour la configuration. Le pattern chaîne de responsabilité vous permet de :

  1. Superposer des comportements sans modifier le code existant
  2. Composer des fonctionnalités en ordonnant les backends
  3. Séparer les responsabilités entre stockage, contrôle d’accès et logique métier

L’ordre compte :

ActiveSupport::CombinedConfiguration.new(
  RequestScopedConfiguration.new,  # Plus haute priorité : surcharges par requête
  ScheduledConfiguration.new,       # Règles basées sur le temps
  DeveloperOverrides.new,           # Développement local
  Rails.app.envs,                   # Variables d'environnement
  AuditedConfiguration.new(         # Enveloppé pour le logging
    Rails.app.credentials
  )
)

La première valeur non-nil gagne. Chaque backend peut soit gérer la requête, soit la passer plus bas dans la chaîne.

Conclusion

Rails.app.creds ressemble à une simple API de commodité. Mais CombinedConfiguration est une brique de base pour des systèmes de configuration sophistiqués :

  • Feature flags sans services externes
  • Sandboxes développeur sans conflits de credentials
  • Debugging scopé par requête
  • Journal d’audit prêt pour la conformité
  • Configuration basée sur le temps sans deploys

Le meilleur ? Votre code applicatif reste propre. Il appelle juste Rails.app.creds.option(:whatever) et la couche de configuration gère la complexité.

Voir PR #56404 pour l’implémentation.