En CI, un correo de prueba parece un dato pequeño. En un cluster de Kubernetes, sin embargo, cada inbox, alias o fixture tiene un ciclo de vida. Si nadie define cuándo termina, el namespace empieza a acumular recursos, los tests leen mensajes viejos y una alerta de SRE llega con muy poco contexto.
La solución no es borrar todo al final del job sin mirar. Es crear un contrato de caducidad: cada dato de email nace con dueño, hora de expiración, motivo de retención y una forma segura de dejar evidencia. Así una dirección de correo desechable sirve para una ejecución concreta, no para convertirse en basura compartida.
El síntoma: el namespace nunca queda vacío
El incidente suele empezar de forma poco espectacular. Un pipeline falla después de un timeout. El job de limpieza no corre porque el pod fue terminado. Al día siguiente, otro test encuentra el mismo alias o un registro de verificación que todavía parece valido. El namespace no queda vacio, pero el panel de CI solo muestra un fallo de aserción.
En un equipo que mantiene muchos servicios, aparecen otros síntomas:
- los datos se queda más tiempo del esperado y el coste de almacenamiento sube;
- un test nuevo hereda un inbox creado por un job anterior;
- el equipo no sabe quien puede borrar un recurso compartido;
- la alerta menciona "email no recibido" pero no indica qué ejecución lo esperaba.
He visto este patrón confundirse con un problema de proveedor de correo. A veces el proveedor funciona bien: el fallo real es que Kubernetes no tiene una frontera clara entre una ejecución y la siguiente.
Qué debe significar caducar
Caducar no significa únicamente añadir ttlSecondsAfterFinished a un Job. Ese campo puede retirar el objeto Job, pero no necesariamente elimina el inbox remoto, el Secret temporal, los mensajes o los registros de auditoría que creó el flujo.
Para cada fixture de email, define al menos estas propiedades:
- Identidad: un
run_idúnico y el nombre del servicio que lo creó. - Propietario: el pipeline, equipo o servicio responsable de limpiarlo.
- Expiración: una hora absoluta, no solo "cuando termine el pod".
- Alcance: namespace, entorno y caso de prueba donde se puede usar.
- Evidencia: los metadatos mínimos para entender el resultado sin guardar el contenido completo.
La hora absoluta es importante. Si el cluster pierde un nodo o el controlador se reinicia, el sistema puede retomar la limpieza comparando el reloj con expires_at. Un timeout de veinte minutos debería seguir siendo veinte minutos, aunque el pod haya estado detenido durante un rato.
Un contrato sencillo para cada job
Antes de crear el inbox, el job puede producir un pequeño documento de control:
{ "run_id": "orders-1842-7f3a", "owner": "checkout-ci", "namespace": "ci-pr-1842", "created_at": "2026-11-12T14:00:00Z", "expires_at": "2026-11-12T14:20:00Z", "purpose": "verify-signup-email", "evidence_key": "ci/orders-1842-7f3a/result" } El controlador de limpieza solo necesita verificar que expires_at ya pasó, que el run_id coincide y que el recurso pertenece a un entorno de CI. Si falta uno de esos datos, debe marcarlo para revisión, no eliminarlo a ciegas.
También conviene hacer la operación idempotente. Un reintento puede encontrar que el inbox ya fue borrado y debe tratar ese resultado como éxito. La misma idea aparece en este artículo sobre idempotencia en emails de prueba: limpiar dos veces no debe crear un estado nuevo ni esconder el error original.
Para los Jobs de Kubernetes, separo tres responsabilidades:
- el test crea y registra el fixture;
- un
preStopo una etapaalwaysintenta la limpieza rápida; - un recolector periódico elimina lo que sobrevivió a una cancelación.
La tercera capa es la que hace el sistema resistente. La limpieza del propio pipeline es útil, pero no puede ser la única garantía.
Cómo conservar evidencia sin conservar datos
El error opuesto también ocurre: por miedo a perder contexto, guardamos el cuerpo completo de cada email durante semanas. Eso amplía la superficie de privacidad y vuelve más difícil saber qué es relevante.
Para una alerta de SRE normalmente bastan:
-
run_idy nombre del test; - timestamps de creación, envío, observación y caducidad;
- identificador o hash del mensaje;
- asunto normalizado, sin tokens ni enlaces privados;
- resultado de la aserción y razón de un posible rechazo;
- estado de la limpieza.
Si el mensaje debe conservarse para una investigación, debe existir una retención separada y explícita, con acceso limitado. No mezcles el archivo de evidencias con el almacén de fixtures. La frontera tiene que ser visible en el diseño y en los permisos.
En la práctica, un registro como temp gamil com puede aparecer en una nota de triage por una entrada manual equivocada. No lo trates como una dirección real ni lo reutilices para otra ejecución. Ese detalle pequeño ayuda a detectar que el flujo de entrada necesita validación y que el operador está trabajando con información incompleta.
En la interfaz de un dashboard, los estados también deben ser claros: creado, esperando, recibido, rechazado, expirado y limpiado. Los patrones de formularios async que respetan el foco ofrecen una lección aplicable aquí: no escondas el estado importante detrás de una animación o de un mensaje genérico.
Checklist de revisión para SRE
Cuando reviso un flujo de email en Kubernetes, hago estas preguntas:
- ¿Cada ejecución tiene un
run_idque no se reutiliza? - ¿El recurso tiene
owner,created_atyexpires_at? - ¿La caducidad elimina también los recursos fuera del cluster?
- ¿La limpieza se puede repetir sin producir un error falso?
- ¿Un job cancelado deja una ruta para el recolector periódico?
- ¿La alerta incluye namespace, test, run y última transición?
- ¿La evidencia evita guardar tokens, enlaces privados y contenido innecesario?
- ¿Un operador puede distinguir un mensaje retrasado de un mensaje de otra ejecución?
Si falla, el equipo mira primero el correo y despues el log. El orden debería ser al revés: empieza por la línea temporal y luego inspecciona el mensaje solo si la evidencia indica que hace falta.
Un contrato de caducidad no elimina todos los fallos de CI. Pero hace que cada fallo tenga una historia más corta: quién creó el dato, qué esperaba, cuándo dejó de ser válido y si la limpieza terminó. Para SRE, esa claridad suele valer más que otro reintento automático.
