servicio-catalogo ya arranca, pero su src/config.js es una versión mínima que lee cuatro variables y confía en que estén bien. En un sistema con siete servicios, tres entornos (desarrollo, staging, producción) y varias réplicas por servicio, la configuración deja de ser un detalle: es lo que hace que el mismo artefacto (la misma imagen, el mismo código) se comporte de forma distinta en cada sitio sin recompilar nada, y es también la vía más habitual por la que se filtran contraseñas. Esta lección fija cómo un servicio de TechCorp obtiene, valida y usa su configuración: qué es configuración y qué no, de dónde viene y con qué precedencia, un módulo config.js con validación fail-fast que reutilizarán todos los servicios (lo escribimos para servicio-pedidos, que en 04-04 lo necesita entero), cómo se tratan los secretos, cuándo compensa una configuración centralizada, y qué son y cómo se leen los feature flags.

Contenido

  1. Configuración frente a código: el principio de los 12 factores
  2. Tipos de configuración: por entorno, por instancia y feature flags
  3. Fuentes y precedencia
  4. src/config.js: cargar y validar al arrancar
  5. .env en desarrollo y secretos en el resto de entornos
  6. Configuración centralizada: cuándo compensa
  7. Recarga en caliente frente a reinicio
  8. Feature flags con un ejemplo mínimo
  9. La configuración de TechCorp por entorno y su enganche con Kubernetes

  1. Configuración frente a código: el principio de los 12 factores

La regla del tercer factor de la metodología Twelve-Factor App es la que seguimos: la configuración vive en el entorno, no en el código. La prueba práctica para distinguirlas: ¿este valor cambia entre desarrollo, staging y producción, o entre dos instalaciones del mismo servicio? Si sí, es configuración; si no, es código.

Es configuración (varía por entorno) Es código (no varía)
URL de la base de datos, del broker, de otros servicios (PEDIDOS_DB_URL, RABBITMQ_URL, CATALOGO_URL) El nombre del exchange techcorp.eventos y de la cola pedidos.saga (son parte del contrato de 03-02, iguales en todos los entornos)
Puerto de escucha, nivel de log, timeouts La máquina de estados del pedido, las reglas de validación
Credenciales, claves de API, certificados Los códigos de error (CLIENTE_NO_EXISTE) y las rutas (/v1/pedidos)
Feature flags (CATALOGO_REMOTO, PAGO_NUEVO_PROVEEDOR) El límite de 100 ids por lote (regla del contrato, 03-01)

El corolario que más cuesta interiorizar: la misma imagen Docker se ejecuta en los tres entornos. Si para pasar a producción hay que reconstruir con otro fichero de propiedades dentro, no se ha probado en staging lo que se despliega. La imagen se construye una vez (05-01, 05-03) y la configuración se inyecta al arrancar.

  1. Tipos de configuración: por entorno, por instancia y feature flags

Tipo Ejemplos en TechCorp Quién la cambia y con qué frecuencia
Por entorno URLs, credenciales, LOG_NIVEL, TIMEOUT_HTTP_MS Plataforma, al crear o cambiar el entorno; rara vez
Por instancia PUERTO (en local, si se ejecutan dos servicios a la vez), HOSTNAME/nombre del pod para los logs, número de workers El orquestador, en cada réplica; el servicio la lee, no la decide
Feature flags CATALOGO_REMOTO (leer productos del nuevo servicio o del monolito, 02-04), PAGO_NUEVO_PROVEEDOR Los equipos de producto/desarrollo, con frecuencia y sin desplegar

Los tres tipos entran por la misma puerta (variables de entorno) al principio; la diferencia está en la frecuencia de cambio, y eso es lo que más adelante justifica un servicio de flags separado (apartado 8).

  1. Fuentes y precedencia

Un servicio suele poder recibir su configuración de varios sitios a la vez. Para que el comportamiento sea predecible hay que fijar una precedencia y no salirse de ella:

