Problema

Los entornos que generan gran volumen de eventos en CloudWatch Logs suelen ver que la factura de ingestión supera con creces el costo de almacenamiento. Cada gigabyte de datos enviados a CloudWatch se factura a alrededor de $0.50, mientras que el mismo dato comprimido y guardado en S3 cuesta menos de $0.04 por mes. Cuando los logs son escritos intensamente y rara vez consultados, la arquitectura tradicional de CloudWatch se vuelve ineficiente: se paga por la ingestión aunque la mayor parte del valor reside en la retención a largo plazo.

Este patrón se repite en pipelines de Lambda, contenedores ECS/Fargate y agentes de Fluent Bit que envían logs en tiempo real. El reto es mantener la visibilidad para alertas críticas y, al mismo tiempo, evitar que la ingestión masiva de datos “fríos” incremente la factura.

Causa

  1. Modelo de facturación de CloudWatch – La tarifa de ingestión se aplica a cada evento, sin importar su tamaño. Workloads con muchos eventos pequeños (por ejemplo, métricas de latencia o trazas de depuración) generan un sobrecosto significativo por el overhead de 26 B por evento.

  2. Retención predeterminada larga – La mayoría de los equipos configuran retenciones de 30‑90 días por defecto. Los datos que ya no son relevantes para alertas siguen consumiendo capacidad de ingestión y almacenamiento.

  3. Ausencia de filtrado en el agente – Herramientas como el CloudWatch Agent o Fluent Bit envían todo al endpoint de AWS sin posibilidad de redirigir flujos que no requieran análisis inmediato.

  4. Falta de capa de archivado – Sin un proceso que copie o mueva los logs a un almacenamiento más barato, el único punto de referencia sigue siendo CloudWatch.

Solución

Una arquitectura de dos capas permite separar los flujos “calientes” de los “fríos”. La capa caliente sigue enviando eventos a CloudWatch para que las alarmas y dashboards funcionen sin cambios. La capa fría redirige los eventos a S3, donde se almacenan comprimidos en formato zstd y se organizan en ventanas horarias alineadas a UTC. La solución se puede implementar con cualquier herramienta que exponga la API mínima de CloudWatch Logs (PutLogEvents, CreateLogGroup, etc.) y que permita configurar un endpoint alternativo. En la práctica, una herramienta Rust de código abierto cumple con estos requisitos y aporta bajo consumo de recursos.

Pasos clave

  1. Seleccionar o compilar la herramienta – Descarga el binario estático o compílalo con cargo install. El binario incluye dos modos:

    • Drain: extrae logs existentes mediante FilterLogEvents y los escribe en S3. Cada ventana de una hora genera un objeto y un manifiesto JSON que facilita la re‑reproducción.
    • Gateway: actúa como un proxy que acepta las llamadas de la API de CloudWatch y las persiste directamente en S3. Opcionalmente, puede reenviar los eventos a CloudWatch para grupos que requieran alertas.
  2. Configurar el bucket S3 – Crea un bucket dedicado al archivado y define una política de ciclo de vida que migre objetos a GLACIER_IR después de 30 días si el coste es crítico. Mantén los manifiestos en la clase STANDARD para que la reconstitución sea rápida.

  3. Definir reglas de enrutamiento – Un archivo routing.toml permite especificar, por expresión de nombre de log group, si el tráfico se envía a S3, a CloudWatch o se descarta. La regla “first‑match wins” simplifica la gestión de excepciones (por ejemplo, logs de seguridad que deben permanecer en CloudWatch).

  4. Desplegar el gateway – Ejecuta el binario como servicio (systemd, Docker o Kubernetes). Usa TLS para proteger el endpoint y, si se requiere, habilita la verificación de firmas SIGV4 (--auth-mode sigv4). En entornos donde el gateway está expuesto a internet, habilita ACME para obtener certificados automáticamente.

  5. Re‑configurar agentes y SDK – Cambia la opción endpoint de los agentes de CloudWatch (Fluent Bit, CloudWatch Agent) o de los SDK de la aplicación para que apunten al host del gateway. No es necesario modificar el código de la aplicación siempre que la API sea compatible.

  6. Ejecutar el proceso de drenado – Programa una tarea (cron, Step Functions) que invoque s4logs drain para copiar los logs históricos que ya están en CloudWatch. Usa la opción --reconcile para procesar ventanas atrasadas y --apply-retention solo cuando el manifiesto cubra todo el rango, evitando pérdidas accidentales.

