Skip to main content
โšก Calmops

Configuration Management: Environment-Specific Settings

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

  1. Use environment variables: For values that change between environments
  2. Never commit secrets: Use .gitignore
  3. Use configuration classes: Type-safe and validated
  4. Fail fast on missing required config: Check at startup
  5. Document all settings: Help other developers
  6. 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