Módulo 5 · Arquitectura y código mantenible

Lección 27 — Configuración por entorno (12-factor)

Secretos y settings por entorno: la misma imagen, tres despliegues.

Publicada
En esta lección
  1. Objetivos
  2. 1. La regla: config ≠ código
  3. 2. Organización de settings
  4. 3. Los tres entornos de TicketFlow
  5. 4. Secretos y rotación dual
  6. 5. Fail-fast y validación al arrancar
  7. Autoevaluación

Stack: Django/DRF · Proyecto: TicketFlow Estado: Publicada — la misma imagen, tres despliegues Prerrequisito: Lección 26 — Errores centralizados


Objetivos

  1. Aplicar el principio 12-factor: toda configuración vive en el entorno, el código no cambia entre dev/staging/prod.
  2. Organizar settings de Django por entorno sin duplicar y sin if "prod" in hostname.
  3. Gestionar secretos con rotación dual (23) y validar la configuración al arrancar, no a las 3 a.m.

1. La regla: config ≠ código

El tercer principio 12-factor: la configuración es todo lo que varía entre despliegues (URL de BD, credenciales, DEBUG), y vive en el entorno — variables de ambiente — nunca en el código. La prueba: la misma imagen Docker (40) debe desplegarse en dev, staging y prod cambiando solo variables. Si tu Dockerfile tiene ENV DJANGO_SETTINGS_MODULE=prod_settings, viola el principio: la imagen ya sabe dónde vive.

Apariencia de violación que NO lo es: settings.DEBUG = False por defecto es un default seguro en código; la var DJANGO_DEBUG lo sobreescribe en dev. Defaults seguros en código + diferencias en entorno = la combinación correcta.

2. Organización de settings

Tres formatos en la naturaleza: paquete con splits, settings moduleado, y variables. El paquete de splits (base/dev/prod) encaja con el brain de Django, pero invita a imports cruzados que esconden qué varía de verdad. El más honesto para 12-factor: un solo settings.py que lee TODO del entorno con defaults seguros, sin branches por entorno:

python
# settings.py — un solo módulo, todo del entorno
import environ

env = environ.Env(
    DJANGO_DEBUG=(bool, False),                 # default SEGURO
    ALLOWED_HOSTS=(list, []),
    DATABASE_URL=(str, ""),
    CELERY_BROKER_URL=(str, ""),
    PAYMENT_GATEWAY_URL=(str, "https://sandbox.gw.example"),
)
SECRET_KEY = env("DJANGO_SECRET_KEY")           # sin default: si falta, explota al arrancar
DEBUG = env("DJANGO_DEBUG")
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS")
DATABASES = {"default": env.db("DATABASE_URL")}
CACHES = {"default": env.cache("REDIS_URL")}

La diferencia con if os.environ.get("IS_PROD"):...: aquí no hay branching, hay un diccionario plano de "toda la config que este proceso puede necesitar". El entorno decide valores; el código decide claves y tipos. Para proyectos sin dependencia extra: django-environ o pydantic-settings — ambos parsean tipos (DATABASE_URL a dict, listas separadas por coma) y fallan con error legible.

3. Los tres entornos de TicketFlow

VarDevStagingProd
DJANGO_DEBUGtruefalsefalse
ALLOWED_HOSTSlocalhost,127.0.0.1staging.ticketflow.devticketflow.com
DATABASE_URLpostgres://dev:dev@localhost/tfpostgres://staging:***@db-staging/tfpostgres://app:***@db-prod/tf
REDIS_URLlocalhost:6379redis-stagingredis-prod
PAYMENT_GATEWAY_URLsandboxsandboxproducción
DJANGO_SECRET_KEYcualquierasecreto stagingsecreto prod
FEATURE_WEBHOOKStruetruetrue

El commit que toca settings.py para "pasar a producción" es el commit que se olvida: con 12-factor, pasar a producción = desplegar la misma imagen con otro.env/vars del orquestador. El staging con las mismas variables QUE PROD pero apuntando a sandboxes (pasarela, SMTP) es la prueba de integración realista: lo que valida staging es la COINCIDENCIA de configuración, no el valor.

4. Secretos y rotación dual

La 23 dejó la política; aquí el mecánico: los secretos NO van al repo (el .gitignore de la 06 cubre .env), no van al log (el de acceso de la 45 filtra SECRET/TOKEN), y no van a la imagen Docker (un docker history los filtra — van al entorno en runtime o al gestor de secretos del orquestador, 42/43). Rotación dual: SECRET_KEY y PAYMENT_GATEWAY_KEY aceptan _OLD en settings:

python
SECRET_KEYS = [env("DJANGO_SECRET_KEY")] + (
    [env("DJANGO_SECRET_KEY_OLD")] if env.bool("DJANGO_SECRET_KEY_OLD", default=False) else []
)

Durante la rotación el proceso valida la nueva y acepta la vieja; después se elimina *_OLD (ventana corta, planificado, con la checklist de la 23). Idéntico patrón para claves de webhook de terceros (17): ambas activas durante la ventana.

5. Fail-fast y validación al arrancar

El peor momento de un error de config es el primer request de un cliente; el segundo peor es el deploy; el mejor es el arranque del proceso. Checklist de validación:

python
# settings.py (o un check al final del módulo)
if not DEBUG and not SECRET_KEY:
    raise ImproperlyConfigured("DJANGO_SECRET_KEY es obligatoria fuera de dev")
if not DEBUG and not ALLOWED_HOSTS:
    raise ImproperlyConfigured("ALLOWED_HOSTS vacío en producción: imposible servir")

Más sistemático: python manage.py check --deploy (la 22) en el pipeline de la 41, y un test de humo de settings que instancia el conjunto completo por entorno fake: la config rota no llega a producción porque un test falla, no porque un usuario escribió a soporte.


Autoevaluación

  1. ¿Qué convierte un valor en "configuración" 12-factor y cuál es la prueba de la misma imagen?
  2. Un solo settings.py sin branches vs splits base/dev/prod: ¿qué problema de legibilidad resuelve el primero?
  3. ¿Por qué el staging debe tener la MISMA ESTRUCTURA de variables que prod aunque los valores apunten a sandbox?
  4. Explica la rotación dual de secretos en settings y su ventana. ¿Qué pasa si olvidas eliminar *_OLD?
  5. ¿Cuál es el mejor momento para detectar config rota y cómo lo garantizas (checklist + pipeline)?

Continúa con los ejercicios. Las solutions.md solo tras intentarlo.