Configuración y Gestión de Secretos sin Drama: Guía Práctica de FastAPI × pydantic-settings — Variables de Entorno, .env, Conmutación Multi-Entorno, Seguridad de Tipos, Validación, Operaciones con Secretos y Feature Flags
Resumen (Primero la Vista General)
- El objetivo es estandarizar la configuración y el manejo de secretos en FastAPI y automatizar de forma segura la conmutación entre múltiples entornos (desarrollo, staging y producción).
- La arquitectura recomendada se centra en pydantic-settings v2, con variables de entorno como máxima prioridad, archivos
.envcomo ayudantes locales y Administradores de Secretos o Secrets de Docker/Kubernetes en producción. - Mantén la calidad con seguridad de tipos y validación, divide la configuración en estructuras por dependencia, usa Feature Flags para cambios graduales de comportamiento y aplica la misma disciplina a pruebas y CI/CD.
- Como toque final, proporcionamos ejemplos de logging, CORS, conexiones a BD, claves de API externas y profundizamos en operaciones como rotación y revocación durante incidentes.
Quién se Beneficia (Personas Concretas)
- Aprendiz A (último año de carrera / dev en solitario): Quiere entender de forma confiable dónde colocar
.env, el orden de carga y los conceptos básicos de la gestión de secretos en producción. - Equipo Pequeño B (agencia de 3 personas): Cortes frecuentes debido a diferencias entre staging y producción. Busca un modelo unificado de Settings y validación para reducir incidentes.
- Equipo SaaS C (startup): Quiere introducir Feature Flags y despliegues graduales para experimentar con seguridad. Necesita una estrategia para rotación de claves y sustitución en tiempo de ejecución.
Revisión de Accesibilidad
- Estructura: Resumen al inicio → Diseño central → Implementación → Multi-entorno → Gestión de secretos → Operaciones y pruebas → Escollos → Recapitulación (pirámide invertida).
- Lenguaje: Definir la jerga brevemente en el primer uso. Mantener comentarios en el código al mínimo y presentar el código en bloques monoespaciados para legibilidad.
- Cuidado: Párrafos cortos, viñetas para reducir el recorrido visual, reiterar audiencia y beneficios en cada sección.
- Nivel general: Equivalente a AA.
1. Política Central (A Través del Prisma de 12-Factor App)
En 12-Factor App, la configuración debe inyectarse mediante variables de entorno. Mantener los secretos fuera del repositorio es primordial.
pydantic-settings implementa este principio con seguridad de tipos.
Pilares de la política
- Crear una única clase de settings como fuente única de la verdad de la aplicación.
- Precedencia de carga: “Variables de entorno >
.env> Valores por defecto”. - Secretos en producción provenientes de fuentes de Secretos externas (variables de entorno o montajes de archivos).
- Realizar validación y transformación en la clase de settings (fallar inmediatamente en el arranque).
- Conmutación multi-entorno mediante nombre de entorno y diferencias de valores, con lógica condicional mínima.
2. Empieza con la Clase de Settings más Pequeña Posible
2.1 Dependencias
pydantic>=2.6
pydantic-settings>=2.3
python-dotenv # (opcional) ayudante para .env
2.2 Ejemplo Mínimo
# app/core/settings.py
from pydantic import Field, PostgresDsn, AnyUrl, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import Literal, Optional
EnvName = Literal["dev", "stg", "prod"]
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env", # local helper
env_file_encoding="utf-8",
env_prefix="", # optional prefix
extra="ignore" # ignore unexpected keys (can be stricter)
)
# Common
app_name: str = "My FastAPI"
env: EnvName = "dev"
host: str = "0.0.0.0"
port: int = 8000
log_level: Literal["debug", "info", "warning", "error"] = "info"
# DB
database_url: PostgresDsn | str = "postgresql+psycopg://user:pass@localhost:5432/appdb"
# CORS
cors_origins: list[str] = ["http://localhost:3000"]
# External API
external_api_base: AnyUrl | None = None
external_api_key: Optional[str] = Field(default=None, repr=False)
# Security
secret_key: str = Field(..., repr=False, description="Used for JWT, etc.")
access_token_expires_minutes: int = 15
# Feature Flag
enable_new_search: bool = False
@field_validator("cors_origins", mode="before")
@classmethod
def parse_cors_origins(cls, v):
if isinstance(v, str):
# Turn "https://a,https://b" from env vars into a list
return [s.strip() for s in v.split(",") if s.strip()]
return v
def get_settings() -> Settings:
return Settings()
2.3 Uso (en la app)
# app/main.py
from fastapi import FastAPI
from app.core.settings import get_settings
settings = get_settings()
app = FastAPI(title=settings.app_name)
@app.get("/meta")
def meta():
return {
"app": settings.app_name,
"env": settings.env,
"log_level": settings.log_level,
"flags": {"new_search": settings.enable_new_search}
}
Puntos clave
- Usa anotaciones de tipo para restringir la entrada; rechaza valores inválidos temprano con
Literaly tiposDsn. - Usa
repr=Falsepara evitar registrar secretos. - Usa
field_validatorpara preprocesamiento pragmático como conversiones de cadena a lista para entradas desde variables de entorno.
3. Orden de Carga y Precedencia (Garantías al Arranque)
Por principio, las variables de entorno tienen la máxima prioridad. .env es un ayudante para desarrollo local.
- Variables de entorno del proceso (p. ej.,
ENV=prod) .env(por lo general no lo incluyas; si lo haces, solo una plantilla)- Valores por defecto (valores iniciales en el código)
Consejos prácticos
- Proporciona un
.env.exampleen el repo, marcando explícitamente los campos requeridos con comentarios. - No uses
.enven producción; inyecta mediante secretos gestionados como variables de entorno. - No des valores por defecto a campos requeridos (haz
secret_keyobligatorio, por ejemplo).
4. Conmutación Multi-Entorno (dev/stg/prod)
4.1 Reglas
- Cambia el comportamiento mediante el valor
env. Diseña para que los valores controlen el comportamiento y no proliferen los if. - Sustituye elementos como CORS, logging y endpoints de APIs externas mediante valores de configuración; mantén el código de la aplicación sin cambios.
4.2 Ejemplo: Inyectar Diferencias Específicas por Entorno
# dev (.env para local)
ENV=dev
SECRET_KEY=dev-secret
DATABASE_URL=postgresql+psycopg://dev:dev@localhost:5432/app_dev
CORS_ORIGINS=http://localhost:3000
# stg (inyectado por CI/CD)
ENV=stg
SECRET_KEY=stg-secret
DATABASE_URL=postgresql+psycopg://stg:stg@stg-db:5432/app_stg
CORS_ORIGINS=https://stg.example.com
# prod (inyectado por un Secret Manager o K8s)
ENV=prod
SECRET_KEY=prod-secret
DATABASE_URL=postgresql+psycopg://prod:prod@prod-db:5432/app
CORS_ORIGINS=https://app.example.com
LOG_LEVEL=warning
ENABLE_NEW_SEARCH=true
4.3 Un Diseño de Conmutación Solo por Valores
# app/core/bootstrap.py
from app.core.settings import get_settings
from app.core.logging import setup_logging
def bootstrap():
s = get_settings()
setup_logging(s.log_level)
# Pass s.cors_origins as-is to CORS middleware, etc.
Cuanto más inmutable sea el código de arranque de tu app, menos probable será que las diferencias entre entornos causen incidentes.
5. Dividir en Sub-Configs (Claridad y Reutilización)
A medida que crecen los settings, querrás dividir por preocupación. Por ejemplo, organízalos en estructuras anidadas así:
# app/core/settings.py (extracto, con divisiones)
from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict
class DbConfig(BaseModel):
url: str
pool_size: int = 5
echo: bool = False
class SecurityConfig(BaseModel):
secret_key: str
access_token_expires_minutes: int = 15
class CorsConfig(BaseModel):
origins: list[str] = []
allow_credentials: bool = True
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
env: EnvName = "dev"
log_level: Literal["debug", "info", "warning", "error"] = "info"
db: DbConfig = DbConfig(url="sqlite:///./app.db") # keep defaults small
security: SecurityConfig = SecurityConfig(secret_key="PLEASE-SET")
cors: CorsConfig = CorsConfig(origins=["http://localhost:3000"])
Para sobrescribir campos anidados con variables de entorno, usa doble guion bajo (convención de anidamiento de pydantic), p. ej., DB__URL.
# Ejemplo
DB__URL=postgresql+psycopg://user:pass@host:5432/appdb
CORS__ORIGINS=https://app.example.com,https://stg.example.com
6. Validación y Consistencia Entre Dependencias
La configuración debe fallar al arranque cuando esté rota. Comprueba la consistencia entre condiciones.
# app/core/settings.py (ejemplo de comprobación de consistencia)
from pydantic import model_validator
class Settings(BaseSettings):
# ... (omitted)
env: EnvName = "dev"
external_api_base: AnyUrl | None = None
external_api_key: str | None = None
@model_validator(mode="after")
def check_external_api(self):
if (self.external_api_base is None) ^ (self.external_api_key is None):
raise ValueError("Set external_api_base and external_api_key together.")
if self.env == "prod" and self.log_level == "debug":
raise ValueError("log_level=debug is not allowed in prod.")
return self
Esto hace que combinaciones inválidas fallen de inmediato, no durante el despliegue.
7. Manejo de Secretos (Operar con Seguridad)
7.1 Principios
- No codifiques secretos en el código; evita ponerlos en
.envcuando sea posible. - En producción, usa un Administrador de Secretos o Secrets de Kubernetes/Docker e inyecta como variables de entorno.
- No registres secretos (
repr=False, enmascaramiento). - Planifica la rotación (actualizaciones periódicas de claves) y la revocación (corte inmediato tras filtración).
7.2 Secretos Montados como Archivos
A veces los Secretos de cloud/Kubernetes se montan como archivos. Con un pequeño ayudante, pydantic-settings puede leer archivos fácilmente.
# app/core/secret_loader.py
from pathlib import Path
def from_file(path: str) -> str:
p = Path(path)
if not p.exists():
raise FileNotFoundError(path)
return p.read_text(encoding="utf-8").strip()
# app/core/settings.py (ejemplo de uso)
secret_key: str = Field(default_factory=lambda: from_file("/run/secrets/secret_key"))
Asume inyección en tiempo de ejecución en producción mientras usas .env localmente como ayudante: es un patrón robusto.
8. Feature Flags (Conmutación Segura de Comportamientos)
En lugar de lanzar una función nueva en producción de una sola vez, actívala gradualmente con flags.
8.1 Diseño
- Añade campos
enable_xxx: boola la clase de settings. - Ramifica en rutas o capas de servicio, pero localiza los puntos de ramificación.
- Para experimentos con porcentajes o cohortes, combina variables de entorno con atributos de usuario.
8.2 Ejemplo
# app/services/search.py
from app.core.settings import get_settings
def search(query: str):
s = get_settings()
if s.enable_new_search:
return new_engine(query)
return legacy_engine(query)
Opera los flags asumiendo que serán eliminados; decide una vida útil para evitar la degradación.
9. Ejemplos Comunes de Settings: Logging, CORS, BD
9.1 Logging
# app/core/logging.py
import json, logging, sys
from typing import Any
class JsonFormatter(logging.Formatter):
def format(self, record):
payload: dict[str, Any] = {
"t": self.formatTime(record, "%Y-%m-%dT%H:%M:%S"),
"lvl": record.levelname,
"name": record.name,
"msg": record.getMessage(),
}
return json.dumps(payload, ensure_ascii=False)
def setup_logging(level: str = "info"):
h = logging.StreamHandler(sys.stdout)
h.setFormatter(JsonFormatter())
root = logging.getLogger()
root.handlers[:] = [h]
root.setLevel(level.upper())
9.2 CORS
# app/main.py (CORS)
from fastapi.middleware.cors import CORSMiddleware
from app.core.settings import get_settings
s = get_settings()
app.add_middleware(
CORSMiddleware,
allow_origins=s.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
9.3 Conexión a BD (SQLAlchemy)
# app/db/session.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.core.settings import get_settings
s = get_settings()
engine = create_engine(s.database_url, pool_pre_ping=True, echo=False)
SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
10. Inyección de Settings en Pruebas y CI
10.1 Usar un .env.test Dedicado
- Al ejecutar
pytest, estableceENV=deve inyecta una BD solo para tests ysecret_key. - En lugar de
BaseSettings(env_file=".env"), apunta a un archivo diferente solo durante el arranque de pruebas.
# tests/conftest.py
import os
os.environ["ENV"] = "dev"
os.environ["SECRET_KEY"] = "test-secret"
os.environ["DATABASE_URL"] = "sqlite:///./test.db"
Sobrescribir mediante variables de entorno es simple y mantiene la consistencia a nivel de proceso.
10.2 Inyección en CI/CD
- Guarda claves en almacenes de secretos de CI (GitHub Actions / GitLab CI) e inyecta como variables de entorno del job.
- Dado que la validación de pydantic se ejecuta antes de fusionar/desplegar, detectarás settings faltantes al instante.
11. Operación con Docker/Kubernetes
11.1 Docker
- No coloques valores desconocidos en instrucciones
ENV; inyecta condocker run -e KEY=...oenvironmentencompose. - Para secretos, usa
docker secreto fuentes externas de Secretos (inyecta como variables de entorno o lee desde archivo).
Ejemplo docker-compose.yml:
services:
app:
image: my-fastapi:latest
environment:
ENV: "stg"
LOG_LEVEL: "info"
DATABASE_URL: "${DATABASE_URL}"
SECRET_KEY: "${SECRET_KEY}"
ports: ["8000:8000"]
11.2 Kubernetes
- Separa la configuración no secreta en
ConfigMapy los secretos enSecret. - Inyéctalos en Pods mediante variables de entorno o volúmenes.
- Durante la rotación, combínalo con Rolling Update y estabiliza con probes de readiness.
12. Comportamiento ante Fallos y Redes de Seguridad
- Falla rápido ante errores de validación en el arranque. Es más seguro que ejecutar ambiguamente.
- Para settings críticos, falla en modo cerrado (desactiva la funcionalidad cuando falten valores).
- Si falta una clave de API externa, lanza error en lugar de cambiar a una implementación dummy para evitar accidentes.
13. Respuesta a Incidentes (Revocación y Rotación)
- Ante filtración de claves, revoca de inmediato. Regenera desde la consola o API del emisor y actualiza las variables de entorno.
- Ayuda si la app soporta recarga de configuración. Puedes recargar con SIGHUP o en un programador periódico (pero recargar secretos en tiempo de ejecución requiere diseño operativo cuidadoso).
Ejemplo simple de gancho de recarga:
# app/core/runtime.py
from app.core.settings import Settings
_settings_cache: Settings | None = None
def current_settings() -> Settings:
global _settings_cache
if _settings_cache is None:
_settings_cache = Settings()
return _settings_cache
def reload_settings():
global _settings_cache
_settings_cache = Settings()
14. Patrones que Ayudan en el Mundo Real
14.1 A/B Testing (Flags por Porcentaje)
# app/core/flags.py
import hashlib
from app.core.settings import get_settings
def rollout(user_id: str, percentage: int) -> bool:
# Asignación estable mediante hashing
v = int(hashlib.sha256(user_id.encode()).hexdigest()[:8], 16) % 100
return v < percentage
def new_ui_enabled(user_id: str) -> bool:
s = get_settings()
if not s.enable_new_search:
return False
return rollout(user_id, 20) # desplegar al 20%
14.2 Conmutación de Destinos (Región/Tenant)
# app/core/endpoint_resolver.py
from app.core.settings import get_settings
def api_base_for_tenant(tenant: str) -> str:
s = get_settings()
if s.env == "prod":
return f"https://{tenant}.api.example.com"
return f"https://stg-{tenant}.api.example.com"
14.3 Auto-Desactivación con Tiempo Límite
# app/core/flag_until.py
from datetime import datetime, timezone
def enabled_until(dt_iso: str) -> bool:
# "2025-12-31T23:59:59Z"
limit = datetime.fromisoformat(dt_iso.replace("Z","+00:00"))
return datetime.now(timezone.utc) < limit
15. Escollos Comunes y Remedios
| Síntoma | Causa | Remedio |
|---|---|---|
| Solo falla producción | Dependencia oculta de .env / falta reemplazo de variable de entorno |
Las variables de entorno tienen prioridad, omite valores por defecto para valores requeridos |
| Los secretos aparecen en logs | Impresos o incluidos en excepciones | repr=False, enmascaramiento, logs estructurados con control de campos |
| CORS falla | Cadena no convertida a lista | Validador cadena→lista, registra valores finales |
| Demasiados flags, confusión | Acumulación de flags sin caducidad | Fechas de caducidad y tareas de limpieza de flags, convenciones de nombres |
| Validación débil colapsa luego | Validación insuficiente | Usa field_validator / model_validator para fallar al arranque |
16. Hoja de Ruta de Adopción
- Introduce una única clase de Settings y prepara
.env.example. No ofrezcas valores por defecto para elementos requeridos. - Inyecta valores multi-entorno mediante variables de entorno; haz que la app solo consuma valores.
- Añade validación y comprobaciones de consistencia. Para CORS, BD y claves de API externas, falla al arranque cuando estén mal.
- Traslada la gestión de secretos a un Administrador de Secretos o a Secrets de K8s. Establece procedimientos de rotación.
- Introduce Feature Flags y despliegue gradual para lanzamientos seguros.
- Integra inyección de settings y validación de esquema en CI/CD para prevenir accidentes por diferencias.
Referencias
- Pydantic
- pydantic-settings
- 12-Factor
- FastAPI
- Seguridad/Secretos
Recapitulación
- Para la configuración, los fundamentos son seguridad de tipos, precedencia de variables de entorno y .env solo como ayudante local.
- Usa pydantic-settings para configuración estructurada, y estabiliza operaciones fallando al arranque mediante validación y comprobaciones de consistencia.
- Inyecta secretos desde fuentes externas de Secretos, y gestiona su ciclo de vida con rotación y revocación.
- Cambia de entorno por valores, buscando mantener el código inmutable. Feature Flags permiten despliegues graduales para que puedas probar cambios de alto riesgo con seguridad.
- Empezando hoy, unifica tus Settings, revisa los campos requeridos y prepara
.env.example. Con una base sólida, el desarrollo y la operación con FastAPI se vuelven sorprendentemente fluidos. ¡Estoy contigo!
