Un pipeline de CI que verifica un email no falla solamente cuando el mensaje no llega. También falla cuando llega tarde, cuando el worker mira el inbox equivocado o cuando dos pods creen tener el mismo buzón. El primer síntoma son pruebas intermitentes: un commit verde por la mañana y rojo por la tarde, sin un cambio claro en el código.
En revisiones de operaciones, me resulta más útil tratar el email como una señal distribuida. El test publica una intención, el proveedor procesa un mensaje y el inbox devuelve evidencia. Kubernetes ejecuta los actores, pero no convierte automáticamente esa cadena en observable. Necesitamos un contrato pequeño para saber qué pasó, dónde y cuándo.
La señal perdida entre el pod y el inbox
Un flujo típico crea un usuario, solicita verificación, espera un mensaje y sigue el enlace. En un cluster, cada paso puede vivir en pods distintos. Si el test guarda solo una dirección de buzón y un sleep(10), no sabremos si la espera terminó porque llegó el mensaje o porque se agotó el tiempo.
El problema crece con un dummy e mail de pruebas compartido por varios jobs. Un mensaje antiguo puede satisfacer la aserción equivocada. Otra ejecución puede limpiar el inbox mientras el primer pod todavía está leyendo. Y si el correo no llega a tiempo, el pipeline queda en espera sin saber porqué.
Por eso cada ejecución necesita un identificador de correlación, una fecha de expiración y un resultado explícito. El nombre del pod sirve para buscar logs, pero no es una identidad de negocio estable: Kubernetes puede recrearlo durante el mismo test.
Qué debe contener el contrato de señales
Un contrato sencillo puede acompañar cada solicitud de email con estos campos:
-
run_id: identifica la ejecución completa del pipeline. -
mailbox_lease_id: identifica la reserva temporal del buzón. -
message_id: identifica el mensaje esperado, si el proveedor lo expone. -
requested_atyexpires_at: delimitan la ventana válida. -
state:requested,observed,expiredofailed. -
attempt: cuenta las lecturas, no solo los envíos.
No hace falta copiar el token de verificación ni la dirección completa en cada log. Una huella corta del destinatario y el run_id suelen bastar para unir eventos. Así podemos investigar sin convertir los logs en otra bandeja de entrada.
Este enfoque se parece a diseñar validación de email sin saltos de layout: el estado visible debe explicar la transición real, no ocultar el problema detrás de una espera genérica. Para un pipeline, la interfaz es el resultado del job y sus evidencias.
Diseñar el lease del buzón
Un buzón de prueba debe reservarse antes de crear el usuario. La reserva tiene propietario, duración y una operación de liberación idempotente. Si el job muere, el lease expira solo; no dependemos de que un finally sea ejecutado en un pod que ya desapareció.
El orden recomendado es:
- Crear
run_idy solicitar unmailbox_lease_id. - Guardar ambos valores como variables del job, no como configuración global del cluster.
- Crear el usuario y enviar el email con un encabezado de correlación cuando sea posible.
- Consultar el inbox filtrando por lease, destinatario y ventana temporal.
- Confirmar el mensaje observado y liberar el lease.
En esta etapa puede usarse un buzón de pruebas que deja pistas para separar la captura del mensaje de la aserción final. La idea no es hacer polling mas agresivo, sino leer con una condición que explique por qué el mensaje pertenece a esta ejecución.
Si el flujo necesita un servicio externo, una opción es usar un correo temporal desechable con un alcance limitado al test. No debe reutilizarse para datos reales ni convertirse en un destino permanente. El lease debe guardar solo la referencia necesaria y borrar la evidencia sensible al cerrar la ejecución.
Correlacionar eventos sin filtrar datos
El job puede emitir eventos con una forma estable:
{
"run_id": "ci-8421",
"mailbox_lease_id": "lease-17",
"event": "message_observed",
"state": "observed",
"age_ms": 1840,
"message_fingerprint": "b7c2..."
}
La huella no necesita ser reversible. Evita registrar asunto completo, cuerpo o enlaces con tokens. En un incidente, message_observed junto con age_ms responde una pregunta concreta: ¿el proveedor tardó, el lector consultó mal o la aserción llegó después de que expiró el lease?
Conviene que los eventos de Kubernetes y los del servicio de email compartan run_id. El primer intento de instrumentación suele mezclar nombres de namespaces, y luego nadie sabe cual es el correcto. Una tabla o consulta guardada con las transiciones ayuda más que veinte filtros improvisados.
Reintentos y estados terminales
El lector debe reintentar con un límite y registrar el motivo de cada intento. Un 404 de mensaje todavía no es un fallo si la ventana sigue abierta; un lease expirado sí es un estado terminal. Una reintento después de expired solo aumenta ruido y puede observar un correo de otra ejecución.
Una regla práctica es separar:
- Transitorio: inbox aún vacío, respuesta temporal del proveedor o pod reiniciado.
-
Terminal: lease vencido, formato inválido o mensaje asociado a otro
run_id. - Accionable: proveedor lento, duplicado detectado o pérdida de correlación.
Si un worker se reinicia, debe recuperar el estado desde el contrato persistido. No debe empezar desde cero solo porque su memoria se perdió. En SRE, esta diferencia reduce los falsos positivos y hace que una reejecución sea una decisión, no un reflejo.
Checklist de operación
Antes de confiar en el flujo, compruebo lo siguiente:
- ¿Cada job obtiene un lease único y con expiración?
- ¿El
run_idaparece en los eventos del test, del proveedor y del lector? - ¿La consulta descarta mensajes fuera de la ventana temporal?
- ¿Un pod nuevo puede continuar desde el estado guardado?
- ¿Los logs evitan tokens, cuerpos y direcciones completas?
- ¿Los reintentos tienen límite y diferencian lo transitorio de lo terminal?
- ¿El cleanup funciona aunque el job termine por timeout?
También probaría una caída del lector, un mensaje duplicado y un retraso del proveedor. El caso feliz demuestra que el email funciona; esos tres casos muestran si el contrato realmente protege al equipo.
Un buzón temporal es solo una pieza de la prueba. La confiabilidad aparece cuando cada mensaje tiene contexto, cada lease tiene límites y cada espera termina con una razón legible. Con ese contrato, Kubernetes deja de ser el lugar donde “a veces falla CI” y se vuelve una fuente útil de señales para diagnosticar el flujo.
Top comments (0)