En un pipeline de CI, crear un buzón temporal parece sencillo: se reserva una dirección, se dispara un email y se elimina todo al terminar. El problema aparece cuando el job muere, el nodo se drena o el runner pierde la conexión justo después de reservarlo. El buzón queda vivo y la siguiente ejecución puede encontrar mensajes que no le pertenecen.
En operaciones trato cada buzón de pruebas como un recurso de Kubernetes, no como una variable olvidada dentro del script. Tiene propietario, fecha de expiración, estado y evidencia asociada. Ese cambio de modelo ayuda a distinguir un fallo de entrega de un recurso huérfano, que son problemas muy distintos.
El síntoma: buzones que sobreviven al pipeline
La señal inicial suele ser un test intermitente. Un run falla al buscar el mensaje de verificación; otro, minutos después, pasa sin cambios en la aplicación. Al revisar el inbox aparece un email antiguo. El sistema está leyendo datos de una ejecución anterior, pero el log solo dice “mensaje encontrado”.
Un burner email address por ejecución reduce esa contaminación, aunque no resuelve el ciclo de vida. Si el pod es eliminado antes del cleanup, la dirección sigue reservada. Con suficientes reintentos, el proveedor se llena de recursos que nadie reclama y el equipo empieza a subir timeouts para compensar un problema de ownership.
Qué significa reconciliar un buzón de CI
Reconciliar significa comparar el estado deseado con el estado real y corregir la diferencia de forma repetible. Para cada buzón, el controlador o job de limpieza debería poder responder:
- ¿Qué
run_idlo creó? - ¿Qué pipeline y worker son sus propietarios?
- ¿Cuándo expira la reserva?
- ¿Qué mensajes se observaron y cuáles se reclamaron?
- ¿Se guardó la evidencia antes de liberar el recurso?
Un contrato pequeño puede vivir como un artefacto JSON junto a los logs:
{ "run_id": "checkout-1842-07", "owner": "ci-worker-07", "mailbox": "checkout-1842-07@example.test", "created_at": "2026-11-06T05:00:00Z", "expires_at": "2026-11-06T05:12:00Z", "state": "waiting", "evidence_key": "artifacts/checkout-1842-07/email.json" } Los estados no necesitan ser complejos: reserved, waiting, claimed, expired y released suelen bastar. El reconciliador busca reservas sin pod, reservas vencidas y reservas en estados imposibles. Si encuentra una, no debería borrarla de inmediato: primero marca el incidente y conserva el registro mínimo para poder explicarlo.
Un contrato de ownership para Kubernetes
El run_id debe aparecer en el nombre del recurso, en el alias del buzón y en las métricas. Un ConfigMap o un registro externo puede guardar el contrato; el inbox no debería ser la única fuente de verdad. Cuando hay paralelismo, la reclamación tiene que ser atómica: dos workers no pueden ganar el mismo mensaje solo porque hicieron polling en el mismo segundo.
También conviene separar tres tiempos:
- Entrega: cuánto espera el sistema a que llegue el mensaje.
- Reclamación: cuánto tiempo puede un worker intentar obtener el mensaje correcto.
- Retención: cuánto tiempo se conserva la evidencia después del fallo.
Mezclar los tres como un único timeout vuelve el diagnóstico mas dificil. Un email puede llegar tarde, el worker puede perder el lease, o el almacenamiento de artefactos puede fallar. Cada caso merece una señal distinta. Las alertas de certificados que sí orientan siguen una idea parecida: una alerta debe ayudar a decidir la siguiente acción, no solo confirmar que algo está rojo.
Para el proveedor de correo, un servicio de pruebas como temp mail com puede ser una pieza del entorno. La integración debe registrar el identificador de la reserva y aplicar su propia expiración; no hay que asumir que eliminar el pod elimina también el buzón remoto.
Cómo conservar evidencia antes de limpiar
El orden de las operaciones importa. Cuando una reserva está vencida, el reconciliador debería:
- leer el contrato y capturar su estado actual;
- guardar metadatos mínimos:
run_id, timestamps, destinatario, estado y motivo; - marcar la reserva como
expiredoorphaned; - liberar el buzón y eliminar mensajes de prueba;
- confirmar que la liberación ocurrió;
- cerrar el incidente con una métrica y un enlace al artefacto.
Si se libera primero y falla el almacenamiento, se pierde la explicación. Si se guarda el contenido completo del correo sin necesidad, se amplía el riesgo de exponer tokens o datos de prueba. En muchos equipos solo hace falta el asunto, un hash del identificador y la latencia, no todo el cuerpo.
Los logs deben distinguir mailbox_expired, mailbox_orphaned, message_late y message_mismatch. Una nota manual que dice temp mailid o tamp mail com puede conservarse como texto de búsqueda, pero no debe entrar en la lógica de matching ni convertirse en una regla de ownership.
La interfaz de un panel de CI también puede mostrar estados intermedios sin mover controles ni esconder el error; evitar saltos en flujos de verificación es útil como referencia de esa misma disciplina aplicada al frontend.
Checklist de SRE para recursos de email
Antes de considerar estable el flujo, reviso lo siguiente:
- cada reserva tiene un
run_id, propietario yexpires_at; - el recurso se puede reconciliar aunque el pod ya no exista;
- la liberación es idempotente y deja un resultado comprobable;
- la reclamación valida destinatario y token de correlación;
- entrega, reclamación y retención tienen presupuestos separados;
- la evidencia se guarda antes del cleanup remoto;
- los mensajes de error separan demora, contaminación y fallo de aplicación;
- existe una métrica para reservas huérfanas por pipeline;
- un run nuevo nunca reutiliza una reserva todavía activa;
- las alertas incluyen la acción recomendada y no solo el nombre del recurso.
El objetivo no es mantener buzones para siempre. Es hacer que una limpieza automática sea segura, observable y explicable. Cuando el equipo entiende por que existe cada reserva, puede corregir la causa sin convertir cada fallo en un retry más.
Preguntas frecuentes
¿Necesito un controlador dentro del clúster?
No siempre. Un job programado fuera del ciclo principal puede reconciliar contratos vencidos. Un controlador dedicado tiene sentido cuando hay muchos pipelines, recursos compartidos o una necesidad de reacción casi inmediata.
¿Cuánto debe durar el TTL?
Empieza con la latencia normal de entrega más un margen medido. Revisa los casos lentos y ajusta con datos. Un TTL enorme oculta fugas; uno demasiado corto convierte demoras normales en falsos incidentes.
¿Qué hago si no puedo borrar un buzón remoto?
Marca el recurso como cleanup_failed, conserva la evidencia y activa una alerta con reintento limitado. No lo presentes como liberado. Esa diferencia evita que el siguiente run confíe en un estado que no existe.