flowchart LR
    A[1. Valores por defecto<br/>en config.js] --> B[2. Fichero .env<br/>solo en desarrollo]
    B --> C[3. Variables de entorno<br/>del proceso]
    C --> D[4. Secretos montados<br/>como fichero o variable]
    D --> E[Objeto config<br/>validado e inmutable]
    style E fill:#dfd,stroke:#393
  • Valores por defecto: solo para lo que es seguro en cualquier entorno (PUERTO=3002, LOG_NIVEL=info, TIMEOUT_HTTP_MS=2000). Nunca una URL de base de datos por defecto que apunte a algún sitio real, y nunca una credencial.
  • Fichero .env: comodidad de desarrollo (04-02). En producción no existe.
  • Variables de entorno: la interfaz universal. Docker, Docker Compose, Kubernetes, systemd, GitHub Actions... todos saben ponerlas.
  • Secretos: técnicamente llegan también como variables o como ficheros montados; los distinguimos porque su ciclo de vida (quién los ve, cómo rotan) es distinto (apartado 5).

Lo que no está en la lista: argumentos de línea de comandos (mezclan configuración con arranque) y ficheros de configuración por entorno dentro de la imagen (config/produccion.json), que violan el apartado 1.

  1. src/config.js: cargar y validar al arrancar

El objetivo es fallar rápido y con un mensaje claro si falta algo o tiene un tipo incorrecto, en lugar de arrancar y descubrir a las tres de la mañana que TIMEOUT_HTTP_MS era la cadena "2s" y que fetch la interpretó como NaN. Usamos zod, ya presente en el servicio desde 04-02. Este es el config.js completo de servicio-pedidos, con las variables acordadas en 03-05:

// src/config.js (servicio-pedidos)
const { z } = require('zod');

// 1. El esquema: nombre, tipo, obligatoriedad y valor por defecto de CADA variable que el servicio usa.
//    Es también su documentación: si no está aquí, el servicio no la lee.
const esquema = z.object({
  NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
  PUERTO: z.coerce.number().int().min(1).max(65535).default(3002),      // coerce: las variables de entorno siempre son texto
  LOG_NIVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),

  // Dependencias propias (sin valor por defecto: obligatorias)
  PEDIDOS_DB_URL: z.string().url().startsWith('postgres'),              // postgres://usuario:clave@host:5432/pedidos
  RABBITMQ_URL: z.string().url().startsWith('amqp'),                    // amqp://rabbitmq:5672

  // Servicios que Pedidos llama de forma síncrona (03-01, 03-05)
  CATALOGO_URL: z.string().url(),                                       // http://servicio-catalogo:3001
  CLIENTES_URL: z.string().url(),                                       // http://servicio-clientes:3004
  TIMEOUT_HTTP_MS: z.coerce.number().int().min(100).max(30000).default(2000),

  // Relay del outbox (04-04)
  OUTBOX_INTERVALO_MS: z.coerce.number().int().min(50).default(500),

  // Feature flag heredado del branch by abstraction de 02-04:
  // true = leer productos de servicio-catalogo; false = del monolito (vía CATALOGO_URL apuntando a él)
  CATALOGO_REMOTO: z.enum(['true', 'false']).default('true').transform((v) => v === 'true')
});

// 2. Cargar y validar. safeParse no lanza: nos deja construir un mensaje legible.
function cargarConfig(entorno = process.env) {
  const resultado = esquema.safeParse(entorno);
  if (!resultado.success) {
    const problemas = resultado.error.issues.map((i) => `  - ${i.path.join('.')}: ${i.message}`).join('\n');
    // Fail-fast: sin configuración válida no hay servicio. Kubernetes verá el pod en CrashLoopBackOff con este mensaje.
    throw new Error(`Configuración inválida:\n${problemas}`);
  }
  // 3. Inmutable: nadie puede cambiar config.PUERTO en tiempo de ejecución "para probar"
  return Object.freeze(resultado.data);
}

// 4. Se carga UNA vez al importar el módulo. Los tests llaman a cargarConfig() con un objeto propio.
const config = cargarConfig();

