Overview
Configuration management in Ruby centers on organizing, storing, and accessing application settings through multiple approaches. Ruby provides several mechanisms for configuration: constants for immutable values, class and instance variables for runtime modification, environment variables for deployment flexibility, and external files for complex structured data.
The core Ruby approach uses constants defined at the module or class level for configuration that remains unchanged during execution. Class variables (@@variable
) and class instance variables (@variable
defined at class level) handle configuration that changes at runtime. Environment variables accessed through ENV
provide deployment-specific configuration, while file-based approaches using YAML, JSON, or Ruby files handle complex hierarchical settings.
# Constants for immutable configuration
module Config
DATABASE_TIMEOUT = 30
MAX_RETRIES = 3
end
# Class variables for runtime configuration
class ApiClient
@@base_url = 'https://api.example.com'
@@timeout = 10
def self.configure(base_url: nil, timeout: nil)
@@base_url = base_url if base_url
@@timeout = timeout if timeout
end
end
# Environment variable configuration
database_url = ENV['DATABASE_URL'] || 'postgres://localhost/app'
debug_mode = ENV['DEBUG'] == 'true'
Ruby's configuration patterns support both compile-time and runtime modification. Constants provide immutable configuration loaded when the class loads, while class variables and methods enable dynamic reconfiguration. Environment variables bridge the gap between application code and deployment environments, following the twelve-factor app methodology.
File-based configuration typically uses YAML for human-readable structured data, JSON for programmatic configuration, or Ruby files for executable configuration with conditional logic. Each approach offers different trade-offs between readability, flexibility, and security.
Basic Usage
Ruby configuration management begins with choosing the appropriate storage mechanism based on when configuration changes and who controls the values. Constants work for values determined at application design time, class variables for values that change during application runtime, and environment variables for values that change between deployments.
Constants provide the simplest configuration approach. Define constants in modules to create namespaced configuration accessible throughout the application:
module DatabaseConfig
HOST = 'localhost'
PORT = 5432
POOL_SIZE = 20
TIMEOUT = 30
end
# Access configuration
connection = Database.connect(
host: DatabaseConfig::HOST,
port: DatabaseConfig::PORT,
pool_size: DatabaseConfig::POOL_SIZE
)
Class variables enable runtime configuration changes. Use class methods to modify configuration and instance methods to access current values:
class Logger
@@level = :info
@@output = STDOUT
def self.configure
yield(self) if block_given?
end
def self.level=(new_level)
@@level = new_level
end
def self.level
@@level
end
def self.output=(new_output)
@@output = new_output
end
def log(message, level = :info)
return unless should_log?(level)
@@output.puts "[#{level.upcase}] #{message}"
end
private
def should_log?(level)
level_priority(level) >= level_priority(@@level)
end
def level_priority(level)
{ debug: 0, info: 1, warn: 2, error: 3 }[level] || 1
end
end
# Configure at runtime
Logger.configure do |config|
config.level = :debug
config.output = File.open('app.log', 'a')
end
Environment variables provide deployment-specific configuration without modifying code. Ruby accesses environment variables through the ENV
hash, with common patterns for default values and type conversion:
class AppConfig
def self.database_url
ENV['DATABASE_URL'] || 'postgres://localhost/development'
end
def self.port
(ENV['PORT'] || '3000').to_i
end
def self.debug_mode?
ENV['DEBUG'].to_s.downcase == 'true'
end
def self.worker_count
ENV['WORKER_COUNT']&.to_i || processor_count
end
private
def self.processor_count
`nproc`.to_i
rescue
4
end
end
# Usage
server = WebServer.new(port: AppConfig.port)
server.enable_debug if AppConfig.debug_mode?
File-based configuration handles complex structured data. YAML provides human-readable configuration with Ruby's built-in YAML
module:
require 'yaml'
class FileConfig
def self.load(file_path)
@config = YAML.load_file(file_path)
rescue Errno::ENOENT
raise "Configuration file not found: #{file_path}"
rescue Psych::SyntaxError => e
raise "Invalid YAML in configuration file: #{e.message}"
end
def self.get(key_path)
keys = key_path.split('.')
keys.reduce(@config) { |config, key| config&.dig(key.to_s) }
end
def self.[](key)
@config[key.to_s]
end
end
# config/application.yml
# database:
# host: localhost
# port: 5432
# pool_size: 20
# logging:
# level: info
# file: log/application.log
FileConfig.load('config/application.yml')
db_host = FileConfig.get('database.host')
log_level = FileConfig.get('logging.level')
Advanced Usage
Advanced configuration management in Ruby involves dynamic configuration loading, nested configuration hierarchies, configuration inheritance, and runtime reconfiguration with validation. These patterns handle complex applications with multiple environments, feature flags, and configuration dependencies.
Dynamic configuration loading creates configuration objects that respond to method calls with configuration values. This pattern uses method_missing
to provide dot notation access to nested configuration:
class DynamicConfig
def initialize(data = {})
@data = data.transform_keys(&:to_s)
end
def self.load_from_file(file_path)
data = YAML.load_file(file_path)
new(data)
end
def method_missing(method_name, *args, &block)
key = method_name.to_s
if key.end_with?('=')
# Setter method
actual_key = key.chop
@data[actual_key] = args.first
elsif @data.key?(key)
value = @data[key]
# Return nested DynamicConfig for Hash values
value.is_a?(Hash) ? DynamicConfig.new(value) : value
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
key = method_name.to_s
key.end_with?('=') || @data.key?(key.to_s) || super
end
def to_h
@data.dup
end
def merge(other_config)
merged_data = deep_merge(@data, other_config.to_h)
DynamicConfig.new(merged_data)
end
private
def deep_merge(hash1, hash2)
hash1.merge(hash2) do |key, old_val, new_val|
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
deep_merge(old_val, new_val)
else
new_val
end
end
end
end
# Usage with nested access
config = DynamicConfig.load_from_file('config/app.yml')
puts config.database.host
puts config.redis.timeout
config.feature_flags.new_ui = true
Configuration inheritance handles different environments and overrides through a hierarchy system. Base configuration provides defaults while environment-specific configuration overrides specific values:
class EnvironmentConfig
def initialize(base_config = {})
@base_config = base_config
@environment_configs = {}
@current_environment = ENV['RAILS_ENV'] || 'development'
end
def load_environment(env_name, config_data)
@environment_configs[env_name.to_s] = config_data
end
def load_from_directory(config_dir)
base_file = File.join(config_dir, 'base.yml')
@base_config = YAML.load_file(base_file) if File.exist?(base_file)
Dir.glob(File.join(config_dir, '*.yml')).each do |file|
next if File.basename(file, '.yml') == 'base'
env_name = File.basename(file, '.yml')
env_config = YAML.load_file(file)
load_environment(env_name, env_config)
end
end
def current_config
@current_config ||= build_config_for(@current_environment)
end
def config_for(environment)
build_config_for(environment.to_s)
end
def method_missing(method_name, *args, &block)
current_config.send(method_name, *args, &block)
end
private
def build_config_for(environment)
env_config = @environment_configs[environment] || {}
merged_config = deep_merge(@base_config, env_config)
DynamicConfig.new(merged_config)
end
def deep_merge(hash1, hash2)
hash1.merge(hash2) do |key, old_val, new_val|
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
deep_merge(old_val, new_val)
else
new_val
end
end
end
end
# Directory structure:
# config/
# base.yml
# development.yml
# staging.yml
# production.yml
config = EnvironmentConfig.new
config.load_from_directory('config')
puts config.database.host # Uses current environment
puts config.config_for('production').database.host # Specific environment
Runtime configuration validation ensures configuration values meet application requirements. This pattern validates configuration on access and provides helpful error messages:
class ValidatedConfig
ConfigError = Class.new(StandardError)
def initialize(data = {})
@data = data
@validators = {}
@required_keys = Set.new
end
def validate(key, &validator_block)
@validators[key.to_s] = validator_block
self
end
def require_key(key)
@required_keys.add(key.to_s)
self
end
def get(key)
key_str = key.to_s
# Check if required key exists
if @required_keys.include?(key_str) && !@data.key?(key_str)
raise ConfigError, "Required configuration key missing: #{key}"
end
value = @data[key_str]
# Validate value if validator exists
if @validators.key?(key_str)
validator = @validators[key_str]
begin
validator.call(value)
rescue StandardError => e
raise ConfigError, "Configuration validation failed for #{key}: #{e.message}"
end
end
value
end
def []=(key, value)
@data[key.to_s] = value
end
def validate_all
@required_keys.each { |key| get(key) }
@data.keys.each do |key|
get(key) if @validators.key?(key)
end
true
end
end
# Configuration with validation
config = ValidatedConfig.new
config.require_key('database_url')
.validate('database_url') { |url| raise 'Invalid URL' unless url =~ /\A\w+:\/\// }
config.require_key('port')
.validate('port') { |port| raise 'Port must be 1-65535' unless (1..65535).include?(port) }
config.validate('timeout') { |t| raise 'Timeout must be positive' unless t > 0 }
config['database_url'] = 'postgres://localhost/app'
config['port'] = 3000
config['timeout'] = 30
config.validate_all # Validates all configuration
database_url = config.get('database_url') # Validates on access
Error Handling & Debugging
Configuration management error handling focuses on missing configuration files, invalid values, type conversion failures, and environment variable parsing problems. Ruby configuration systems must handle these errors gracefully while providing useful debugging information for developers and operators.
Missing configuration files represent the most common configuration error. Applications should provide clear error messages and fallback behavior when configuration files cannot be loaded:
class ConfigLoader
ConfigurationError = Class.new(StandardError)
FileNotFoundError = Class.new(ConfigurationError)
ParseError = Class.new(ConfigurationError)
def self.load_yaml(file_path, required: true)
YAML.load_file(file_path)
rescue Errno::ENOENT
if required
raise FileNotFoundError, "Required configuration file not found: #{file_path}"
else
puts "Warning: Optional configuration file not found: #{file_path}"
{}
end
rescue Psych::SyntaxError => e
raise ParseError, "YAML parsing error in #{file_path}: #{e.message}"
rescue StandardError => e
raise ConfigurationError, "Failed to load configuration from #{file_path}: #{e.message}"
end
def self.load_with_fallback(primary_path, fallback_path = nil)
load_yaml(primary_path, required: false).tap do |config|
if config.empty? && fallback_path
return load_yaml(fallback_path, required: true)
elsif config.empty?
raise FileNotFoundError, "No configuration file found at #{primary_path}"
end
end
end
def self.load_multiple(file_paths)
configs = []
errors = []
file_paths.each do |path|
begin
configs << load_yaml(path, required: false)
rescue ConfigurationError => e
errors << e
end
end
if configs.all?(&:empty?)
error_messages = errors.map(&:message).join('; ')
raise FileNotFoundError, "No valid configuration files found. Errors: #{error_messages}"
end
configs.reduce({}) { |merged, config| merged.merge(config) }
end
end
# Usage with error handling
begin
config = ConfigLoader.load_with_fallback(
"config/#{ENV['RAILS_ENV']}.yml",
'config/development.yml'
)
rescue ConfigLoader::FileNotFoundError => e
puts "Configuration error: #{e.message}"
puts "Create a configuration file or set default values"
exit 1
rescue ConfigLoader::ParseError => e
puts "Configuration parsing error: #{e.message}"
puts "Check YAML syntax in configuration file"
exit 1
end
Environment variable parsing requires careful type conversion and validation. Environment variables always arrive as strings, requiring conversion to appropriate Ruby types with error handling:
class EnvConfig
ConversionError = Class.new(StandardError)
def self.string(key, default: nil)
ENV[key] || default
end
def self.integer(key, default: nil)
value = ENV[key]
return default unless value
Integer(value)
rescue ArgumentError
raise ConversionError, "Cannot convert ENV['#{key}'] = '#{value}' to integer"
end
def self.float(key, default: nil)
value = ENV[key]
return default unless value
Float(value)
rescue ArgumentError
raise ConversionError, "Cannot convert ENV['#{key}'] = '#{value}' to float"
end
def self.boolean(key, default: nil)
value = ENV[key]
return default unless value
case value.downcase
when 'true', '1', 'yes', 'on'
true
when 'false', '0', 'no', 'off'
false
else
raise ConversionError, "Cannot convert ENV['#{key}'] = '#{value}' to boolean. Use true/false, 1/0, yes/no, or on/off"
end
end
def self.array(key, delimiter: ',', default: nil)
value = ENV[key]
return default unless value
value.split(delimiter).map(&:strip)
end
def self.required(key, type: :string)
value = send(type, key)
return value unless value.nil?
raise ConversionError, "Required environment variable ENV['#{key}'] is not set"
end
def self.validate_presence(*keys)
missing_keys = keys.select { |key| ENV[key].nil? || ENV[key].empty? }
return if missing_keys.empty?
raise ConversionError, "Missing required environment variables: #{missing_keys.join(', ')}"
end
end
# Usage with comprehensive error handling
begin
EnvConfig.validate_presence('DATABASE_URL', 'SECRET_KEY')
config = {
database_url: EnvConfig.required('DATABASE_URL'),
port: EnvConfig.integer('PORT', default: 3000),
debug: EnvConfig.boolean('DEBUG', default: false),
worker_count: EnvConfig.integer('WORKERS', default: 2),
allowed_hosts: EnvConfig.array('ALLOWED_HOSTS', default: ['localhost'])
}
rescue EnvConfig::ConversionError => e
puts "Environment configuration error: #{e.message}"
puts "Check your environment variable settings"
exit 1
end
Configuration debugging requires visibility into configuration loading, merging, and resolution. A debugging-enabled configuration class provides detailed information about configuration sources and values:
class DebuggableConfig
def initialize(debug: false)
@debug = debug
@config_sources = {}
@access_log = []
@final_config = {}
end
def load_from_source(source_name, data)
log_debug "Loading configuration from #{source_name}"
log_debug "Data: #{data.inspect}"
@config_sources[source_name] = data
rebuild_final_config
end
def get(key)
value = @final_config[key.to_s]
source = find_value_source(key.to_s)
@access_log << {
key: key,
value: value,
source: source,
timestamp: Time.now
}
log_debug "Accessing #{key} = #{value.inspect} (from #{source})"
value
end
def debug_info(key = nil)
if key
key_info(key.to_s)
else
full_debug_info
end
end
def access_history
@access_log.dup
end
private
def rebuild_final_config
@final_config = {}
@config_sources.each do |source_name, data|
log_debug "Merging #{source_name}: #{data.keys.join(', ')}"
@final_config.merge!(data) do |key, old_val, new_val|
log_debug "Override: #{key} #{old_val.inspect} -> #{new_val.inspect}"
new_val
end
end
log_debug "Final config keys: #{@final_config.keys.join(', ')}"
end
def find_value_source(key)
@config_sources.reverse_each do |source_name, data|
return source_name if data.key?(key)
end
'unknown'
end
def key_info(key)
{
key: key,
current_value: @final_config[key],
source: find_value_source(key),
all_sources: @config_sources.select { |_, data| data.key?(key) },
access_count: @access_log.count { |entry| entry[:key].to_s == key }
}
end
def full_debug_info
{
sources: @config_sources.keys,
final_keys: @final_config.keys,
total_accesses: @access_log.length,
most_accessed: most_accessed_keys
}
end
def most_accessed_keys
@access_log
.group_by { |entry| entry[:key] }
.transform_values(&:length)
.sort_by { |_, count| -count }
.first(5)
.to_h
end
def log_debug(message)
puts "[CONFIG DEBUG] #{message}" if @debug
end
end
# Usage for debugging configuration issues
config = DebuggableConfig.new(debug: true)
config.load_from_source('defaults', { 'host' => 'localhost', 'port' => 3000 })
config.load_from_source('environment', { 'host' => 'production.com' })
config.load_from_source('override', { 'timeout' => 30 })
host = config.get('host') # Shows resolution process
puts config.debug_info('host') # Shows detailed info for specific key
puts config.debug_info # Shows overall configuration statistics
Production Patterns
Production configuration management requires patterns for secure storage, environment separation, configuration validation, monitoring, and deployment coordination. Ruby applications in production environments need robust configuration systems that handle secrets securely, support multiple deployment environments, and provide observability into configuration state.
Secret management separates sensitive configuration from regular settings, using encryption, external secret stores, and environment variable injection to protect credentials and API keys:
require 'base64'
require 'openssl'
class SecureConfig
SecretNotFound = Class.new(StandardError)
DecryptionError = Class.new(StandardError)
def initialize(encryption_key: nil)
@encryption_key = encryption_key || derive_key_from_env
@secrets = {}
@regular_config = {}
end
def load_encrypted_secrets(file_path)
encrypted_data = File.read(file_path)
decrypted_data = decrypt(encrypted_data)
@secrets = YAML.load(decrypted_data)
rescue Errno::ENOENT
raise SecretNotFound, "Encrypted secrets file not found: #{file_path}"
rescue OpenSSL::Cipher::CipherError
raise DecryptionError, "Failed to decrypt secrets file - check encryption key"
end
def load_regular_config(file_path)
@regular_config = YAML.load_file(file_path)
end
def secret(key)
@secrets[key.to_s] || raise(SecretNotFound, "Secret not found: #{key}")
end
def config(key)
@regular_config[key.to_s]
end
def database_url
# Construct URL from separate secret components
host = secret('database_host')
user = secret('database_user')
password = secret('database_password')
database = config('database_name')
"postgres://#{user}:#{password}@#{host}/#{database}"
end
def redis_url
host = secret('redis_host')
port = config('redis_port') || 6379
password = secret('redis_password')
"redis://:#{password}@#{host}:#{port}"
end
def self.encrypt_secrets(secrets_hash, encryption_key)
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.encrypt
cipher.key = encryption_key
iv = cipher.random_iv
encrypted = cipher.update(secrets_hash.to_yaml)
encrypted << cipher.final
auth_tag = cipher.auth_tag
# Combine IV + auth_tag + encrypted data and base64 encode
combined = iv + auth_tag + encrypted
Base64.strict_encode64(combined)
end
private
def decrypt(encrypted_data)
combined = Base64.strict_decode64(encrypted_data)
# Extract IV (16 bytes), auth tag (16 bytes), and encrypted data
iv = combined[0, 16]
auth_tag = combined[16, 16]
encrypted = combined[32..-1]
cipher = OpenSSL::Cipher.new('AES-256-GCM')
cipher.decrypt
cipher.key = @encryption_key
cipher.iv = iv
cipher.auth_tag = auth_tag
decrypted = cipher.update(encrypted)
decrypted << cipher.final
decrypted
end
def derive_key_from_env
key_material = ENV['CONFIG_ENCRYPTION_KEY']
raise SecretNotFound, 'CONFIG_ENCRYPTION_KEY environment variable required' unless key_material
# Derive consistent 32-byte key from environment variable
OpenSSL::PBKDF2.pbkdf2_hmac(
key_material,
'ruby-config-salt',
10000,
32,
OpenSSL::Digest::SHA256.new
)
end
end
# Usage in production
config = SecureConfig.new
config.load_regular_config('config/production.yml')
config.load_encrypted_secrets('config/secrets.yml.enc')
# Database connection with secrets
ActiveRecord::Base.establish_connection(config.database_url)
# Redis connection with secrets
Redis.new(url: config.redis_url)
Configuration validation in production environments requires comprehensive checks at application startup, with clear error reporting for operators and deployment systems:
class ProductionConfigValidator
ValidationError = Class.new(StandardError)
def initialize
@validations = []
@warnings = []
@errors = []
end
def validate_required_env(*keys)
@validations << proc do
missing = keys.select { |key| ENV[key].nil? || ENV[key].empty? }
unless missing.empty?
@errors << "Missing required environment variables: #{missing.join(', ')}"
end
end
end
def validate_database_connection(database_url)
@validations << proc do
begin
# Simplified connection test
uri = URI.parse(database_url)
@errors << "Invalid database URL scheme: #{uri.scheme}" unless %w[postgres postgresql mysql2].include?(uri.scheme)
@errors << "Database URL missing host" if uri.host.nil? || uri.host.empty?
@errors << "Database URL missing database name" if uri.path.nil? || uri.path.length <= 1
rescue URI::InvalidURIError
@errors << "Invalid database URL format: #{database_url}"
end
end
end
def validate_port_range(port, name = 'port')
@validations << proc do
unless (1..65535).include?(port.to_i)
@errors << "#{name.capitalize} must be between 1 and 65535, got: #{port}"
end
end
end
def validate_file_accessible(file_path, required: true)
@validations << proc do
unless File.readable?(file_path)
message = "Cannot read file: #{file_path}"
if required
@errors << message
else
@warnings << message
end
end
end
end
def validate_directory_writable(dir_path)
@validations << proc do
unless Dir.exist?(dir_path) && File.writable?(dir_path)
@errors << "Directory not writable: #{dir_path}"
end
end
end
def validate_memory_limits
@validations << proc do
available_memory = `free -m | awk 'NR==2{print $7}'`.to_i rescue 0
if available_memory > 0 && available_memory < 512
@warnings << "Low available memory: #{available_memory}MB"
end
end
end
def run_validations
@errors.clear
@warnings.clear
@validations.each(&:call)
unless @warnings.empty?
puts "Configuration warnings:"
@warnings.each { |warning| puts " - #{warning}" }
puts
end
unless @errors.empty?
error_message = "Configuration validation failed:\n" +
@errors.map { |error| " - #{error}" }.join("\n")
raise ValidationError, error_message
end
puts "Configuration validation passed"
end
def validation_report
{
passed: @errors.empty?,
errors: @errors.dup,
warnings: @warnings.dup,
checks_run: @validations.length
}
end
end
# Production validation setup
validator = ProductionConfigValidator.new
validator.validate_required_env(
'DATABASE_URL', 'REDIS_URL', 'SECRET_KEY_BASE',
'RAILS_ENV', 'LOG_LEVEL'
)
validator.validate_database_connection(ENV['DATABASE_URL'])
validator.validate_port_range(ENV['PORT'] || '3000', 'web server port')
validator.validate_file_accessible('config/application.yml')
validator.validate_directory_writable('log')
validator.validate_directory_writable('tmp')
validator.validate_memory_limits
begin
validator.run_validations
rescue ProductionConfigValidator::ValidationError => e
puts e.message
exit 1
end
Configuration monitoring provides observability into configuration changes, access patterns, and runtime behavior. This pattern tracks configuration usage and exposes metrics for monitoring systems:
class MonitoredConfig
def initialize
@config = {}
@access_count = Hash.new(0)
@last_access = {}
@change_log = []
@start_time = Time.now
end
def load(source, data)
timestamp = Time.now
data.each do |key, value|
old_value = @config[key]
@config[key] = value
if old_value != value
@change_log << {
key: key,
old_value: old_value,
new_value: value,
source: source,
timestamp: timestamp
}
end
end
end
def get(key)
@access_count[key] += 1
@last_access[key] = Time.now
@config[key]
end
def metrics
uptime = Time.now - @start_time
{
uptime_seconds: uptime.to_i,
total_keys: @config.keys.length,
total_accesses: @access_count.values.sum,
most_accessed: @access_count.sort_by { |_, count| -count }.first(5).to_h,
recent_changes: @change_log.last(10),
unused_keys: find_unused_keys,
memory_usage: calculate_memory_usage
}
end
def health_check
{
status: 'healthy',
checks: {
config_loaded: !@config.empty?,
recent_access: @last_access.values.any? { |time| Time.now - time < 300 },
no_recent_errors: @change_log.none? { |entry| entry[:error] }
}
}
end
def export_for_monitoring
# Format suitable for Prometheus/monitoring systems
{
'config_keys_total' => @config.keys.length,
'config_accesses_total' => @access_count.values.sum,
'config_changes_total' => @change_log.length,
'config_unused_keys' => find_unused_keys.length,
'config_memory_bytes' => calculate_memory_usage
}
end
private
def find_unused_keys
cutoff_time = Time.now - 3600 # 1 hour ago
@config.keys.select do |key|
@last_access[key].nil? || @last_access[key] < cutoff_time
end
end
def calculate_memory_usage
# Rough estimation of memory usage
@config.to_s.bytesize + @change_log.to_s.bytesize
end
end
# Integration with monitoring endpoint
class ConfigMonitoringEndpoint
def initialize(monitored_config)
@config = monitored_config
end
def metrics_endpoint
# Returns Prometheus-style metrics
metrics = @config.export_for_monitoring
output = []
metrics.each do |metric_name, value|
output << "# TYPE #{metric_name} gauge"
output << "#{metric_name} #{value}"
end
output.join("\n")
end
def health_endpoint
@config.health_check.to_json
end
def debug_endpoint
@config.metrics.to_json
end
end
Common Pitfalls
Configuration management in Ruby contains several subtle issues that commonly cause production problems. These pitfalls involve constant modification attempts, class variable inheritance behavior, environment variable type assumptions, configuration loading timing, and security vulnerabilities from improper handling of sensitive data.
Constant modification represents a frequent mistake where developers attempt to change constants after definition. Ruby allows constant modification but issues warnings, leading to confusing behavior in applications:
# PROBLEMATIC: Attempting to modify constants
module BadConfig
API_ENDPOINT = 'https://dev-api.example.com'
end
# Later in the application...
if Rails.env.production?
# This generates a warning and causes confusion
BadConfig::API_ENDPOINT = 'https://api.example.com' # warning: already initialized constant
end
# BETTER: Use class variables or methods for runtime configuration
class ProperConfig
@@api_endpoint = 'https://dev-api.example.com'
def self.api_endpoint
@@api_endpoint
end
def self.configure_for_environment(env)
@@api_endpoint = case env
when 'production'
'https://api.example.com'
when 'staging'
'https://staging-api.example.com'
else
'https://dev-api.example.com'
end
end
end
# BEST: Use environment-specific configuration from the start
class EnvironmentConfig
ENDPOINTS = {
'development' => 'https://dev-api.example.com',
'staging' => 'https://staging-api.example.com',
'production' => 'https://api.example.com'
}.freeze
def self.api_endpoint
ENDPOINTS[Rails.env] || ENDPOINTS['development']
end
end
Class variable inheritance creates unexpected behavior when subclasses modify class variables, affecting all classes in the inheritance hierarchy. This behavior often surprises developers expecting isolated configuration:
# PROBLEMATIC: Class variables shared across inheritance hierarchy
class BaseService
@@timeout = 30
@@retries = 3
def self.configure(timeout: nil, retries: nil)
@@timeout = timeout if timeout
@@retries = retries if retries
end
def self.timeout
@@timeout
end
def self.retries
@@retries
end
end
class EmailService < BaseService
end
class PaymentService < BaseService
end
# This configuration affects ALL services
EmailService.configure(timeout: 60)
puts BaseService.timeout # => 60 (unexpected!)
puts PaymentService.timeout # => 60 (unexpected!)
# BETTER: Use class instance variables for isolated configuration
class BetterBaseService
@timeout = 30
@retries = 3
def self.configure(timeout: nil, retries: nil)
@timeout = timeout if timeout
@retries = retries if retries
end
def self.timeout
@timeout
end
def self.retries
@retries
end
end
class BetterEmailService < BetterBaseService
@timeout = 60 # Isolated configuration
@retries = 5
end
class BetterPaymentService < BetterBaseService
@timeout = 120 # Different isolated configuration
@retries = 1
end
puts BetterEmailService.timeout # => 60
puts BetterPaymentService.timeout # => 120
puts BetterBaseService.timeout # => 30 (unchanged)
Environment variable type conversion errors occur when developers assume environment variables contain non-string data or when conversion fails silently:
# PROBLEMATIC: Assuming environment variables are non-strings
class ProblematicConfig
def self.port
ENV['PORT'] || 3000 # Returns string "3000" when ENV['PORT'] is nil
end
def self.enabled?
ENV['FEATURE_ENABLED'] # Returns string, not boolean
end
def self.worker_count
ENV['WORKERS'] * 2 # String repetition, not multiplication
end
end
# This causes subtle bugs
puts ProblematicConfig.port.class # => String (should be Integer)
puts ProblematicConfig.enabled? # => "false" (truthy in Ruby!)
# BETTER: Explicit type conversion with defaults
class ReliableConfig
def self.port
(ENV['PORT'] || '3000').to_i
end
def self.enabled?
case ENV['FEATURE_ENABLED']&.downcase
when 'true', '1', 'yes'
true
when 'false', '0', 'no', nil
false
else
raise ArgumentError, "Invalid boolean value for FEATURE_ENABLED: #{ENV['FEATURE_ENABLED']}"
end
end
def self.worker_count
workers = ENV['WORKERS']
return 2 unless workers # Default value
count = Integer(workers)
raise ArgumentError, "Worker count must be positive, got: #{count}" if count <= 0
count
rescue ArgumentError => e
raise ArgumentError, "Invalid worker count in WORKERS environment variable: #{e.message}"
end
def self.timeout_seconds
timeout = ENV['TIMEOUT_SECONDS']
return 30.0 unless timeout
Float(timeout).tap do |value|
raise ArgumentError, "Timeout must be positive, got: #{value}" if value <= 0
end
rescue ArgumentError => e
raise ArgumentError, "Invalid timeout in TIMEOUT_SECONDS environment variable: #{e.message}"
end
end
Configuration loading timing issues arise when configuration depends on other components that may not be initialized yet, or when configuration changes after components have already used the values:
# PROBLEMATIC: Configuration loaded before dependencies are ready
class ProblematicApp
# This might fail if database isn't ready yet
CONFIG = YAML.load_file('config/database.yml')
def self.start
database = Database.connect(CONFIG['database'])
# Configuration already loaded, can't change based on runtime conditions
end
end
# BETTER: Lazy configuration loading
class ReliableApp
def self.config
@config ||= load_configuration
end
def self.start
# Configuration loaded when first accessed
database = Database.connect(config['database'])
end
private
def self.load_configuration
config_file = determine_config_file
YAML.load_file(config_file)
rescue Errno::ENOENT => e
puts "Configuration file not found: #{e.message}"
puts "Using default configuration"
default_configuration
end
def self.determine_config_file
if File.exist?('config/local.yml')
'config/local.yml' # Development override
elsif ENV['CONFIG_FILE']
ENV['CONFIG_FILE'] # Explicit configuration
else
"config/#{ENV['RAILS_ENV'] || 'development'}.yml"
end
end
def self.default_configuration
{
'database' => {
'host' => 'localhost',
'port' => 5432,
'name' => 'app_development'
}
}
end
end
# BEST: Configuration with initialization hooks
class ConfigurableApp
def self.configure
yield(configuration) if block_given?
end
def self.configuration
@configuration ||= Configuration.new
end
def self.start
# Validate configuration before starting
configuration.validate!
database = Database.connect(configuration.database_options)
end
class Configuration
attr_accessor :database_host, :database_port, :database_name
def initialize
@database_host = ENV['DATABASE_HOST'] || 'localhost'
@database_port = (ENV['DATABASE_PORT'] || '5432').to_i
@database_name = ENV['DATABASE_NAME'] || 'app_development'
end
def database_options
{
host: @database_host,
port: @database_port,
database: @database_name
}
end
def validate!
raise ArgumentError, "Database host cannot be empty" if @database_host.nil? || @database_host.empty?
raise ArgumentError, "Database port must be positive" if @database_port <= 0
raise ArgumentError, "Database name cannot be empty" if @database_name.nil? || @database_name.empty?
end
end
end
# Usage with proper initialization
ConfigurableApp.configure do |config|
config.database_host = 'production-db.example.com'
config.database_port = 5433
end
ConfigurableApp.start
Security vulnerabilities in configuration management occur when sensitive data appears in logs, version control, or error messages, or when configuration files have incorrect permissions:
# PROBLEMATIC: Exposing secrets in logs and errors
class InsecureConfig
def initialize
@database_password = ENV['DATABASE_PASSWORD']
@api_key = ENV['API_KEY']
puts "Loaded configuration: #{inspect}" # Exposes secrets in logs
end
def inspect
# This exposes sensitive data
"#<InsecureConfig @database_password=#{@database_password} @api_key=#{@api_key}>"
end
def database_url
"postgres://user:#{@database_password}@host/db" # Password in connection string
end
end
# BETTER: Secure handling of sensitive configuration
class SecureConfig
SENSITIVE_KEYS = %w[password secret key token].freeze
def initialize
@config = load_configuration
@sensitive_data = extract_sensitive_data
log_configuration_loaded
end
def get(key)
@config[key.to_s]
end
def secret(key)
@sensitive_data[key.to_s] || raise("Secret not found: #{key}")
end
def inspect
safe_config = @config.reject { |key, _| sensitive_key?(key) }
sensitive_count = @sensitive_data.keys.length
"#<SecureConfig #{safe_config.keys.length} config keys, #{sensitive_count} secrets>"
end
def database_url
# Use connection options instead of URL to avoid password in logs
{
host: get('database_host'),
port: get('database_port'),
database: get('database_name'),
username: get('database_username'),
password: secret('database_password')
}
end
private
def load_configuration
config_data = YAML.load_file('config/application.yml')
# Validate file permissions
file_stat = File.stat('config/application.yml')
if file_stat.mode & 0o044 != 0 # World or group readable
puts "Warning: Configuration file has overly permissive permissions"
end
config_data
rescue Errno::ENOENT
{}
end
def extract_sensitive_data
sensitive = {}
@config.each do |key, value|
if sensitive_key?(key)
sensitive[key] = value
@config.delete(key) # Remove from main config
end
end
# Load additional secrets from environment
ENV.each do |key, value|
if sensitive_key?(key.downcase)
sensitive[key.downcase] = value
end
end
sensitive
end
def sensitive_key?(key)
key_lower = key.to_s.downcase
SENSITIVE_KEYS.any? { |sensitive| key_lower.include?(sensitive) }
end
def log_configuration_loaded
safe_keys = @config.keys.reject { |key| sensitive_key?(key) }
puts "Configuration loaded: #{safe_keys.length} settings, #{@sensitive_data.keys.length} secrets"
puts "Configuration keys: #{safe_keys.join(', ')}" unless safe_keys.empty?
end
end
Reference
Core Configuration Classes and Methods
Method/Class | Parameters | Returns | Description |
---|---|---|---|
YAML.load_file(path) |
path (String) |
Object |
Loads YAML file and returns parsed data structure |
YAML.load(string) |
string (String) |
Object |
Parses YAML string and returns Ruby objects |
JSON.parse(string) |
string (String) |
Object |
Parses JSON string into Ruby objects |
ENV[key] |
key (String) |
String, nil |
Retrieves environment variable value |
ENV.fetch(key, default) |
key (String), default (Object) |
String, Object |
Retrieves environment variable with default |
File.exist?(path) |
path (String) |
Boolean |
Checks if file exists at path |
File.readable?(path) |
path (String) |
Boolean |
Checks if file is readable |
Integer(value) |
value (Object) |
Integer |
Converts value to integer or raises ArgumentError |
Float(value) |
value (Object) |
Float |
Converts value to float or raises ArgumentError |
Environment Variable Conversion Patterns
Pattern | Example | Result Type | Error Handling |
---|---|---|---|
String with default | ENV['HOST'] || 'localhost' |
String |
Returns default if nil |
Integer conversion | (ENV['PORT'] || '3000').to_i |
Integer |
Returns 0 for invalid input |
Safe integer | Integer(ENV['PORT'] || '3000') |
Integer |
Raises ArgumentError for invalid |
Boolean parsing | ENV['DEBUG'] == 'true' |
Boolean |
Explicit string comparison |
Array splitting | ENV['HOSTS'].split(',') |
Array<String> |
Raises NoMethodError if nil |
Safe array | (ENV['HOSTS'] || '').split(',') |
Array<String> |
Returns [''] if nil |
Configuration File Formats
Format | Extension | Loader | Advantages | Disadvantages |
---|---|---|---|---|
YAML | .yml , .yaml |
YAML.load_file |
Human readable, comments, multi-line | Slower parsing, security risks |
JSON | .json |
JSON.parse(File.read) |
Fast parsing, widely supported | No comments, no multi-line strings |
Ruby | .rb |
require_relative |
Full Ruby syntax, conditional logic | Security risk, harder validation |
INI | .ini |
Custom parser | Simple format | Limited nesting, no standard parser |
Common Configuration Errors
Error Class | Typical Cause | Prevention |
---|---|---|
Errno::ENOENT |
Missing configuration file | Check file existence, provide defaults |
Psych::SyntaxError |
Invalid YAML syntax | Validate YAML, use linting tools |
JSON::ParserError |
Invalid JSON syntax | Validate JSON, handle parse errors |
ArgumentError |
Invalid type conversion | Validate input before conversion |
NoMethodError |
Calling method on nil | Check for nil before method calls |
KeyError |
Missing required key | Validate required keys at startup |
Security Best Practices
Practice | Implementation | Benefit |
---|---|---|
File permissions | chmod 600 config/secrets.yml |
Prevents unauthorized access |
Environment variables | Store secrets in ENV , not files |
Keeps secrets out of version control |
Encryption at rest | Encrypt configuration files | Protects secrets in backups |
Secret rotation | Regular key/password updates | Limits exposure window |
Audit logging | Log configuration access | Enables security monitoring |
Validation | Check configuration at startup | Fails fast on misconfiguration |
Configuration Loading Order
Priority | Source | Example | Use Case |
---|---|---|---|
1 (Highest) | Command line arguments | --port=8080 |
Runtime overrides |
2 | Environment variables | PORT=8080 |
Deployment configuration |
3 | Local config files | config/local.yml |
Development overrides |
4 | Environment config | config/production.yml |
Environment-specific settings |
5 | Base config | config/base.yml |
Default values |
6 (Lowest) | Application defaults | Hard-coded constants | Fallback values |
Configuration Validation Patterns
Validation Type | Code Pattern | Error Type |
---|---|---|
Required key | raise "Missing: #{key}" unless config[key] |
StandardError |
Type checking | raise "Invalid type" unless value.is_a?(Integer) |
TypeError |
Range validation | raise "Out of range" unless (1..100).include?(value) |
RangeError |
Format validation | raise "Invalid format" unless value =~ /\A\w+\z/ |
ArgumentError |
Dependency check | raise "Requires X" if needs_x && !config[:x] |
ConfigurationError |
Custom validation | validate_custom(value) or raise "Invalid" |
Custom error |
Thread Safety Considerations
Configuration Type | Thread Safety | Synchronization Needed |
---|---|---|
Constants | Thread-safe | No (immutable) |
Class variables | Not thread-safe | Yes (mutex) |
Class instance variables | Not thread-safe | Yes (mutex) |
Instance variables | Thread-safe per instance | No (if not shared) |
Environment variables | Thread-safe | No (read-only) |
File-based config | Not thread-safe | Yes (for writes) |
Performance Characteristics
Configuration Source | Load Time | Memory Usage | Reload Cost |
---|---|---|---|
Constants | Negligible | Low | Cannot reload |
Class variables | Negligible | Low | Low |
Environment variables | Negligible | Very low | Low |
YAML files | Medium | Medium | Medium |
JSON files | Low | Low | Low |
Ruby files | High | Variable | High |
Database | High | Low | High |
Remote APIs | Very high | Low | Very high |