Diseñé, construí y opero solo un ecosistema SaaS multi-tenant en producción, con clientes reales que pagan. Esta página muestra los números medidos, cómo ocurre una publicación y — la parte que importa — los incidentes que tumbaron el sistema y qué cambió después de cada uno.
Esta tabla existe para que no tengas que leer la página entera. La columna que más importa es la última: medir es lo que separa operar de suponer. Y donde solo tengo formación, no práctica, está dicho — quien marca todo no informa nada.
No es un laboratorio. Hay clientes pagando, y cuando esto cae alguien deja de vender. Todo lo demás de esta página existe por esa frase.
ARM64 en nube pública, 8 vCPU y 30 GB cada uno, detrás de un túnel Cloudflare. Los dos sirven tráfico: no existe un nodo ocioso esperando el desastre.
Row-Level Security en PostgreSQL, fail-closed: sin el alcance del cliente en la sesión, la consulta devuelve cero filas. El rol de runtime no tiene BYPASSRLS.
Cobertura mínima que reprueba el build: 95% en dos servicios, 90,3% en otro. No es una meta de pared — es condición para publicar.
Un número sin procedencia es una opinión con cara de dato. Cada línea de abajo dice qué se contó, cómo, cuándo — y qué NO prueba la medición. La última columna es la que me interesa: una medición honesta declara su propio límite.
docker ps en los dos nodos: 112 cada uno. De esos 112, 59 son procesos de aplicación (web, trabajador, publicador, consumidor, programador); los otros 53 son bus, datos, observabilidad, borde y bóveda
no es medida de carga ni de utilización — es conteo de procesos“Muéstrame el diagrama y desconfío. Muéstrame qué pasa cuando falla y te creo.”
lo que esta página intenta responderEl dibujo completo: por dónde entra el tráfico, dónde corre cada pieza, cómo se llega al dato — y qué pasa cuando cada parte falla.
Dos vistas que vienen antes del dibujo de implantación. La primera responde “quién usa esto y con qué mundo externo habla”. La segunda responde “qué aplicaciones existen y cómo conversan” — sin ninguna infraestructura, a propósito: mezclar las dos cosas en un solo diagrama es el error que vuelve ilegibles a la mayoría.
Los 224 contenedores no son 224 copias de lo mismo. Contados por papel, dibujan la plataforma — y cada capa existe porque algún dolor la pidió.
Ninguno de los dos nodos expone puerto público. El tráfico entra por túnel Cloudflare (conector de salida) y cae en Traefik, que hace enrutamiento por host, TLS automático y sirve de punto de balanceo. Drenar un nodo del túnel es lo que hace posible la publicación canaria: así se saca un nodo del balanceo sin tumbar nada.
PostgreSQL en host dedicado, compartido por las verticales, con HAProxy como punto único de acceso y pgbouncer provisionado pero todavía fuera del camino (medido: cero clientes activos; las aplicaciones van directo a HAProxy) — entra cuando el número de conexiones lo pida. Son dos instancias: primaria y réplica caliente. El aislamiento de cliente es Row-Level Security, no WHERE tenant_id esparcido por el código: el rol de runtime no tiene BYPASSRLS, así que olvidar el alcance devuelve cero filas en vez de filtrar datos.
Honestidad sobre el objetivo: no hay failover automático de base de datos. La promoción es manual y documentada — y prefiero decirlo a dibujar un Patroni que no existe.
Los servicios no se llaman en cadena: publican hechos. El camino es outbox transaccional (el hecho commitea junto con el dato, o ninguno de los dos) → relay → Redis Streams → inbox con dedup por grupo de consumo → DLQ para lo que falló. Son 14 instancias de Redis: el bus es por vertical y por nodo, y justamente por eso la convergencia se resuelve en Postgres, no en un lock distribuido que no cruzaría los nodos.
.envLas credenciales viven en bóveda (OpenBao) con token de servicio por consumidor. Hay una alerta dedicada para token de la bóveda por expirar — porque un secreto que vence en silencio tumba todo de una vez, en el peor horario.
La regla es publicar hecho, no llamar servicio. El camino por defecto es asíncrono: outbox transaccional (el hecho solo existe si el dato existe), relay, cola, inbox con deduplicación por grupo de consumo y DLQ para lo que falló. Eso significa que un servicio caído atrasa al otro — no lo tumba con él.
Cuando la respuesta se necesita ahora — “¿este cliente tiene derecho a esta funcionalidad?”, “¿esta empresa es hija de aquella?” — la llamada es síncrona, pero nunca es HTTP suelto esparcido por el código: pasa por un cliente tipado del SDK único, con operaciones nombradas y autenticación máquina a máquina por clave propia de cada servicio. La autoridad sobre plan, jerarquía y derecho de uso tiene un solo dueño; los demás preguntan, no deducen.
Cuando ocurre una venta, el servicio de ventas no avisa a nadie. Publica el hecho — y doce grupos de consumo independientes lo leen al ritmo de cada uno. Inventario descuenta la pieza, finanzas genera el cobro, fiscal prepara el documento, contabilidad asienta. Ninguno sabe que los otros existen, y uno de ellos caído atrasa su propia cola, no la venta.
Por nodo: 13 trabajadores, 11 publicadores, 10 consumidores y 10 programadores. Cada uno corre con concurrencia 1, declarada en el archivo de composición. El valor por defecto de la herramienta es 8 — y así fue como la flota cayó una vez. Límite que no está escrito no existe.
El trabajo de IA, de análisis y de rutina tienen colas propias. Una llamada de modelo que tarda treinta segundos no puede sentarse delante de la cola que confirma una venta. Separar es lo que impide que lo lento atrase lo urgente.
El bus escribe en disco. Si un nodo cae con trabajo en cola, ese trabajo no se evapora — espera a que el nodo vuelva y retoma. La diferencia entre “el cliente esperó” y “el pedido desapareció” es esta línea de configuración.
Prometheus raspando 9 objetivos (los dos nodos, cAdvisor, exportador de PostgreSQL, Traefik, sondas HTTP), Grafana, Alertmanager, cAdvisor, node y postgres exporter, blackbox sondeando rutas de fuera hacia dentro. 12 reglas de alerta en el aire.
Lo que considero la parte madura no es la cantidad — es qué vigilan. Tres de ellas no preguntan “¿algo se rompió?”, preguntan “¿algo dejó de ocurrir?”: backup off-site vacío, backup atrasado, backup sin la base dentro. Una bóveda de backup que existe pero está vacía pasa por saludable en cualquier verificación de presencia — y solo se descubre el día del restore. Las otras cubren caída de PostgreSQL, disco llenándose en cada nodo, nodo fuera del aire, slot de WAL acumulando y contenedor en ciclo de reinicio.
“La mejor herramienta es la planificación.”
por qué cada pieza está donde estáToda elección aquí tiene una ganancia, un costo que yo pago y una consecuencia comercial. Quien solo lista la ganancia está vendiendo, no diseñando.
El presupuesto de cómputo entero son dos nodos de 8 vCPU y 30 GB, y ya corren 224 contenedores. Un plano de control de Kubernetes — servidor de API, etcd, planificador, controladores, red superpuesta, controlador de entrada — consume CPU y memoria del mismo presupuesto, de forma sostenida. Y la carga sostenida de CPU es exactamente lo que ya me costó dos suspensiones de infraestructura.
Como decisión de negocio: Kubernetes resuelve un problema de escala que todavía no tengo, cobrando hoy en complejidad operativa y en consumo del recurso más escaso que tengo. Mientras la carga quepa en dos nodos, adoptarlo sería pagar por adelantado un seguro contra un riesgo que no existe — y crear uno nuevo y real: una superficie que no puedo sostener solo en guardia. Esto no es rechazar la herramienta; es no cambiar de herramienta por moda. El día en que la cuenta cambie — más de un operador, o carga que no quepa en dos nodos — la migración ya está diseñada, y es barata justamente porque todo ya son imágenes de contenedor y configuración declarativa.
Como decisión de negocio: un balanceador gestionado cuesta por hora y por volumen, y encima me devuelve una IP pública que defender. El túnel cuesta cero en el plan en uso, elimina la superficie expuesta y entrega filtro de aplicación junto. Cambié un riesgo que tendría que operar (exposición) por un riesgo que monitoreo (disponibilidad de terceros) — y este segundo tiene sonda externa y alerta.
Como decisión de negocio: el failover automático sin capa de consenso y aislamiento del nodo defectuoso cambia una parada rara y recuperable por una corrupción silenciosa y permanente. Para quien guarda dato financiero de terceros, ese cambio es malo en cualquier escenario. La mitigación no es automatizar la promoción: es que la réplica esté caliente y al día, que el procedimiento esté escrito, y que exista recuperación a punto en el tiempo — todo medido.
Como decisión de negocio: la reserva ociosa es un costo que solo paga en el peor día — y que suele fallar justo entonces, porque nadie la ejercita. Con los dos activos, el mismo dinero compra capacidad y resiliencia, y la prueba de que funciona es el tráfico de ayer. Honestidad sobre el límite: hoy los dos están en el mismo dominio de disponibilidad, así que un fallo correlacionado de dominio todavía me tumba. Está marcado en el diagrama porque es el próximo paso, no porque no lo vea.
Como decisión de negocio: el valor de la puerta no está en dónde se ejecuta, está en qué reprueba. Un ejecutor hospedado corriendo la suite contra una base sin aislamiento de cliente da sensación de cobertura y deja pasar la clase de defecto que más duele en un SaaS multi-tenant: fuga entre clientes. Compensación del costo que asumí: una prueba que vigila la propia puerta y falla alto.
Las doce verticales no tienen dirección pública propia. Todas se montan por ruta bajo un origen único, y el servidor de identidad — OAuth2 y publicación de claves — vive en ese mismo origen. La autorización no queda solo en una puerta al frente: viaja con el servicio, por un SDK único que todas las verticales cargan y que deniega la llamada cuando el alcance no está presente.
Como decisión de negocio: el producto es multi-tenant y vende a empresas que necesitan heredar jerarquía de cliente, plan contratado y segregación de funciones. La identidad tercerizada cobra por usuario activo — el costo crece con la base justo cuando el margen debería mejorar — y empuja la regla de negocio fuera del sistema que es su dueño. Manteniéndola en casa, el costo es de ingeniería (una vez) en vez de suscripción (siempre), y la regla de quién-ve-qué queda donde es verdad. El costo se mitiga en la operación: hay alerta dedicada para clave y token por expirar, y la puerta de publicación valida que el emisor de claves responde antes de devolver el nodo al tráfico.
Java con Spring Boot en la identidad, la contabilidad y el motor de flujo. Go en los servicios nuevos — binario único, sin runtime, arranca rápido y ocupa poca memoria, que es el recurso escaso aquí. Rust en dos lugares precisos: el agente que observa el núcleo del sistema operativo y el motor de cálculo fiscal. Python con Django en las verticales de negocio, donde la velocidad de cambio vale más que los microsegundos.
Como decisión de negocio: reescribir lo que funciona es la forma más cara de no entregar nada. El legado productivo sigue rindiendo en Python mientras lo que nace ya nace en Go, y el contrato estable entre ellos es lo que permite cambiar pieza por pieza sin parar la operación — sin el riesgo de la reescritura total, que históricamente tumba empresas.
Adquirente de tarjeta, bureau de crédito, APIs de gobierno, open banking, firma electrónica, marketplace, mensajería — todo entra y sale por un solo servicio. Él traduce cada proveedor a una capacidad normalizada y devuelve el resultado al ecosistema como hecho en el bus. Ninguna vertical de negocio conoce el nombre del proveedor; la regla de negocio queda en quien consume, no en el adaptador.
Como decisión de negocio: la integración con terceros es donde vive el riesgo contractual — credencial filtrada, cobro indebido, dato de cliente saliendo afuera. Concentrar significa que auditar “¿quién habla con el mundo de afuera, y con qué credencial?” es mirar un servicio, no doce. Y esto es verificable, no promesa: medí las seis verticales de negocio y ninguna tiene SDK de terceros instalado — solo existen en el servicio de integración. Una regla que depende de disciplina humana falla callada; esta se puede medir.
Operar solo con clientes reales impone una restricción que una empresa grande no tiene: cada servicio gestionado que contrato es dinero todos los meses y una pieza que no controlo. Por eso el diseño evita las tres fuentes clásicas de factura que crece sin aviso: tráfico de salida cobrado, balanceador por hora e identidad por usuario activo.
Como decisión de negocio: en un SaaS, el costo de infraestructura es lo que separa margen de pérdida cuando la base crece. La pregunta que hago antes de adoptar cualquier servicio es “¿este costo crece con el número de clientes?” — si crece linealmente con la receta, se come la economía de escala que el modelo debería dar. El dimensionamiento, los límites de CPU y concurrencia declarados y el libro de registro de publicación existen tanto por confiabilidad como por costo: un recurso que nadie mide es un recurso que nadie recorta.
Un plan de recuperación que nunca fue cronometrado es intención, no plan. Estos tres números fueron medidos de verdad, no estimados.
“Nada se publica a mano — y nada compila en el servidor de producción.”
una regla que nació de dos suspensionesPublicar es el momento en que un sistema estable se vuelve peligroso. El camino de abajo existe para que cambiar deje de ser un acto de coraje.
Nada se publica a mano y nada compila en el servidor de producción. La restricción nació de dolor real: la infraestructura anterior fue suspendida dos veces por carga sostenida de CPU — compilar en el host era uno de los disparadores. La arquitectura cambió por eso.
La imagen se construye en la estación/CI y se publica en el registry con la tag del commit. El servidor de producción solo ejecuta pull — nunca compila.
El conector del túnel se detiene en el nodo A. 100% del tráfico va al nodo B. La publicación ocurre en un nodo al que ningún usuario está accediendo.
Sin crashloop, emisor de claves respondiendo y cero migraciones pendientes. Esta última nació de un incidente: proceso de pie no significa base al día.
Vuelve al digest anterior en el propio nodo y aborta — el nodo estable nunca fue tocado. Y el conector del túnel se restaura con verificación: si falla, grita en stderr con el comando del arreglo manual.
El nodo validado vuelve al túnel y solo entonces el segundo nodo recibe la misma imagen, un servicio a la vez. Un lock global serializa la flota entera: dos deploys simultáneos ya tumbaron la infraestructura una vez.
Cada servicio recreado entra en un libro de registro en el nodo con fecha, digest de la imagen y el commit. Así se responde “¿qué exactamente está corriendo?” sin adivinar.
La cadena es propia: no hay runner hospedado revisando el pull request. GitHub guarda el código y sirve de registry de las imágenes; la puerta de calidad corre antes, en la estación. Esto tiene una consecuencia incómoda que prefiero decir en voz alta: no existe red de seguridad remota — si yo no corro la puerta, nadie la corre.
Formato y lint con catraca (el piso sube, nunca baja), verificación de integridad del proyecto y la suite corriendo en PostgreSQL con RLS armado — no en base de juguete. Cobertura bajo el piso reprueba. En el servicio en Java, la misma puerta suma pruebas de arquitectura, integración con contenedores reales y análisis estático.
Trabajo en rama, publicación por pull request, y la línea principal está protegida. push --force, reset --hard y borrar la principal son operaciones bloqueadas por política. Antes de una refactorización grande, una marca de retorno.
El objetivo de la puerta llamaba al intérprete del sistema y moría justo después de que el lint imprimiera “todo bien”. Quien leyera la pantalla veía verde en una puerta que no había ejecutado ni una prueba. Hoy una prueba vigila la propia puerta — y falla alto.
“Un sistema que nunca se rompió es un sistema que nadie usa.”
tres incidentes reales, y la cicatriz de cada unoLa parte que normalmente no se publica. Causa, qué falló en la detección, qué empeoré intentando arreglarlo, y la guarda que impide la reincidencia.
Sistema que nunca se rompió es sistema que nadie usa. Lo que diferencia no es la ausencia de incidente — es lo que queda después. Estos tres son reales, del mismo día, y cada uno dejó una guarda en el código.
La guarda nació con el mismo vicio que fue a cazar: contaba solo la pendencia, y grep -c en salida vacía imprime cero — igual a “ninguna pendiente”. Hoy cuenta el total también: lista vacía no es schema al día, es falta de evidencia.
except ancho que envolvía al cliente entero: toda divergencia era detectada y descartada. Transacción fantasma en el adquirente, o pago sin confirmación — dinero — se volvía una línea de log.Ausencia de medición no es ausencia del hecho. Y alerta que grita siempre ciega al guardián — habría sido peor que el defecto original.
Al medir una integración, no pares en “el evento se publica”. Pregunta quién escucha — y si lo que lleva significa algo del otro lado.
Todo incidente de arriba se volvió una prueba que reprueba el build si la clase del defecto vuelve. Convención escrita en documento envejece; guarda ejecutable, no. Y toda guarda se prueba con un mutante: si reintroduzco el defecto y sigue verde, no sirve.
“¿Este depósito cuenta como saldo vendible?” estaba escrita, literal, en cinco lugares. Con un segundo tipo de depósito, cada copia olvidada se vuelve un hueco silencioso. La regla pasó a tener dueño único — y una prueba reprueba a quien la escriba a mano.
Barrido por AST: todo tipo de evento publicado en el código tiene que estar declarado. Encontró dos casos vivos — uno de ellos tragaba divergencia de tarjeta.
Guarda estructural, no comportamental: en pruebas los dos roles de base colapsan, así que el defecto solo existe en producción. Una prueba de comportamiento quedaría verde con el sistema en el suelo.
Dos enlaces absolutos apuntando a la máquina de quien los creó terminaron dentro de la imagen de producción — y eran autorreferentes: recursión infinita para cualquier barrido que los siga.
SRE, aquí, no es un título: es un conjunto de reglas que ya me costaron caro aprender y hoy están escritas en script, en prueba y en política. Seis de ellas:
El relato del primer incidente de esta página tiene un campo llamado “Yo lo empeoré”: mi intento de arreglo manual apiló tres sesiones atascadas antes de que entendiera el problema. Post-mortem que omite el error del autor no sirve para que nadie aprenda — sirve para proteger al autor.
Ante producción en el suelo, el camino es volver al digest anterior y solo entonces investigar. El rollback es automático y por digest — no por “reconstruir la imagen de antes”, que no es lo mismo y ya me engañó.
Una alerta disparada por total acumulado queda encendida para siempre y pasa a ser ignorada — entonces la muerte nueva llega sin que nadie la vea. La alerta tiene que medir el cambio, y silenciarse cuando el cambio para.
La infraestructura anterior fue suspendida dos veces por carga sostenida de CPU, y compilar en el servidor era uno de los disparadores; la tercera suspensión sería definitiva. Hoy “producción nunca compila” no es preferencia de estilo — es regla con revisión que bloquea el pull request.
Dos deploys simultáneos ya tumbaron la flota. Existe un lock global: un servicio a la vez, un nodo a la vez, y el lock huérfano se reclama en vez de trabar la flota para siempre.
Cada recreación de servicio entra en un libro de registro en el nodo: fecha, digest y commit. La pregunta “¿qué exactamente está corriendo?” tiene respuesta por consulta, no por memoria de quien publicó. De ahí salió el promedio medido de 92 publicaciones por día.
Ya reporté “es configuración del operador” sobre un campo que era imposible de configurar — el tipo no cabía el identificador del otro servicio. Cero de doce registros llenados en la flota entera no era negligencia: era un defecto. Hoy mido antes de concluir.
Una validación que no logró medir no puede reportar “todo bien”. Esa confusión ya apareció tres veces: en el deploy, en la conciliación de tarjeta y en una guarda mía — siempre con cara de verde.
Suite con miles de pruebas verdes convivía con la pantalla principal rota. Prueba con doble no mide la regla que solo existe en el borde real — y la verificación visual agarra lo que ninguna prueba agarra.
“Lo que todavía no probé vale tanto como lo que sí probé — mientras yo diga cuál es cuál.”
el estado honesto de lo que faltaQué está hecho, qué está parcial y qué todavía no existe. Separar los tres es lo que vuelve confiables a las dos primeras columnas.
Puedo demostrar una publicación canaria de punta a punta — incluyendo el rollback automático cuando la validación reprueba.