// 5. Versión segura para logs y para /health: los secretos, enmascarados
function configParaLog(c = config) {
  const ocultar = (url) => url.replace(/\/\/([^:]+):([^@]+)@/, '//$1:***@');   // postgres://svc:***@host/pedidos
  return { ...c, PEDIDOS_DB_URL: ocultar(c.PEDIDOS_DB_URL), RABBITMQ_URL: ocultar(c.RABBITMQ_URL) };
}

module.exports = { config, cargarConfig, configParaLog };

Cómo se usa desde servidor.js (y solo desde ahí y desde los módulos de infraestructura: las rutas y el dominio no leen process.env jamás):

// src/servidor.js (servicio-pedidos, fragmento)
const { config, configParaLog } = require('./config');    // si la config es inválida, este require lanza y el proceso muere aquí
const logger = crearLogger({ servicio: 'servicio-pedidos', nivel: config.LOG_NIVEL });
logger.info({ config: configParaLog() }, 'configuración cargada');

const pool = crearPoolPostgres({ url: config.PEDIDOS_DB_URL });
const catalogoCliente = crearCatalogoCliente({ urlBase: config.CATALOGO_URL, timeoutMs: config.TIMEOUT_HTTP_MS });

Y lo que ocurre si alguien despliega sin RABBITMQ_URL y con un puerto imposible:

Error: Configuración inválida:
  - RABBITMQ_URL: Required
  - PUERTO: Number must be less than or equal to 65535

Cinco decisiones que conviene entender:

  1. z.coerce porque todas las variables de entorno son cadenas: PUERTO="3002" debe convertirse en el número 3002, y CATALOGO_REMOTO="true" en el booleano true. Sin conversión explícita, if (process.env.CATALOGO_REMOTO) es verdadero también para "false".
  2. Obligatorio sin valor por defecto para todo lo que apunta a algo real. Un PEDIDOS_DB_URL por defecto que apunte a localhost "funciona" en el portátil y arranca en producción contra ninguna parte, con un error confuso minutos después.
  3. Validación en el arranque, no en el primer uso: es preferible un pod que no arranca (y que Kubernetes marca como fallido de inmediato) a uno que arranca, pasa el readiness y falla al primer pedido.
  4. Object.freeze para que la configuración sea un valor, no un estado mutable global.
  5. Las dependencias reciben valores, no leen process.env: crearCatalogoCliente({ urlBase, timeoutMs }). Es lo que permite, en 04-05, probar el cliente contra un servidor falso en otro puerto sin manipular el entorno del proceso.

  1. .env en desarrollo y secretos en el resto de entornos

En desarrollo, el fichero .env (cargado por dotenv en npm run dev, 04-02) contiene los valores locales. Dos reglas fijas:

  • .env está en .gitignore. Siempre. Aunque "solo tenga contraseñas de desarrollo": lo que hoy es dev-pedidos mañana es una copia de la de producción pegada por prisas.
  • .env.ejemplo se versiona: mismas claves, valores de ejemplo o vacíos, comentarios. Es la documentación viva de qué necesita el servicio y lo primero que copia un desarrollador nuevo.
# .env.ejemplo (servicio-pedidos) — copiar a .env y rellenar. NUNCA poner valores reales aquí.
NODE_ENV=development
PUERTO=3002
LOG_NIVEL=debug
PEDIDOS_DB_URL=postgres://svc_pedidos:dev-pedidos@localhost:5432/pedidos
RABBITMQ_URL=amqp://localhost:5672
CATALOGO_URL=http://localhost:3001
CLIENTES_URL=http://localhost:3004
TIMEOUT_HTTP_MS=2000
OUTBOX_INTERVALO_MS=500
CATALOGO_REMOTO=true

En staging y producción no hay .env. Los valores no sensibles los pone el orquestador como variables de entorno; los secretos (contraseñas de BD, credenciales del broker, claves de la pasarela de pago) siguen tres reglas:

