Introduction
Configuration management separates configuration from code, enabling deployments across environments without code changes. The 12-factor app methodology provides principles for building portable, scalable applications.
Environment Variables
import os
from dataclasses import dataclass
from typing import Optional
@dataclass
class Config:
"""Application configuration from environment variables."""
# Required
database_url: str
redis_url: str
secret_key: str
# Optional with defaults
log_level: str = "INFO"
debug: bool = False
max_connections: int = 10
timeout: int = 30
@classmethod
def from_env(cls) -> "Config":
return cls(
database_url=os.getenv("DATABASE_URL"),
redis_url=os.getenv("REDIS_URL", "redis://localhost:6379"),
secret_key=os.getenv("SECRET_KEY"),
log_level=os.getenv("LOG_LEVEL", "INFO"),
debug=os.getenv("DEBUG", "false").lower() == "true",
max_connections=int(os.getenv("MAX_CONNECTIONS", "10")),
timeout=int(os.getenv("TIMEOUT", "30"))
)
# Usage
config = Config.from_env()
if not config.database_url:
raise ValueError("DATABASE_URL is required")
12-Factor App Principles
# I. Codebase: One codebase, many deployments
# - Single repo per app
# - Deploy to dev, staging, production
# II. Dependencies: Explicitly declare dependencies
# requirements.txt
# package.json
# Use virtual environments
# III. Config: Store config in environment
# Database URLs, API keys, feature flags
# NOT in code
# IV. Backing Services: Treat as attached resources
# Database = attached service
# Redis cache = attached service
# Email service = attached service
# V. Build, Release, Run: Strict separation
# Build: compile/code -> artifact
# Release: artifact + config -> runnable
# Run: execute release
# VI. Processes: Stateless
# No local state
# Use Redis for session state
# Filesystem for temp only
# VII. Port Binding: Export services via port binding
# Web: listen on port 8000
# API: listen on port 8080
# VIII. Concurrency: Scale via process model
# Web: multiple processes
# Workers: background processes
# IX. Disposability: Fast startup and shutdown
# Graceful shutdown handling
# No SIGKILL without cleanup
# X. Dev/Prod Parity: Keep environments similar
# Same OS, same tools
# Minimize gaps
# XI. Logs: Treat logs as event streams
# Write to stdout
# Aggregate with ELK/Splunk
# XII. Admin Processes: Run admin tasks in same environment
# Migrations
# Console/REPL
# One-off jobs
Secrets Management
import os
from abc import ABC, abstractmethod
class SecretsManager(ABC):
"""Abstract secrets manager."""
@abstractmethod
def get_secret(self, key: str) -> str:
pass
@abstractmethod
def set_secret(self, key: str, value: str):
pass
class EnvironmentSecretsManager(SecretsManager):
"""Secrets from environment variables."""
def get_secret(self, key: str) -> str:
return os.getenv(key)
def set_secret(self, key: str, value: str):
os.environ[key] = value
class AWSSecretsManager(SecretsManager):
"""Secrets from AWS Secrets Manager."""
def __init__(self):
import boto3
self.client = boto3.client("secretsmanager")
def get_secret(self, key: str) -> str:
response = self.client.get_secret_value(SecretId=key)
secrets = json.loads(response["SecretString"])
return secrets.get(key)
def set_secret(self, key: str, value: str):
# Update secret
pass
# Usage
secrets = AWSSecretsManager()
api_key = secrets.get_secret("api-key")
Conclusion
Configuration management is critical for deployability. Store config in environment, not code. Use secrets managers for sensitive data. Follow 12-factor principles for portability. Keep dev/prod parity.
Resources
- 12factor.net
- AWS Secrets Manager Documentation
Comments