En un pipeline de CI, el correo suele parecer una pieza secundaria: se dispara una tarea, se espera un mensaje y el job termina. En realidad, ese flujo tiene varias dependencias escondidas. Una bandeja compartida, un reintento sin límite o un mensaje viejo pueden convertir una prueba sencilla en un incidente dificil de explicar.

En equipos que operan Kubernetes, el objetivo no es solamente recibir el mensaje. Es poder responder: ¿qué ejecución lo creó?, ¿cuánto tardó?, ¿qué pasó cuando no llegó? Para eso conviene tratar el correo de prueba como una pequeña dependencia de producción, con aislamiento, señales y un límite claro de espera.

El síntoma: el correo de CI parece aleatorio

El fallo normalmente se presenta como un timeout en un test de registro o de recuperación de contraseña. Al volver a ejecutarlo, pasa. Al mirar la bandeja, hay varios mensajes parecidos y no queda claro cual pertenece al run que falló.

Hay cuatro causas que se mezclan con frecuencia:

  • El job reutiliza una dirección o una bandeja de una ejecución anterior.
  • Los pods comparten una credencial de acceso y un test puede leer el mensaje de otro.
  • El consumidor busca por asunto, pero no por un identificador único.
  • El timeout del test es menor que el tiempo real de entrega, mientras los reintentos ocultan el retraso.

Un correo temporal puede ser útil para aislar datos de prueba, pero no resuelve por sí solo la trazabilidad. Incluso cuando se usa tempmailso como parte del entorno, la ejecución necesita su propio contrato. La infraestructura no debería depender de que una persona recuerde limpiar una bandeja al final del día.

Qué debe aislarse en Kubernetes

El aislamiento tiene más de una capa. Primero, cada ejecución necesita un identificador único, por ejemplo run_id. Después, ese identificador debe viajar en la dirección, en un header o en ambos, según las capacidades del sistema de correo.

Una forma simple de modelarlo es:

CI run └── namespace de prueba ├── job de aplicación ├── dirección de correo de ese run └── registro de entrega 

No hace falta crear un clúster por prueba. Sí hace falta que el Job tenga una identidad y un ciclo de vida propio. Un ttlSecondsAfterFinished razonable evita que los recursos se queden vivos, y un activeDeadlineSeconds evita que una prueba bloqueada consuma capacidad indefinidamente. Los valores dependen del pipeline; copiar un número de otro equipo no es una estrategia.

También hay que separar los secretos. El pod que ejecuta la prueba debe recibir solo el acceso que necesita para leer su buzón. Un Secret común para todos los namespaces hace que un error de configuración sea mucho mas caro. En un entorno compartido, Role y RoleBinding pueden limitar quién lee el secreto y quién elimina el Job.

Si el correo se crea fuera del clúster, guarda en el resultado del job el proveedor, el identificador del buzón y la fecha de expiración. No guardes el contenido completo del mensaje como log por defecto: puede contener tokens, nombres o datos que no deberían terminar en el sistema de observabilidad.

Un contrato pequeño para cada ejecución

La prueba debe conocer sus límites antes de empezar. Un contrato mínimo puede tener este aspecto:

email_check: run_id: "${CI_PIPELINE_ID}-${CI_JOB_ID}" subject_prefix: "ci-${CI_PIPELINE_ID}" poll_interval: 2s delivery_timeout: 90s cleanup: required 

El run_id no es solo un dato para depurar. Debe aparecer en el asunto o en un header que el consumidor pueda filtrar. Si el servicio bajo prueba no permite headers, un prefijo corto en el asunto es mejor que buscar por el texto completo del correo.

El lector debe distinguir tres resultados: mensaje recibido, mensaje no recibido dentro del presupuesto y error del proveedor. Mezclarlos en un único false hace que el equipo pierda tiempo mirando el lugar equivocado. Un error de autenticación no se arregla aumentando el polling.

Para evitar carreras, el consumidor puede registrar el momento en que comienza a buscar y rechazar mensajes con un timestamp anterior. Si el proveedor no entrega timestamps fiables, usa una marca de correlación generada para esa ejecución. El mensaje viejo ya no puede hacerse pasar por una señal nueva.

Observabilidad sin ruido

Las señales útiles son pequeñas y consistentes:

  • duración desde el envío hasta la recepción;
  • estado final: received, timeout o provider_error;
  • run_id, namespace y nombre del Job;
  • contador de reintentos;
  • motivo de limpieza y su resultado.

En el dashboard de SRE, una métrica de timeout es más accionable que cientos de líneas diciendo “esperando correo”. Conviene registrar un evento al enviar, otro al recibir y otro al cerrar la prueba. Si los logs llega desordenados, la correlación debe poder reconstruirse con el run_id, no con la hora aproximada.

Para profundizar en el diseño de señales de tareas efímeras, puede servir este artículo sobre señales útiles para CronJobs frágiles. Y si el envío sale de un servicio FastAPI, es útil mantener el contexto cuando un job dispara correos.

No alertes por cada timeout aislado de un test. Una alerta debe representar una tendencia o un impacto real: varios fallos en una ventana, aumento de la latencia o errores del proveedor. La prueba local puede ser estricta; la alerta de plataforma debe tener un poco más de contexto.

Checklist antes de darlo por resuelto

  • Cada ejecución genera un run_id que aparece en la búsqueda del mensaje.
  • El buzón o dirección no se comparte sin una correlación inequívoca.
  • El timeout, el intervalo de polling y el número de reintentos están documentados.
  • provider_error no se confunde con “el mensaje tardó”.
  • Los secretos tienen el alcance mínimo posible.
  • Los tokens y el cuerpo del correo no se escriben en logs sin necesidad.
  • El Job se limpia aunque el test falle.
  • El pipeline conserva un resumen reutilizable para diagnosticar el siguiente fallo.

El detalle que suele cambiar el resultado es aceptar que el correo no es una espera pasiva. Es una dependencia distribuida, igual que una API externa: tiene latencia, estados y fallos. Cuando Kubernetes le da una identidad al run y SRE le da señales claras, las pruebas dejan de parecer aleatorias. No quedan perfectas, pero ya se pueden operar y mejorar sin adivinar.