Regla Qué significa en la práctica
Nunca en el repositorio Ni en .env, ni en YAML de Kubernetes en claro, ni en el historial de git (un secreto que se subió y se borró sigue en el historial: hay que rotarlo)
Nunca en la imagen Ni COPY .env, ni ENV PASSWORD=... en el Dockerfile; cualquiera con acceso al registro de imágenes lo leería
Inyectados por el entorno en tiempo de ejecución El servicio los recibe como variable de entorno o como fichero montado en una ruta conocida; no sabe ni le importa de dónde salen

Ese último punto es la interfaz entre el servicio y la gestión de secretos, y es todo lo que un desarrollador de Pedidos necesita saber: PEDIDOS_DB_URL llegará como variable de entorno. Quien la pone puede ser un Secret de Kubernetes (05-02), Vault con inyección en el pod, o el gestor de secretos de la nube (07-04 profundiza). Si en algún caso el secreto llega como fichero (práctica habitual con Vault: /vault/secrets/pedidos-db), el config.js se amplía con una regla sencilla: si existe PEDIDOS_DB_URL_FILE, se lee el fichero y su contenido pasa a ser PEDIDOS_DB_URL. Esa convención <VARIABLE>_FILE es la que usan las imágenes oficiales de PostgreSQL o RabbitMQ, y la adopta la plantilla de TechCorp.

  1. Configuración centralizada: cuándo compensa

Cuando hay decenas de servicios y muchos valores compartidos (por ejemplo, la URL del broker, que es la misma para todos), algunas organizaciones montan un servidor de configuración del que cada servicio lee al arrancar (o al que se suscribe):

Herramienta Modelo Fortaleza Coste
Spring Cloud Config Servidor HTTP que sirve propiedades desde git Integración perfecta con Spring; historial en git Solo tiene sentido en ecosistemas Java/Spring
Consul KV Almacén clave-valor distribuido con watch Cambios en caliente; se combina con el descubrimiento de 03-05 Una pieza más que operar y asegurar
etcd Clave-valor con consistencia fuerte Es lo que usa el propio Kubernetes Raro usarlo directamente desde aplicaciones
AWS Parameter Store / Secrets Manager, Azure App Configuration, GCP Secret Manager Servicio gestionado Nada que operar; auditoría y rotación incluidas Acoplamiento a la nube; latencia y cuotas de lectura
ConfigMap + Secret de Kubernetes Objetos del clúster inyectados como variables o ficheros Ya está ahí; declarativo; por namespace Sin recarga (apartado 7); no es un gestor de secretos "de verdad" sin cifrado adicional

Cuándo compensa un servidor de configuración: cuando cambian valores con frecuencia y en muchos servicios a la vez (umbrales, feature flags, límites de tarifa), o cuando el mismo valor debe estar sincronizado en decenas de sitios y editar veinte ConfigMaps es un riesgo. Cuándo no: con siete servicios, tres entornos y valores que cambian pocas veces al año, es una pieza más que puede caer y que todos los servicios necesitan al arrancar (si el servidor de configuración no responde, nada arranca).

Decisión de TechCorp: variables de entorno como interfaz única del servicio; en Kubernetes, ConfigMap para valores no sensibles y Secret para secretos (05-02). Los feature flags, si crecen, irán a un servicio de flags (apartado 8), no a un servidor de configuración general.

  1. Recarga en caliente frente a reinicio

Un servidor de configuración o un ConfigMap montado como fichero permiten que un servicio detecte cambios y los aplique sin reiniciar. Suena atractivo, pero tiene coste:

Aspecto Recarga en caliente Reinicio (nueva configuración = nuevo despliegue)
Complejidad en el código Alta: hay que gestionar qué partes recargar (¿se puede cambiar PEDIDOS_DB_URL con un pool abierto?), atomicidad, valores a medio aplicar Ninguna: el proceso arranca con la config nueva y punto
Trazabilidad Difícil saber qué configuración tiene cada réplica en cada momento Cada despliegue queda registrado; todas las réplicas convergen
Validación Hay que validar en caliente y decidir qué hacer si el nuevo valor es inválido El fail-fast del apartado 4: si es inválida, el nuevo pod no arranca y el antiguo sigue
Interrupción Ninguna Ninguna, si el despliegue es rolling (05-04): las réplicas se sustituyen una a una

