Un pipeline de CI puede decir que un email de verificación no llegó. Eso no explica si el problema fue Kubernetes, el proveedor de correo, la aplicación o el propio test. En una guardia, la diferencia entre esas dos frases es bastante grande.
En varios equipos he visto el mismo patrón: el test crea una bandeja, dispara el flujo y espera un mensaje. Si falla, el pod termina y los únicos restos son un timeout y un log de aplicación. La siguiente persona vuelve a ejecutar el pipeline, quizá en un nodo distinto, y el incidente se vuelve una adivinanza. Ni siquiera queda claro si alguien buscó el mensaje correcto o simplemente el más reciente.
Mi solución es tratar cada email de CI como un pequeño contrato de evidencia. No hace falta montar una plataforma enorme. Hace falta acordar qué datos se conservan, por cuanto tiempo y quién puede leerlos.
El síntoma: un email que desaparece dentro del pod
Un correo de prueba suele cruzar varios límites: el navegador, el servicio de identidad, la cola de salida, el proveedor de correo y la bandeja temporal. Kubernetes añade sus propios límites: reinicios, namespaces, Jobs efímeros y logs que pueden expirar rapido.
Cuando solo registramos email received: false, perdemos la secuencia. ¿La aplicación generó el mensaje? ¿El worker lo procesó? ¿El test consultó la bandeja desde el mismo entorno? Una señal de observavilidad útil debe contestar esas preguntas sin guardar el contenido completo del correo.
Si un compañero escribe “tamp mail com” en la incidencia para describir la bandeja usada, no es un detalle menor: muestra que el identificador real no viajó con el fallo.
Qué debe contener el contrato de evidencia
Para cada intento, guardaría un registro pequeño y estructurado:
-
run_id: el identificador del pipeline y del intento. -
namespaceypod: dónde se ejecutó el test. -
inbox_id: la bandeja o alias reservado para ese intento. -
triggered_at: cuándo terminó la acción que debía producir el email. -
poll_started_atypoll_deadline: la ventana exacta de consulta. -
message_ids_seen: identificadores y asuntos observados, sin cuerpos sensibles. -
decision:accepted,expiredorejected, con una razón corta. -
service_version: la versión desplegada que generó la notificación.
El run_id es la pieza que une CI, Kubernetes y el proveedor de correo. También evita confundir un mensaje viejo con el mensaje de esta ejecución. En proyectos donde la activación SaaS forma parte del flujo, conviene seguir el hilo de activación del email con el mismo identificador.
Hay una regla de privacidad importante: evidencia no significa copiar el email entero en cada log. Guarda metadatos, un hash del destinatario si hace falta y la razón de la decisión. El enlace a un generador de correo desechable puede servir para preparar una bandeja aislada, pero no convierte una bandeja compartida en evidencia confiable.
Cómo implementarlo en un Job de Kubernetes
El test puede escribir un archivo JSON al final de cada intento. Un sidecar o un paso posterior lo envía a un almacenamiento de artefactos con retención corta. El Job debe conservar el estado aunque el proceso de prueba termine con error.
apiVersion: batch/v1 kind: Job metadata: generateName: email-ci- spec: backoffLimit: 0 template: spec: restartPolicy: Never containers: - name: browser-tests image: registry.example.test/email-tests:2026-11-04 env: - name: RUN_ID valueFrom: fieldRef: fieldPath: metadata.uid - name: EVIDENCE_PATH value: /artifacts/email-evidence.json volumeMounts: - name: artifacts mountPath: /artifacts volumes: - name: artifacts emptyDir: {} El ejemplo deja el archivo dentro del ciclo de vida del pod, por eso un paso de CI debe copiarlo antes de borrar el Job. En producción, un bucket con retención de unas horas suele ser suficiente. El acceso debe limitarse al equipo que depura el pipeline; una bandeja de prueba todavía puede contener tokens.
También conviene registrar el estado de la espera, no solo el resultado final. Por ejemplo, poll_count, last_seen_at y provider_request_id ayudan a separar una cola lenta de una dirección mal configurada. Los logs no cuenta toda la historia si el cliente de polling no deja su ventana temporal.
Qué mirar durante el incidente
Empiezo por el run_id y sigo una ruta fija:
- Compruebo que el Job se ejecutó una sola vez y que no hubo un reinicio silencioso.
- Comparo
service_versioncon el despliegue que estaba activo cuando se disparó el flujo. - Busco el
provider_request_iden la salida del servicio de correo. - Reviso si la bandeja recibió un mensaje dentro de la ventana, no solo si contiene algún mensaje.
- Comparo el asunto y el destinatario enmascarado con la expectativa del test.
Si la aplicación tiene una cola, separo “mensaje aceptado por la aplicación” de “mensaje visible en la bandeja”. Esa latencía puede ser normal, pero debe tener un límite explícito. Si no lo tiene, cada equipo inventa su propio timeout y el diagnóstico se vuelve inconsistente.
Los estados visuales del flujo también importan para el test y para quien lo lee; los estados claros para los flujos de email son una buena referencia para no llamar “error” a una espera todavía activa.
Checklist para SRE antes de cerrar el pipeline
- ¿Cada intento tiene un
run_idy una bandeja exclusiva? - ¿La ventana de polling empieza después del evento disparador?
- ¿Se conservan los metadatos aunque el test termine en error?
- ¿Podemos distinguir aplicación, cola, proveedor y bandeja?
- ¿Los tokens y cuerpos completos quedan fuera de los logs?
- ¿La retención es suficiente para investigar, pero no indefinida?
- ¿Un reintento crea evidencia nueva en vez de sobrescribir la anterior?
El objetivo no es hacer que el CI parezca mas sofisticado. Es que una alerta se pueda explicar. Cuando el contrato existe, un fallo de email deja de ser “Kubernetes perdió un mensaje” y pasa a ser una cadena comprobable de eventos.
Preguntas y respuestas
¿Debo guardar el cuerpo completo del email?
No como comportamiento por defecto. Guarda asunto, identificadores, marcas de tiempo y una razón de aceptación. Solo conserva una muestra controlada cuando una investigación lo requiere y con acceso restringido.
¿Qué hago si el proveedor no ofrece un request ID?
Genera un correlation_id en el servicio antes de encolar el mensaje y pásalo a los metadatos disponibles. Si no puedes pasarlo al proveedor, al menos relaciona la versión, el destinatario enmascarado y la hora.
¿Cuánto tiempo debo conservar la evidencia?
Lo suficiente para cubrir el tiempo normal de detección y análisis del equipo. Para muchos pipelines, unas horas o un día es razonable. Ajusta la duración a tus obligaciones de auditoría y borra después lo que ya no aporta.
