En un pipeline de CI, los datos de correo suelen crearse como si fueran basura temporal: una dirección, un mensaje de verificación y después nada más. El problema aparece cuando esa basura tiene una vida más larga que el job. Terraform detecta recursos que ya no representan una ejecución activa, Kubernetes conserva Jobs terminados y el siguiente test puede leer señales viejas.
La solución no es borrar todo al final con un script que nadie mira. Es asignar una caducidad explícita y una responsabilidad clara a cada recurso. Un correo temporal, incluso cuando se usa como correo desechable gratis para pruebas, necesita el mismo cuidado operativo que una cola o un bucket: dueño, TTL, estado y evidencia de limpieza.
El síntoma: el estado de Terraform siempre cambia
Un equipo puede tener un módulo que crea una bandeja aislada para cada ejecución. El plan parece correcto, pero en cada pipeline aparece una diferencia nueva: una dirección antigua, una etiqueta que ya no existe o un recurso que el proveedor mantiene aunque el Job terminó.
El incidente suele empezar de forma discreta:
- El test crea una bandeja con el ID del pipeline.
- El pod falla antes de ejecutar el bloque de limpieza.
- Terraform guarda el recurso en el estado, pero el proveedor lo marca como expirado.
- La siguiente ejecución encuentra un nombre ocupado o una bandeja con mensajes viejos.
Reintentar el pipeline puede ocultar la causa. El primer intento deja basura y el segundo encuentra un entorno más limpio por casualidad. Eso parece una mejora, pero solo es una señal dificil de interpretar.
Separar infraestructura estable de datos efímeros
Terraform es bueno declarando infraestructura con una vida relativamente estable. No siempre es la mejor herramienta para representar cada buzón de una prueba que dura dos minutos. Cuando el estado contiene miles de recursos efímeros, el drift deja de ser una excepción y se convierte en el comportamiento normal.
Una división más segura es:
- Terraform administra el proveedor, permisos, límites y políticas de retención.
- Un servicio o un Job crea el buzón por ejecución usando esas políticas.
- El Job registra el identificador, el
run_idy la fecha de expiración. - Un reconciliador retira recursos vencidos aunque el pipeline original haya desaparecido.
Así, el código de Terraform no tiene que saber cuántos mensajes se enviaron esta mañana. Su responsabilidad es mantener el marco estable. La responsabilidad del reconciliador es que ningún recurso efímero quede olvidado.
Este patrón también ayuda a decidir dónde usar un correo temporal. Para una prueba aislada conviene una bandeja por escenario, no una cuenta compartida con filtros improvisados. Este artículo sobre reservar bandejas por escenario muestra la misma idea desde el lado de un servicio de aplicación.
Un contrato de expiración para cada ejecución
El contrato mínimo puede viajar como metadatos del recurso y como parte del resultado del job:
email_fixture: run_id: "${CI_PIPELINE_ID}-${CI_JOB_ID}" owner: "checkout-e2e" created_at: "2026-11-10T02:00:00Z" expires_at: "2026-11-10T02:20:00Z" cleanup: required No hace falta que todas las pruebas duren lo mismo. Un smoke test puede tener 10 minutos y una suite nocturna, 90. Lo importante es que el TTL sea calculable, visible y comprobable. Si el proveedor solo permite una política global, guarda el expires_at esperado y deja que el reconciliador aplique la decisión local.
También conviene normalizar los nombres. En una integración vieja puede aparecer algo como temp org mail o temp mailid; esos valores no deben convertirse en una identidad estable ni mezclarse con el nombre del entorno. Usa un identificador generado por el pipeline, no una palabra que alguien escribió a mano en un archivo de prueba.
Cómo coordinar Terraform y Kubernetes
En Kubernetes, el Job de limpieza debe tener su propia tolerancia a fallos. activeDeadlineSeconds limita cuánto puede quedarse bloqueado y ttlSecondsAfterFinished ayuda a retirar el Job cuando termina. Ninguno de los dos sustituye al reconciliador externo: el clúster puede estar caído justo cuando toca limpiar.
Una secuencia razonable es:
- Crear la fixture y guardar su
fixture_iden un artefacto del run. - Ejecutar la prueba leyendo solo mensajes con ese
run_id. - Marcar la fixture como
passed,failedoexpired. - Intentar la limpieza en un
finallyo paso de salida. - Dejar que el reconciliador vuelva a intentarlo si el paso no termina.
No ejecutes terraform apply para cada correo de una prueba. Eso hace lenta la suite y aumenta la posibilidad de que dos pipelines compitan por el mismo estado. Terraform puede aplicar las políticas y los límites al desplegar la plataforma; el ciclo corto de la fixture debe ser idempotente y mas pequeño.
Cuando la interfaz de la prueba necesita validar mensajes en el navegador, también importa que el estado visual sea honesto. Validar emails sin frenar el formulario es una referencia útil para no confundir “estoy esperando” con “la verificación falló”.
Qué observar cuando la limpieza falla
Una métrica de cantidad de buzones vencidos dice más que un log por cada intento. Yo suelo separar estas señales:
- edad del recurso más antiguo que aún no se limpió;
- número de fixtures por estado (
active,failed,expired); - duración del reconciliador y cantidad de reintentos;
- errores del proveedor frente a errores de autorización;
- diferencia entre
expires_aty la hora real de eliminación.
Los resultados llega mejor agrupados por run_id, proveedor y equipo dueño. No guardes tokens ni el cuerpo completo de cada mensaje en el sistema de logs. Una clave de correlación y el asunto normalizado suelen ser suficientes para investigar sin aumentar el riesgo.
En Cloud, una alerta puede dispararse cuando la edad máxima supera dos ciclos de limpieza, no por cada recurso individual. Eso reduce ruido y avisa cuando hay un patrón operativo real. Si el reconciliador no tiene permiso, el error debe ser visible y accionable; esperar mas tiempo no arregla una política IAM incorrecta.
Checklist de cierre
- Terraform administra políticas, permisos y límites estables.
- Cada fixture tiene
run_id, dueño yexpires_at. - El test nunca lee mensajes fuera de su propia identidad.
- La limpieza se intenta al terminar y también desde un reconciliador.
- Los Jobs de Kubernetes tienen límites de tiempo y TTL.
- El estado de Terraform no crece con cada correo de prueba.
- Las métricas distinguen expiración, proveedor y autorización.
- Los logs conservan evidencia útil sin copiar tokens ni cuerpos.
La caducidad no es un detalle cosmético. Es el contrato que evita que una prueba de hoy contamine la de mañana. Cuando Terraform mantiene el marco, Kubernetes ejecuta el trabajo y un reconciliador cierra los huecos, el correo de CI deja de ser un recurso huérfano difícil de explicar.