Por eso en Kubernetes la práctica habitual es reiniciar: un cambio de configuración es un rollout más, con la misma seguridad y el mismo rollback que un cambio de código. La excepción son los feature flags, cuyo valor debe cambiar en segundos y sin despliegue: para ellos, y solo para ellos, se acepta la lectura dinámica.

  1. Feature flags con un ejemplo mínimo

Un feature flag es un valor de configuración que activa o desactiva un comportamiento en tiempo de ejecución. Sirve para desplegar código apagado, encenderlo gradualmente y apagarlo en segundos si algo va mal (05-04 lo relacionará con los despliegues canary). Ya usamos uno en 02-04, CATALOGO_REMOTO, y aquí añadimos el que necesitará Pagos: PAGO_NUEVO_PROVEEDOR.

Versión mínima, leída de la configuración (cambiarlo requiere reinicio):

// src/flags.js (servicio-pagos) — versión 1: flags como configuración
function crearFlags(config) {
  return {
    // Devuelve una función para no acoplar el resto del código a "de dónde sale" el flag
    estaActivo: (nombre) => config.FLAGS[nombre] === true
  };
}
// Uso en el caso de uso de cobro (Pagos): el código nuevo y el viejo conviven; el flag decide
async function cobrar(pedido, { flags, pasarelaActual, pasarelaNueva }) {
  const pasarela = flags.estaActivo('PAGO_NUEVO_PROVEEDOR') ? pasarelaNueva : pasarelaActual;
  return pasarela.cobrar({ importe: pedido.total, claveIdempotencia: pedido.pedidoId });   // 02-05
}

Cuando los flags se multiplican, se cambian varias veces al día o se quieren activar solo para un porcentaje de usuarios, se pasa a un servicio de flags (Unleash, Flagsmith, LaunchDarkly, o el módulo de flags de GitLab). El servicio consulta el estado por SDK, con caché local y valor por defecto si el servicio de flags no responde. Lo importante es que la interfaz estaActivo(nombre) no cambia: solo cambia la implementación de crearFlags, y el caso de uso cobrar ni se entera:

// src/flags.js — versión 2: flags desde Unleash (concepto; SDK real: unleash-client)
function crearFlags({ clienteUnleash, porDefecto = {} }) {
  return { estaActivo: (nombre, contexto) => clienteUnleash.isEnabled(nombre, contexto, porDefecto[nombre] ?? false) };
}

Dos advertencias: los flags deben tener fecha de caducidad (un flag que lleva un año en true es código muerto disfrazado), y su valor por defecto en producción debe ser el comportamiento seguro (el proveedor de pago actual, no el nuevo).

  1. La configuración de TechCorp por entorno y su enganche con Kubernetes

Con todo lo anterior, la tabla que el equipo de Plataforma mantiene para servicio-pedidos (valores ficticios; en producción los secretos ni siquiera están en esta tabla, solo el nombre del Secret que los contiene):

Variable Desarrollo (.env) Staging Producción Fuente en Kubernetes
NODE_ENV development production production ConfigMap
PUERTO 3002 3002 3002 ConfigMap
LOG_NIVEL debug info info ConfigMap
PEDIDOS_DB_URL postgres://svc_pedidos:dev-pedidos@localhost:5432/pedidos postgres://svc_pedidos:stg-Xk3…@pg-staging:5432/pedidos (en el Secret pedidos-db) Secret
RABBITMQ_URL amqp://localhost:5672 amqp://pedidos:stg-Rq7…@rabbitmq:5672 (en el Secret pedidos-rabbitmq) Secret
CATALOGO_URL http://localhost:3001 http://servicio-catalogo:3001 http://servicio-catalogo:3001 ConfigMap
CLIENTES_URL http://localhost:3004 http://servicio-clientes:3004 http://servicio-clientes:3004 ConfigMap
TIMEOUT_HTTP_MS 2000 2000 2000 ConfigMap
OUTBOX_INTERVALO_MS 500 500 250 ConfigMap
CATALOGO_REMOTO true true falsetrue durante la migración ConfigMap (o servicio de flags)

