Módulo 6: Cloud Migration Patterns
3. Config Management Multi-Entorno
Descripción
En esta cápsula vas a construir un sistema de configuración robusto que maneja tres entornos (development/LocalStack, staging/AWS, production/AWS) con tipado, validación, y separación de secrets. En la cápsula anterior abstraíste el entorno. Ahora vas a darle estructura a esa abstracción: Pydantic Settings para tipar y validar, archivos .env por entorno, reglas de validación que impiden configuraciones inválidas, y un patrón de secrets management que no expone credenciales en archivos de configuración.
Contexto: Variables de entorno para 2 entornos parecen simples. Para 3+ entornos (dev, staging, prod) con diferentes credenciales, endpoints, buckets, feature flags, y secrets, el config management necesita estructura. "Pon un .env" no es un patrón — es el primer paso. El patrón completo incluye tipado (no leer strings que deberían ser ints), validación (no aceptar un ENVIRONMENT=produccion si el valor válido es production), defaults coherentes, y separación de secrets (no guardar AWS keys en git).
Por Qué Config Management Importa
El costo de config mal gestionada
Escenario real — lo que pasa sin config management:
1. Developer cambia ENVIRONMENT a "staging" para probar
2. Olvida cambiarlo de vuelta a "local"
3. Ejecuta tests → escriben datos en S3 de staging
4. Otro developer lee esos datos como "reales" en staging
5. Pasa a producción con datos de test → corrupción
Otro escenario:
1. Lambda en producción tiene S3_BUCKET=ai-assets-staging
2. Typo al configurar → lee datos de staging, no de producción
3. Usuarios reciben respuestas con datos de prueba
4. Nadie entiende por qué — el código "está bien"
Config management previene estos escenarios con validación en el momento de la carga, no en runtime cuando ya es tarde.
Qué necesita un sistema de config para producción
| Requisito | Sin estructura | Con Pydantic Settings |
|---|---|---|
| Tipado | Todo es string | int, bool, Optional[str] |
| Validación | Ninguna | Validators que rechazan valores inválidos |
| Defaults | Hardcodeados en código | Centralizados en la clase |
| Secrets | En .env (en git?) | Separados, via IAM/env vars del sistema |
| Documentación | README manual | La clase ES la documentación |
| Composición | Un .env gigante | .env por entorno |
Pydantic Settings: Config Tipada
La clase de settings base
"""config/settings.py — Settings tipados con Pydantic."""
from pydantic_settings import BaseSettings
from pydantic import field_validator, model_validator
from typing import Optional
from enum import Enum
class EnvironmentName(str, Enum):
LOCAL = "local"
STAGING = "staging"
PRODUCTION = "production"
class Settings(BaseSettings):
"""Configuración completa de la aplicación.
Carga valores desde:
1. Variables de entorno del sistema
2. Archivo .env (si existe)
3. Defaults definidos aquí
"""
# --- Identidad del entorno ---
environment: EnvironmentName = EnvironmentName.LOCAL
app_name: str = "ai-migration-app"
app_version: str = "1.0.0"
debug: bool = False
# --- AWS / Cloud ---
aws_region: str = "us-east-1"
aws_endpoint_url: Optional[str] = None
aws_access_key_id: Optional[str] = None
aws_secret_access_key: Optional[str] = None
# --- S3 ---
s3_bucket: str = "ai-assets-local"
s3_prefix_prompts: str = "prompts/"
s3_prefix_documents: str = "documents/"
s3_prefix_responses: str = "responses/"
# --- Lambda ---
lambda_function_name: str = "ai-processor-local"
lambda_timeout: int = 120
lambda_memory: int = 768
# --- AI / LLM ---
openai_api_key: Optional[str] = None
openai_model: str = "gpt-4o-mini"
openai_max_tokens: int = 1000
openai_temperature: float = 0.3
# --- Feature Flags ---
feature_sagemaker_enabled: bool = False
feature_advanced_logging: bool = False
feature_cost_tracking: bool = False
# --- Operational ---
log_level: str = "INFO"
request_timeout: int = 30
max_retries: int = 3
circuit_breaker_threshold: int = 5
@field_validator("environment", mode="before")
@classmethod
def normalize_environment(cls, v):
if isinstance(v, str):
return v.lower().strip()
return v
@field_validator("log_level", mode="before")
@classmethod
def normalize_log_level(cls, v):
if isinstance(v, str):
v = v.upper().strip()
valid = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
if v not in valid:
raise ValueError(f"log_level debe ser uno de {valid}, recibido: '{v}'")
return v
@model_validator(mode="after")
def validate_environment_config(self):
"""Valida que la config es coherente para el entorno."""
if self.environment == EnvironmentName.LOCAL:
if not self.aws_endpoint_url:
self.aws_endpoint_url = "http://localhost:4566"
if not self.aws_access_key_id:
self.aws_access_key_id = "test"
self.aws_secret_access_key = "test"
if self.environment == EnvironmentName.PRODUCTION:
if self.debug:
raise ValueError(
"debug=True no es permitido en producción"
)
if self.aws_endpoint_url:
raise ValueError(
"aws_endpoint_url no debe setearse en producción "
"(podría redirigir tráfico a LocalStack)"
)
return self
@property
def is_local(self) -> bool:
return self.environment == EnvironmentName.LOCAL
@property
def is_aws(self) -> bool:
return self.environment in (EnvironmentName.STAGING, EnvironmentName.PRODUCTION)
@property
def is_production(self) -> bool:
return self.environment == EnvironmentName.PRODUCTION
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
use_enum_values = True
Carga de settings con .env file específico
"""config/loader.py — Carga settings según el entorno."""
import os
from config.settings import Settings
def get_settings(env_name: str | None = None) -> Settings:
"""Carga settings desde el .env del entorno indicado.
Prioridad:
1. Variables de entorno del sistema (siempre ganan)
2. Archivo .env.{environment}
3. Defaults de la clase Settings
"""
env = env_name or os.environ.get("ENVIRONMENT", "local")
env_file = f".env.{env}"
if os.path.exists(env_file):
return Settings(_env_file=env_file)
return Settings()
# Singleton para uso en la aplicación
_settings: Settings | None = None
def get_cached_settings() -> Settings:
"""Retorna settings cacheados (singleton)."""
global _settings
if _settings is None:
_settings = get_settings()
return _settings
Archivos .env por Entorno
.env.local — Desarrollo con LocalStack
# .env.local — Desarrollo local con LocalStack
ENVIRONMENT=local
APP_NAME=ai-migration-app
DEBUG=true
# AWS / LocalStack
AWS_REGION=us-east-1
AWS_ENDPOINT_URL=http://localhost:4566
AWS_ACCESS_KEY_ID=test
AWS_SECRET_ACCESS_KEY=test
# S3
S3_BUCKET=ai-assets-local
# Lambda
LAMBDA_FUNCTION_NAME=ai-processor-local
LAMBDA_TIMEOUT=120
LAMBDA_MEMORY=768
# AI (usa tu key real para invocar OpenAI incluso en local)
OPENAI_API_KEY=sk-your-dev-key-here
OPENAI_MODEL=gpt-4o-mini
OPENAI_MAX_TOKENS=500
OPENAI_TEMPERATURE=0.3
# Feature flags
FEATURE_SAGEMAKER_ENABLED=false
FEATURE_ADVANCED_LOGGING=false
FEATURE_COST_TRACKING=false
# Operational
LOG_LEVEL=DEBUG
REQUEST_TIMEOUT=30
MAX_RETRIES=3
CIRCUIT_BREAKER_THRESHOLD=10
.env.staging — AWS Staging
# .env.staging — AWS Staging
ENVIRONMENT=staging
APP_NAME=ai-migration-app
DEBUG=true
# AWS (sin endpoint_url → usa AWS real)
AWS_REGION=us-east-1
# Credenciales via IAM role — NO poner keys aquí
# S3
S3_BUCKET=ai-assets-staging-123456789012
# Lambda
LAMBDA_FUNCTION_NAME=ai-processor-staging
LAMBDA_TIMEOUT=120
LAMBDA_MEMORY=768
# AI
OPENAI_MODEL=gpt-4o-mini
OPENAI_MAX_TOKENS=800
OPENAI_TEMPERATURE=0.3
# OPENAI_API_KEY via secrets manager o env var del sistema
# Feature flags
FEATURE_SAGEMAKER_ENABLED=true
FEATURE_ADVANCED_LOGGING=true
FEATURE_COST_TRACKING=true
# Operational
LOG_LEVEL=INFO
REQUEST_TIMEOUT=30
MAX_RETRIES=3
CIRCUIT_BREAKER_THRESHOLD=5
.env.production — AWS Producción
# .env.production — AWS Producción
ENVIRONMENT=production
APP_NAME=ai-migration-app
DEBUG=false
# AWS
AWS_REGION=us-east-1
# Credenciales via IAM role exclusivamente
# S3
S3_BUCKET=ai-assets-prod-123456789012
# Lambda
LAMBDA_FUNCTION_NAME=ai-processor-prod
LAMBDA_TIMEOUT=60
LAMBDA_MEMORY=1024
# AI
OPENAI_MODEL=gpt-4o-mini
OPENAI_MAX_TOKENS=1000
OPENAI_TEMPERATURE=0.2
# OPENAI_API_KEY via AWS Secrets Manager
# Feature flags
FEATURE_SAGEMAKER_ENABLED=true
FEATURE_ADVANCED_LOGGING=true
FEATURE_COST_TRACKING=true
# Operational
LOG_LEVEL=WARNING
REQUEST_TIMEOUT=15
MAX_RETRIES=5
CIRCUIT_BREAKER_THRESHOLD=3
.gitignore — Nunca commitear secrets
# .gitignore
.env
.env.local
.env.staging
.env.production
# Pero SÍ commitea los templates:
# .env.example → template sin valores reales
.env.example — Template para nuevos developers
# .env.example — Copia como .env.{entorno} y llena los valores
ENVIRONMENT=local
AWS_REGION=us-east-1
AWS_ENDPOINT_URL=http://localhost:4566
AWS_ACCESS_KEY_ID=test
AWS_SECRET_ACCESS_KEY=test
S3_BUCKET=ai-assets-local
OPENAI_API_KEY=sk-your-key-here
LOG_LEVEL=DEBUG
Validación de Config por Entorno
Validador que previene errores costosos
"""config/validators.py — Validaciones adicionales de configuración."""
from config.settings import Settings, EnvironmentName
class ConfigValidationError(Exception):
"""Error de validación de configuración."""
pass
def validate_config(settings: Settings) -> list[str]:
"""Valida la configuración y retorna warnings.
Raises ConfigValidationError para problemas críticos.
"""
warnings = []
# Producción: debe tener secrets via IAM, no en .env
if settings.is_production:
if settings.aws_access_key_id and settings.aws_access_key_id != "test":
raise ConfigValidationError(
"Producción no debe tener AWS_ACCESS_KEY_ID en config. "
"Usa IAM roles."
)
# Staging: verificar que no apunte a LocalStack
if settings.environment == EnvironmentName.STAGING:
if settings.aws_endpoint_url and "localhost" in settings.aws_endpoint_url:
raise ConfigValidationError(
"Staging apunta a localhost — ¿es esto intencional? "
"Staging debería usar AWS real."
)
# OpenAI key: warning si no está en entornos que lo necesitan
if not settings.openai_api_key and settings.is_aws:
warnings.append(
"OPENAI_API_KEY no configurado. La inferencia AI fallará."
)
# Feature flags: warning si SageMaker habilitado en local
if settings.feature_sagemaker_enabled and settings.is_local:
warnings.append(
"FEATURE_SAGEMAKER_ENABLED=true en local. "
"LocalStack tiene soporte limitado de SageMaker."
)
# Timeout: warning si muy bajo en producción
if settings.is_production and settings.request_timeout < 10:
warnings.append(
f"REQUEST_TIMEOUT={settings.request_timeout}s es bajo para producción. "
f"Considera 15-30s."
)
# Lambda memory: warning si baja para AI workloads
if settings.lambda_memory < 512:
warnings.append(
f"LAMBDA_MEMORY={settings.lambda_memory}MB puede ser insuficiente "
f"para workloads AI. Recomendado: 768MB+."
)
return warnings
def validate_and_report(settings: Settings):
"""Valida config e imprime reporte."""
print(f"Validando configuración para: {settings.environment}")
try:
warnings = validate_config(settings)
except ConfigValidationError as e:
print(f"❌ ERROR CRÍTICO: {e}")
raise
if warnings:
for w in warnings:
print(f"⚠️ {w}")
else:
print("✅ Configuración válida sin warnings")
Startup validation — fallar rápido
"""app_startup.py — Validación al iniciar la aplicación."""
import sys
from config.loader import get_settings
from config.validators import validate_and_report, ConfigValidationError
def startup():
"""Carga y valida configuración al iniciar."""
try:
settings = get_settings()
except Exception as e:
print(f"❌ Error cargando configuración: {e}")
sys.exit(1)
try:
validate_and_report(settings)
except ConfigValidationError as e:
print(f"❌ Configuración inválida. No se puede iniciar.")
sys.exit(1)
print(f"\n{'='*50}")
print(f"App: {settings.app_name} v{settings.app_version}")
print(f"Entorno: {settings.environment}")
print(f"Region: {settings.aws_region}")
print(f"Bucket: {settings.s3_bucket}")
print(f"Debug: {settings.debug}")
print(f"{'='*50}\n")
return settings
if __name__ == "__main__":
startup()
Secrets Management
El problema: secretos en archivos de configuración
La regla de oro: si un valor comprometido causa daño, es un secret.
Secrets:
├── AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY → acceso a tu cuenta AWS
├── OPENAI_API_KEY → consumo de tu crédito de OpenAI
└── DATABASE_PASSWORD → acceso a tus datos
NO son secrets:
├── AWS_REGION → es público (us-east-1)
├── S3_BUCKET → el nombre no da acceso (IAM sí)
├── LOG_LEVEL → no hay daño si se expone
└── LAMBDA_TIMEOUT → configuración operacional
Patrón para secrets por entorno
"""config/secrets.py — Gestión de secrets por entorno."""
import os
import json
from config.settings import Settings
class SecretsProvider:
"""Provee secrets según el entorno."""
def __init__(self, settings: Settings):
self.settings = settings
def get_openai_key(self) -> str:
"""Obtiene la API key de OpenAI."""
# Prioridad 1: variable de entorno directa
key = os.environ.get("OPENAI_API_KEY")
if key:
return key
# Prioridad 2: en AWS, usar Secrets Manager
if self.settings.is_aws:
return self._get_from_secrets_manager("openai-api-key")
# Prioridad 3: leer de .env (ya cargado en settings)
if self.settings.openai_api_key:
return self.settings.openai_api_key
raise ValueError("OPENAI_API_KEY no configurado en ninguna fuente")
def _get_from_secrets_manager(self, secret_name: str) -> str:
"""Lee un secret de AWS Secrets Manager."""
import boto3
kwargs = {"region_name": self.settings.aws_region}
if self.settings.aws_endpoint_url:
kwargs["endpoint_url"] = self.settings.aws_endpoint_url
client = boto3.client("secretsmanager", **kwargs)
response = client.get_secret_value(SecretId=secret_name)
secret_string = response["SecretString"]
try:
secret_dict = json.loads(secret_string)
return secret_dict.get("api_key", secret_string)
except json.JSONDecodeError:
return secret_string
Crear un secret en LocalStack (para testing)
import boto3
sm = boto3.client(
"secretsmanager",
endpoint_url="http://localhost:4566",
aws_access_key_id="test",
aws_secret_access_key="test",
region_name="us-east-1",
)
sm.create_secret(
Name="openai-api-key",
SecretString=json.dumps({"api_key": "sk-test-key-for-localstack"}),
)
response = sm.get_secret_value(SecretId="openai-api-key")
print(f"Secret: {response['SecretString']}")
Config Comparison Tool
Herramienta para comparar configs entre entornos
"""config/compare.py — Compara configuración entre entornos."""
from config.settings import Settings, EnvironmentName
def compare_configs(env_a: str, env_b: str) -> dict:
"""Compara la configuración de dos entornos."""
settings_a = Settings(environment=env_a, _env_file=f".env.{env_a}")
settings_b = Settings(environment=env_b, _env_file=f".env.{env_b}")
fields_a = settings_a.model_dump()
fields_b = settings_b.model_dump()
differences = {}
for key in fields_a:
val_a = fields_a[key]
val_b = fields_b[key]
if val_a != val_b:
differences[key] = {"env_a": val_a, "env_b": val_b}
return differences
def print_config_diff(env_a: str = "local", env_b: str = "staging"):
"""Imprime las diferencias de configuración entre dos entornos."""
diffs = compare_configs(env_a, env_b)
print(f"\n{'='*60}")
print(f"CONFIG DIFF: {env_a} vs {env_b}")
print(f"{'='*60}")
if not diffs:
print("No hay diferencias (¿mismo .env?)")
return
# Separar secrets de config normal
secret_keys = {"aws_access_key_id", "aws_secret_access_key", "openai_api_key"}
for key, vals in sorted(diffs.items()):
if key in secret_keys:
print(f"\n 🔒 {key}:")
print(f" {env_a}: {'*****' if vals['env_a'] else 'Not set'}")
print(f" {env_b}: {'*****' if vals['env_b'] else 'Not set'}")
else:
print(f"\n 📋 {key}:")
print(f" {env_a}: {vals['env_a']}")
print(f" {env_b}: {vals['env_b']}")
print(f"\n Total diferencias: {len(diffs)}")
print(f"{'='*60}")
Ejemplo de output:
============================================================
CONFIG DIFF: local vs staging
============================================================
📋 environment:
local: local
staging: staging
📋 debug:
local: True
staging: True
📋 aws_endpoint_url:
local: http://localhost:4566
staging: None
🔒 aws_access_key_id:
local: *****
staging: Not set
📋 s3_bucket:
local: ai-assets-local
staging: ai-assets-staging-123456789012
📋 feature_sagemaker_enabled:
local: False
staging: True
📋 log_level:
local: DEBUG
staging: INFO
Total diferencias: 12
============================================================
Troubleshooting
Problema 1: Pydantic ValidationError al iniciar
Un valor en .env no coincide con el tipo esperado.
# .env tiene:
# LAMBDA_TIMEOUT=abc ← debería ser int
# Error:
# ValidationError: 1 validation error for Settings
# lambda_timeout
# Input should be a valid integer [type=int_parsing, ...]
# Solución: verificar tipos en .env
# LAMBDA_TIMEOUT=120 ← correcto
Problema 2: Settings ignora el archivo .env
Pydantic Settings prioriza variables de entorno del sistema sobre el .env.
# Si tienes esto en el shell:
export ENVIRONMENT=production
# Y esto en .env.local:
ENVIRONMENT=local
# Pydantic usa "production" (env var del sistema gana)
# Solución: unset la variable del sistema
unset ENVIRONMENT
# O sé explícito en el loader:
# Settings(_env_file=".env.local")
Problema 3: Config de producción tiene debug=True
El validator validate_environment_config debe prevenirlo, pero si se bypasea:
# La clase Settings tiene:
# @model_validator(mode="after")
# def validate_environment_config(self):
# if self.environment == "production" and self.debug:
# raise ValueError("debug=True no es permitido en producción")
# Esto previene iniciar con config inválida.
# Si el error persiste, verifica que el .env.production no tenga DEBUG=true.
Problema 4: Secrets de staging accesibles en local
Esto pasa cuando el developer tiene credenciales AWS en ~/.aws/credentials y no setea AWS_ENDPOINT_URL:
# El client sin endpoint_url → habla con AWS real
# Incluso si ENVIRONMENT=local
# Solución: el validator de Settings setea endpoint_url automáticamente en local:
# if self.environment == "local" and not self.aws_endpoint_url:
# self.aws_endpoint_url = "http://localhost:4566"
Problema 5: .env.example está desactualizado
Agregar un script que verifique que .env.example tiene las mismas keys que la clase Settings:
from config.settings import Settings
settings_keys = set(Settings.model_fields.keys())
with open(".env.example") as f:
example_keys = set()
for line in f:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
key = line.split("=")[0].strip().lower()
example_keys.add(key)
missing = settings_keys - example_keys
extra = example_keys - settings_keys
if missing:
print(f"⚠️ Keys en Settings pero no en .env.example: {missing}")
if extra:
print(f"⚠️ Keys en .env.example pero no en Settings: {extra}")
if not missing and not extra:
print("✅ .env.example sincronizado con Settings")
Ejercicios Prácticos
Ejercicio 1: Settings con validación custom
Extiende la clase Settings con un campo max_document_size_mb (int, default 10) y un validator que rechace valores mayores a 50 en producción y mayores a 100 en otros entornos.
Ver solución
from pydantic_settings import BaseSettings
from pydantic import field_validator, model_validator
from typing import Optional
from enum import Enum
class EnvironmentName(str, Enum):
LOCAL = "local"
STAGING = "staging"
PRODUCTION = "production"
class Settings(BaseSettings):
environment: EnvironmentName = EnvironmentName.LOCAL
aws_region: str = "us-east-1"
aws_endpoint_url: Optional[str] = None
s3_bucket: str = "ai-assets-local"
max_document_size_mb: int = 10
@model_validator(mode="after")
def validate_document_size(self):
if self.environment == EnvironmentName.PRODUCTION:
if self.max_document_size_mb > 50:
raise ValueError(
f"max_document_size_mb={self.max_document_size_mb} "
f"excede el límite de 50MB para producción"
)
else:
if self.max_document_size_mb > 100:
raise ValueError(
f"max_document_size_mb={self.max_document_size_mb} "
f"excede el límite de 100MB"
)
return self
class Config:
env_file = ".env"
use_enum_values = True
# Test: producción con 60MB → falla
try:
s = Settings(environment="production", max_document_size_mb=60)
except ValueError as e:
print(f"✅ Validación correcta: {e}")
# Test: local con 80MB → OK
s = Settings(environment="local", max_document_size_mb=80)
print(f"✅ Local con 80MB: OK")
# Test: local con 150MB → falla
try:
s = Settings(environment="local", max_document_size_mb=150)
except ValueError as e:
print(f"✅ Validación correcta: {e}")
Ejercicio 2: Config loader con fallback chain
Implementa un config loader que intente cargar en este orden: .env.{env}.local (overrides locales) → .env.{env} → .env → defaults. El primer archivo que exista se usa.
Ver solución
import os
from pydantic_settings import BaseSettings
from typing import Optional
class Settings(BaseSettings):
environment: str = "local"
aws_region: str = "us-east-1"
aws_endpoint_url: Optional[str] = None
s3_bucket: str = "ai-assets-local"
debug: bool = False
class Config:
env_file = ".env"
use_enum_values = True
def load_settings_with_fallback(env_name: str | None = None) -> Settings:
"""Carga settings con cadena de fallback de archivos .env."""
env = env_name or os.environ.get("ENVIRONMENT", "local")
candidates = [
f".env.{env}.local", # Overrides locales del developer
f".env.{env}", # Config del entorno
".env", # Fallback general
]
env_file_used = None
for candidate in candidates:
if os.path.exists(candidate):
env_file_used = candidate
break
if env_file_used:
print(f"📁 Config cargada desde: {env_file_used}")
return Settings(_env_file=env_file_used)
else:
print(f"📁 Config cargada desde: defaults (ningún .env encontrado)")
return Settings()
settings = load_settings_with_fallback()
print(f"Entorno: {settings.environment}")
print(f"Bucket: {settings.s3_bucket}")
print(f"Debug: {settings.debug}")
Ejercicio 3: Config export/import
Crea funciones para exportar la configuración actual (sin secrets) a un archivo JSON, e importar configuración desde un JSON para comparar o restaurar.
Ver solución
import json
from datetime import datetime
from config.settings import Settings
SECRET_FIELDS = {"aws_access_key_id", "aws_secret_access_key", "openai_api_key"}
def export_config(settings: Settings, filepath: str) -> str:
"""Exporta configuración actual a JSON (sin secrets)."""
config_dict = settings.model_dump()
for field in SECRET_FIELDS:
if field in config_dict and config_dict[field]:
config_dict[field] = "***REDACTED***"
export = {
"exported_at": datetime.utcnow().isoformat(),
"environment": settings.environment,
"config": config_dict,
}
with open(filepath, "w") as f:
json.dump(export, f, indent=2, default=str)
print(f"✅ Config exportada a: {filepath}")
print(f" Entorno: {settings.environment}")
print(f" Campos: {len(config_dict)}")
print(f" Secrets redactados: {len(SECRET_FIELDS)}")
return filepath
def import_and_compare(filepath: str, current: Settings) -> dict:
"""Importa config de JSON y compara con la actual."""
with open(filepath) as f:
imported = json.load(f)
imported_config = imported["config"]
current_config = current.model_dump()
differences = {}
for key in current_config:
imported_val = imported_config.get(key)
current_val = current_config[key]
if key in SECRET_FIELDS:
continue
if imported_val != current_val:
differences[key] = {
"imported": imported_val,
"current": current_val,
}
print(f"\n📊 Comparación: {filepath} vs config actual")
print(f" Importado de: {imported.get('exported_at', 'unknown')}")
if differences:
for key, vals in differences.items():
print(f" ≠ {key}: {vals['imported']} → {vals['current']}")
else:
print(" ✅ Sin diferencias (excluyendo secrets)")
return differences
settings = Settings()
export_config(settings, "config-backup.json")
diffs = import_and_compare("config-backup.json", settings)
Ejercicio 4: Environment-aware logging config
Crea una función que configure el logging de Python según el entorno: DEBUG con formato detallado en local, INFO con formato JSON en staging, WARNING con formato JSON compacto en producción.
Ver solución
import logging
import json
from datetime import datetime
from config.settings import Settings, EnvironmentName
class JSONFormatter(logging.Formatter):
"""Formatter que produce JSON para entornos cloud."""
def __init__(self, compact: bool = False):
super().__init__()
self.compact = compact
def format(self, record):
log_data = {
"timestamp": datetime.utcnow().isoformat(),
"level": record.levelname,
"message": record.getMessage(),
"module": record.module,
}
if not self.compact:
log_data["function"] = record.funcName
log_data["line"] = record.lineno
if record.exc_info and record.exc_info[0]:
log_data["exception"] = self.formatException(record.exc_info)
return json.dumps(log_data)
def configure_logging(settings: Settings) -> logging.Logger:
"""Configura logging según el entorno."""
logger = logging.getLogger(settings.app_name)
logger.handlers.clear()
logger.setLevel(getattr(logging, settings.log_level))
handler = logging.StreamHandler()
if settings.is_local:
formatter = logging.Formatter(
"%(asctime)s [%(levelname)s] %(name)s.%(funcName)s:%(lineno)d — %(message)s",
datefmt="%H:%M:%S",
)
elif settings.is_production:
formatter = JSONFormatter(compact=True)
else:
formatter = JSONFormatter(compact=False)
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.info(f"Logging configurado: level={settings.log_level}, "
f"env={settings.environment}")
return logger
# Demo
for env in ["local", "staging", "production"]:
print(f"\n--- Entorno: {env} ---")
try:
s = Settings(
environment=env,
log_level="DEBUG" if env == "local" else "INFO" if env == "staging" else "WARNING",
debug=(env == "local"),
)
logger = configure_logging(s)
logger.debug("Mensaje debug")
logger.info("Mensaje info")
logger.warning("Mensaje warning")
except Exception as e:
print(f" Config error: {e}")
Resumen
- Config management no es "pon un .env." Es tipado con Pydantic, validación por entorno, separación de secrets, y archivos .env específicos por entorno.
- Pydantic Settings es tu fuente de verdad. La clase define qué config existe, qué tipo tiene, cuál es el default, y qué validaciones aplican. La clase ES la documentación.
- Tres archivos .env:
.env.local(LocalStack),.env.staging(AWS staging),.env.production(AWS prod). Nunca en git. Sí un.env.examplecomo template. - Validators que protegen:
debug=Trueen producción → error.endpoint_urlen producción → error.SageMaker enableden local → warning. Fallar rápido es mejor que fallar en runtime. - Secrets van separados. En local, en .env (fuera de git). En AWS, via IAM roles o Secrets Manager. Nunca hardcodeados en código.
- En la siguiente cápsula, usarás esta configuración para inyectar boto3 clients con dependency injection.
Recursos Adicionales
- Pydantic Settings Management — Documentación oficial
- The Twelve-Factor App — Config — Principios de configuración
- python-dotenv — Carga de archivos .env
- AWS Secrets Manager — Gestión de secrets en AWS
- Pydantic Validators — Validación custom en Pydantic
- OWASP — Secrets Management — Best practices de secrets