green snake
Photo by Pixabay on Pexels.com
目次

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 .env como 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

  1. Crear una única clase de settings como fuente única de la verdad de la aplicación.
  2. Precedencia de carga: “Variables de entorno > .env > Valores por defecto”.
  3. Secretos en producción provenientes de fuentes de Secretos externas (variables de entorno o montajes de archivos).
  4. Realizar validación y transformación en la clase de settings (fallar inmediatamente en el arranque).
  5. 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 Literal y tipos Dsn.
  • Usa repr=False para evitar registrar secretos.
  • Usa field_validator para 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.

  1. Variables de entorno del proceso (p. ej., ENV=prod)
  2. .env (por lo general no lo incluyas; si lo haces, solo una plantilla)
  3. Valores por defecto (valores iniciales en el código)

Consejos prácticos

  • Proporciona un .env.example en el repo, marcando explícitamente los campos requeridos con comentarios.
  • No uses .env en producción; inyecta mediante secretos gestionados como variables de entorno.
  • No des valores por defecto a campos requeridos (haz secret_key obligatorio, 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 .env cuando 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: bool a 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, establece ENV=dev e inyecta una BD solo para tests y secret_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 con docker run -e KEY=... o environment en compose.
  • Para secretos, usa docker secret o 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 ConfigMap y los secretos en Secret.
  • 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

  1. Introduce una única clase de Settings y prepara .env.example. No ofrezcas valores por defecto para elementos requeridos.
  2. Inyecta valores multi-entorno mediante variables de entorno; haz que la app solo consuma valores.
  3. Añade validación y comprobaciones de consistencia. Para CORS, BD y claves de API externas, falla al arranque cuando estén mal.
  4. Traslada la gestión de secretos a un Administrador de Secretos o a Secrets de K8s. Establece procedimientos de rotación.
  5. Introduce Feature Flags y despliegue gradual para lanzamientos seguros.
  6. Integra inyección de settings y validación de esquema en CI/CD para prevenir accidentes por diferencias.

Referencias


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!

por greeden

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)