Fíjate en que CATALOGO_URL vale lo mismo en staging y producción: es el nombre DNS del Service de Kubernetes (03-05), resuelto dentro de cada namespace. Solo cambian las credenciales y algún ajuste de rendimiento.

Cómo se conecta esto con Kubernetes, sin escribir aún los YAML (05-02): un ConfigMap llamado servicio-pedidos-config contiene las claves no sensibles; un Secret llamado pedidos-db contiene PEDIDOS_DB_URL; el Deployment de Pedidos declara "inyecta todas las claves de ese ConfigMap y de ese Secret como variables de entorno" (envFrom). Desde dentro del contenedor, process.env.PEDIDOS_DB_URL existe y config.js la valida exactamente igual que en el portátil. El servicio no distingue el origen, y esa indiferencia es el objetivo de toda la lección.

Errores Comunes y Consejos

  • Configuración en el código (const CATALOGO_URL = 'http://servicio-catalogo:3001'). Funciona hasta que hay que apuntar a otro sitio en pruebas o en un segundo entorno. Todo lo que varía, por variable de entorno; todo, por config.js.
  • Valores por defecto peligrosos: PEDIDOS_DB_URL por defecto a localhost, NODE_ENV por defecto a development con logs verbosos y stack traces al cliente en producción. Por defecto solo lo inocuo.
  • process.env repartido por todo el código. Cada process.env.X fuera de config.js es una variable sin validar, sin documentar y sin valor por defecto claro. Una sola puerta de entrada.
  • Secretos en los logs. logger.info({ config }) con la URL completa de PostgreSQL manda la contraseña a Loki. configParaLog() siempre.
  • Secretos en git "solo esta vez". Rotación obligatoria; borrarlos del último commit no basta.
  • Booleanos como cadenas. if (process.env.CATALOGO_REMOTO) es siempre verdadero. transform((v) => v === 'true').
  • Recargar configuración en caliente por si acaso. Complejidad sin necesidad; un rollout es más seguro y auditable. Dinámico solo para flags.
  • Flags eternos. Cada flag con dueño, propósito y fecha de retirada en la propia definición.

Ejercicios

Ejercicio 1. Escribe el esquema zod de src/config.js para servicio-catalogo (04-02) con las variables PUERTO (por defecto 3001), MONGO_URL (obligatoria, debe empezar por mongodb), MONGO_BD (por defecto catalogo), LOG_NIVEL y NODE_ENV. Añade la variable CACHE_MAX_AGE_S (segundos del Cache-Control, entero entre 0 y 3600, por defecto 30) y explica en una frase por qué esta sí es configuración y no código.

Ejercicio 2. Un desarrollador de Pedidos propone que, si CATALOGO_URL no está definida, el servicio use http://servicio-catalogo:3001 por defecto "porque siempre es esa". Da un argumento a favor, dos en contra, y decide.

Ejercicio 3. Implementa la convención <VARIABLE>_FILE del apartado 5 en cargarConfig: antes de validar, para cada variable del esquema, si existe NOMBRE_FILE y no existe NOMBRE, lee el fichero (síncrono, UTF-8, sin salto de línea final) y usa su contenido como valor.

Soluciones

Solución 1.

const esquema = z.object({
  NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
  PUERTO: z.coerce.number().int().min(1).max(65535).default(3001),
  LOG_NIVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
  MONGO_URL: z.string().url().startsWith('mongodb'),          // obligatoria: sin valor por defecto
  MONGO_BD: z.string().min(1).default('catalogo'),
  CACHE_MAX_AGE_S: z.coerce.number().int().min(0).max(3600).default(30)
});

