Introduction
Configuration management is critical for deploying applications across multiple environments. Proper configuration handling enables easy environment switching, secrets management, and flexible deployments. This guide covers configuration patterns and best practices.
Configuration should be separated from code. This allows the same code to run in different environments by changing configuration values.
The 12-Factor Config
Principles
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 12-Factor App Config โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ 1. Store config in environment variables โ
โ 2. Strict separation of config and code โ
โ 3. Keep config consistent across environments โ
โ 4. Don't commit secrets to version control โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Configuration Patterns
Environment Variables
import os
from typing import Optional
from functools import lru_cache
class Config:
"""Application configuration from environment variables."""
# Database
DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:///app.db")
DATABASE_POOL_SIZE: int = int(os.getenv("DATABASE_POOL_SIZE", "10"))
DATABASE_MAX_OVERFLOW: int = int(os.getenv("DATABASE_MAX_OVERFLOW", "20"))
# Redis
REDIS_URL: str = os.getenv("REDIS_URL", "redis://localhost:6379")
# Application
APP_NAME: str = os.getenv("APP_NAME", "myapp")
APP_ENV: str = os.getenv("APP_ENV", "development")
DEBUG: bool = os.getenv("DEBUG", "false").lower() == "true"
LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
# Security
SECRET_KEY: str = os.getenv("SECRET_KEY")
API_KEY: Optional[str] = os.getenv("API_KEY")
# External Services
PAYMENT_GATEWAY_URL: str = os.getenv("PAYMENT_GATEWAY_URL")
PAYMENT_GATEWAY_API_KEY: str = os.getenv("PAYMENT_GATEWAY_API_KEY")
@property
def is_production(self) -> bool:
return self.APP_ENV == "production"
@property
def is_development(self) -> bool:
return self.APP_ENV == "development"
def require(self, key: str) -> str:
"""Get required config or raise error."""
value = os.getenv(key)
if not value:
raise ValueError(f"Required environment variable {key} is not set")
return value
# Usage
config = Config()
# In settings
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": config.DATABASE_URL,
"OPTIONS": {
"pool_size": config.DATABASE_POOL_SIZE,
"max_overflow": config.DATABASE_MAX_OVERFLOW
}
}
}
Configuration Classes
from pydantic import BaseModel, Field
from typing import Optional
from enum import Enum
class Environment(str, Enum):
DEVELOPMENT = "development"
STAGING = "staging"
PRODUCTION = "production"
class DatabaseConfig(BaseModel):
url: str
pool_size: int = 10
max_overflow: int = 20
pool_timeout: int = 30
class RedisConfig(BaseModel):
url: str
db: int = 0
password: Optional[str] = None
ssl: bool = False
class SecurityConfig(BaseModel):
secret_key: str
algorithm: str = "HS256"
access_token_expire_minutes: int = 30
class AppConfig(BaseModel):
environment: Environment
debug: bool = False
log_level: str = "INFO"
database: DatabaseConfig
redis: RedisConfig
security: SecurityConfig
@property
def is_production(self) -> bool:
return self.environment == Environment.PRODUCTION
def load_config() -> AppConfig:
"""Load configuration based on environment."""
import os
env = os.getenv("APP_ENV", "development")
if env == "production":
return load_production_config()
elif env == "staging":
return load_staging_config()
else:
return load_development_config()
Configuration Files
# config.yaml
development:
database:
url: "postgresql://localhost:5432/devdb"
pool_size: 5
redis:
url: "redis://localhost:6379"
debug: true
staging:
database:
url: "postgresql://staging-db:5432/staging"
pool_size: 10
redis:
url: "redis://staging-redis:6379"
debug: false
production:
database:
url: ${DATABASE_URL}
pool_size: 20
max_overflow: 40
redis:
url: ${REDIS_URL}
ssl: true
debug: false
import yaml
from pathlib import Path
class ConfigLoader:
def __init__(self, config_dir: Path):
self.config_dir = config_dir
def load(self, environment: str) -> dict:
config_file = self.config_dir / f"{environment}.yaml"
with open(config_file) as f:
config = yaml.safe_load(f)
# Resolve environment variables
return self._resolve_env_vars(config)
def _resolve_env_vars(self, config):
"""Replace ${VAR} with environment variables."""
import re
def replace(match):
var_name = match.group(1)
default = match.group(2)
value = os.getenv(var_name)
if value is None:
return default.lstrip(':') if default else ''
return value
# Recursively process all strings
return self._process_values(config, replace)
Secrets Management
Environment-Based Secrets
# .env file (add to .gitignore)
DATABASE_URL=postgresql://user:password@localhost/db
REDIS_URL=redis://:password@localhost:6379
SECRET_KEY=your-secret-key-here
API_KEY=sk_live_123456789
# .env.example (safe to commit)
DATABASE_URL=
REDIS_URL=
SECRET_KEY=
API_KEY=
Vault Integration
import hvac
class VaultClient:
def __init__(self, vault_url: str, token: str):
self.client = hvac.Client(url=vault_url, token=token)
def get_secret(self, path: str, key: str) -> str:
"""Get secret from Vault."""
secret = self.client.secrets.kv.v2.read_secret_version(path)
return secret['data']['data'][key]
def get_database_creds(self, role: str) -> dict:
"""Get dynamic database credentials."""
return self.client.secrets.database.generate_credentials(role)
# Usage
vault = VaultClient(
vault_url=os.getenv("VAULT_ADDR"),
token=os.getenv("VAULT_TOKEN")
)
db_creds = vault.get_database_creds("app-role")
Feature Flags as Config
# Feature flag configuration
class FeatureFlags:
def __init__(self):
self.flags = {
"new_checkout": os.getenv("FF_NEW_CHECKOUT", "false").lower() == "true",
"ai_recommendations": float(os.getenv("FF_AI_RECS", "0")),
"dark_mode": os.getenv("FF_DARK_MODE", "control"),
"payment_v2": os.getenv("FF_PAYMENT_V2", "percent:10")
}
def is_enabled(self, flag: str) -> bool:
return self.flags.get(flag, False)
def get_percentage(self, flag: str) -> float:
value = self.flags.get(flag, "0")
if value == "true":
return 100.0
if value == "false":
return 0.0
if value.startswith("percent:"):
return float(value.split(":")[1])
return float(value)
def is_enabled_for_user(self, flag: str, user_id: str) -> bool:
percentage = self.get_percentage(flag)
if percentage >= 100:
return True
if percentage <= 0:
return False
# Consistent hashing for same user
bucket = hash(f"{flag}:{user_id}") % 100
return bucket < percentage
Best Practices
- Use environment variables: For values that change between environments
- Never commit secrets: Use .gitignore
- Use configuration classes: Type-safe and validated
- Fail fast on missing required config: Check at startup
- Document all settings: Help other developers
- Use secrets management: Vault for production
Conclusion
Proper configuration management enables flexible deployments across environments. By separating configuration from code, using environment variables, and implementing proper secrets management, you can deploy applications reliably across development, staging, and production.
Comments