Alternativas prácticas

  • Uso de Lambda como bridge – Si la carga es baja, una función Lambda que reciba eventos de CloudWatch y los escriba en S3 puede reemplazar al gateway, aunque la latencia y el coste de invocaciones pueden ser mayores.
  • S3 EventBridge – Configura una regla que capture los PutLogEvents de CloudWatch (a través de CloudTrail) y dispare una función que archive los datos. Esta opción es menos directa y depende de la disponibilidad de logs en CloudTrail.

Cuándo aplicar esta solución

  • Volúmenes de ingestión > 200 GB/mes y la mayor parte del gasto proviene de la tarifa de ingestión.
  • Patrones de acceso donde los logs son consultados poco después de su generación (p.ej., alertas) y luego quedan inactivos.
  • Entornos con múltiples equipos que comparten un bucket de archivado y necesitan un punto de control centralizado.
  • No aplicar cuando los logs deben estar disponibles en tiempo real para dashboards críticos y la latencia de S3 no es aceptable, o cuando la política de retención de CloudWatch es obligatoria por requisitos regulatorios.

Código

# Instalar la herramienta desde el repositorio
cargo install --git https://github.com/abyo-software/s4-logs s4logs-cli

# Ejecutar el gateway con TLS autogestionado (ACME)
s4logs serve \
  --listen 0.0.0.0:443 \
  --bucket my-archive-bucket \
  --prefix logs \
  --account 123456789012 \
  --acme-domain logs.example.com \
  --acme-contact [email protected] \
  --acme-cache-dir /var/lib/s4logs/acme \
  --auth-mode sigv4

# Configurar Fluent Bit para usar el gateway
cat > /etc/fluent-bit/conf.d/cloudwatch.conf <<EOF
[OUTPUT]
    Name cloudwatch_logs
    Match *
    region us-east-1
    log_group_name /aws/lambda/payments
    log_stream_prefix fluent-bit-
    endpoint https://logs.example.com
EOF

# Drenar logs históricos de un grupo específico
s4logs drain \
  --log-group /aws/lambda/payments \
  --bucket my-archive-bucket \
  --prefix logs \
  --account 123456789012 \
  --storage-class glacier-ir \
  --concurrency 4

Verificación

  1. Confirmar que el gateway responde – Ejecuta curl -k https://logs.example.com/health. El cuerpo debe contener {"status":"ok"}.
  2. Enviar un evento de prueba – Usa AWS CLI:
    aws logs put-log-events \
      --log-group-name /aws/lambda/payments-test \
      --log-stream-name test-stream \
      --log-events timestamp=$(date +%s%3N),message="test event" \
      --endpoint-url https://logs.example.com
    
    Verifica que el objeto aparece en S3 bajo la ruta logs/<YYYY>/<MM>/<DD>/<HH>/.
  3. Revisar el manifiesto – Descarga el archivo manifest.json de la ventana correspondiente y comprueba que incluye el event_id del mensaje enviado.
  4. Medir reducción de costos – Después de 30 días, compara la factura de CloudWatch Ingest con la de S3. La diferencia debería acercarse al factor de compresión (≈6×) más la reducción de tarifa de ingestión.

Notas adicionales

  • Persistencia del WAL – Si el gateway se ejecuta con --wal-dir, los eventos se sincronizan en disco antes de responder al cliente. Esto garantiza “al menos una entrega” pero aumenta el uso de I/O. En entornos con SSD rápido, habilitarlo es la opción más segura.
  • Gestión de claves SIGV4 – Al usar --auth-mode sigv4, exporta S4LOGS_AUTH_ACCESS_KEY y S4LOGS_AUTH_SECRET en el entorno del proceso. Cambia las credenciales periódicamente para cumplir con buenas prácticas de rotación.
  • Limitar el tamaño de los objetos – El formato RFC 8878 permite dividir los logs en marcos de 4 MiB. Si la carga supera este límite, la herramienta crea varios objetos dentro de la misma ventana horaria.
  • Compatibilidad con Athena – Los archivos .zst pueden ser consultados directamente con una tabla externa en Athena usando la definición de serde para zstd. La documentación del proyecto incluye un ejemplo de CREATE EXTERNAL TABLE.
  • Monitoreo interno – El binario expone métricas Prometheus bajo /metrics. Integra estas métricas en CloudWatch o Grafana para detectar cuellos de botella en la ingestión o en la escritura a S3.