CACHE_MAX_AGE_S es configuración porque su valor óptimo depende del entorno y del momento (en una campaña con cambios de precio frecuentes se bajaría a 5 s sin desplegar código); el hecho de que el lote lleve Cache-Control sí es contrato y por tanto código.

Solución 2. A favor: comodidad; en Kubernetes el nombre es estable (03-05) y evita una variable en cada ConfigMap. En contra: (1) en desarrollo local ese nombre no resuelve, y el error ("ENOTFOUND servicio-catalogo") aparece en la primera petición, no al arrancar; (2) durante la migración con CATALOGO_REMOTO=false, CATALOGO_URL debe apuntar al monolito, y un valor por defecto "correcto" esconde que alguien olvidó configurarla en un entorno. Decisión: obligatoria, sin valor por defecto; el ConfigMap la lleva explícita. Es una línea más de YAML a cambio de un fallo claro en el arranque.

Solución 3.

const fs = require('node:fs');

function resolverFicherosDeSecretos(entorno) {
  const resultado = { ...entorno };
  for (const nombre of Object.keys(esquema.shape)) {               // solo las variables que el esquema conoce
    const rutaFichero = entorno[`${nombre}_FILE`];
    if (rutaFichero && resultado[nombre] === undefined) {
      resultado[nombre] = fs.readFileSync(rutaFichero, 'utf8').replace(/\r?\n$/, '');
    }
  }
  return resultado;
}

function cargarConfig(entorno = process.env) {
  const resultado = esquema.safeParse(resolverFicherosDeSecretos(entorno));
  // ... igual que antes
}

Con esto, montar el secreto de Vault en /vault/secrets/pedidos-db y definir PEDIDOS_DB_URL_FILE=/vault/secrets/pedidos-db funciona sin tocar el resto del servicio; y si el fichero no existe, readFileSync lanza al arrancar, que es lo que queremos.

Conclusión

La configuración de un microservicio de TechCorp queda así: vive en el entorno, no en el código ni en la imagen; llega por variables de entorno (o ficheros montados, con la convención _FILE) con una precedencia clara (valores por defecto inocuos → .env solo en desarrollo → entorno → secretos); entra por una única puerta, src/config.js, que la valida con zod al arrancar y falla rápido con un mensaje legible, la congela y la reparte a las dependencias como valores; los secretos nunca tocan el repositorio ni la imagen y el servicio no sabe de dónde salen (Kubernetes Secret, Vault: 05-02 y 07-04); la configuración centralizada se reserva para cuando el volumen de cambios lo justifique; los cambios se aplican reiniciando salvo los feature flags, que tienen su interfaz estaActivo(nombre) y, si crecen, su servicio; y la tabla por entorno de servicio-pedidos fija los valores concretos de PUERTO, PEDIDOS_DB_URL, RABBITMQ_URL, CATALOGO_URL, CLIENTES_URL, TIMEOUT_HTTP_MS, OUTBOX_INTERVALO_MS y CATALOGO_REMOTO.

Ese config.js no es un ejercicio teórico: es el primer fichero de servicio-pedidos, el servicio más complejo de TechCorp. En la siguiente lección construimos el resto sobre la plantilla de 04-02: el pool de PostgreSQL y las migraciones, los clientes HTTP hacia Catálogo y Clientes con su timeout y su ACL, el caso de uso crearPedido con Idempotency-Key y outbox en la misma transacción, el relay que publica en RabbitMQ y los consumidores de la saga que mueven el pedido de PENDIENTE a CONFIRMADO. Es la lección en la que todo lo diseñado desde el módulo 2 se convierte en código que se ejecuta de punta a punta.

Curso de Microservicios

Módulo 1: Introducción a los Microservicios

Módulo 2: Diseño de Microservicios

Módulo 3: Comunicación entre Microservicios

Módulo 4: Implementación de Microservicios

Módulo 5: Despliegue y Orquestación

Módulo 6: Monitoreo y Mantenimiento

Módulo 7: Seguridad en Microservicios

Módulo 8: Casos de Estudio y Ejemplos Prácticos

© Copyright 2026. Todos los derechos reservados