El cliente llegó con una factura de AWS que había pasado de $4.200 a $6.800 en seis meses. El equipo técnico no sabía explicar el salto. Lo primero que dijeron: “debemos tener más tráfico”. Lo segundo que dijeron, tres semanas después de nuestra auditoría: “no teníamos ni idea de lo que estábamos pagando”.

Este artículo documenta el proceso completo: qué buscamos, cómo lo medimos, y el plan que ejecutamos. Los comandos son reales. Los números son reales. Puedes replicar exactamente esto en tu cuenta esta semana.

El punto de partida: Cost Explorer no es suficiente

El primer error que cometen los equipos es mirar solo el dashboard de Cost Explorer y sacar conclusiones de los números agregados. Los servicios caros no siempre son los culpables. Lo que mata el presupuesto son los recursos pequeños y olvidados: funciones Lambda que nadie llama, instancias RDS del doble de lo necesario, buckets S3 que acumulan datos sin límite.

Antes de tocar nada, establecimos un baseline claro: exportamos el gasto por servicio y por recurso individual. Con AWS Cost and Usage Reports activado a nivel de recurso, se puede ver el coste de cada función Lambda de forma individual.

Paso 1: identificar Lambda sin invocaciones reales

Empezamos listando todas las funciones de la cuenta y cruzando con CloudWatch para ver cuántas invocaciones tuvieron en los últimos 30 días:

# Listar todas las funciones
aws lambda list-functions --query 'Functions[*].FunctionName' --output text

# Para cada función, consultar invocaciones en los últimos 30 días
aws cloudwatch get-metric-statistics \
  --namespace AWS/Lambda \
  --metric-name Invocations \
  --dimensions Name=FunctionName,Value=NOMBRE_FUNCION \
  --start-time $(date -d '30 days ago' --iso-8601) \
  --end-time $(date --iso-8601) \
  --period 2592000 \
  --statistics Sum

Resultado: de 47 funciones Lambda activas, 23 tenían cero invocaciones en los últimos 30 días. Otras 8 tenían menos de 10 invocaciones al mes. Eran funciones de utilidades, procesos batch que se migraron y nadie eliminó, y entornos de prueba que se olvidaron.

El gasto no venía de las invocaciones (eso es gratis si no se llama) sino de la memoria reservada en Provisioned Concurrency. Alguien había activado Provisioned Concurrency en funciones que no lo necesitaban “para que fueran más rápidas”, y eso se cobra por hora independientemente del uso.

Gasto recuperable identificado: $1.840/mes.

Paso 2: rightsizing de RDS

La base de datos principal era una instancia db.r5.2xlarge con 8 vCPUs y 64 GB de RAM. Consultamos las métricas de los últimos 60 días:

aws cloudwatch get-metric-statistics \
  --namespace AWS/RDS \
  --metric-name CPUUtilization \
  --dimensions Name=DBInstanceIdentifier,Value=prod-main-db \
  --start-time $(date -d '60 days ago' --iso-8601) \
  --end-time $(date --iso-8601) \
  --period 86400 \
  --statistics Average,Maximum

CPU media: 8%. Pico máximo registrado: 31%. Una db.r5.xlarge (4 vCPUs, 32 GB RAM) habría absorbido esa carga con margen. El gasto de la instancia actual: $960/mes. El gasto de la instancia correcta: $480/mes.

Gasto recuperable: $480/mes.

Paso 3: S3 sin lifecycle policies

Tres buckets S3 de logs y backups sin ninguna política de retención. El bucket de logs de aplicación tenía objetos desde hace cuatro años. Los backups de base de datos guardaban los últimos 1.095 días en lugar de 90.

La solución es una lifecycle policy estándar:

{
  "Rules": [
    {
      "ID": "delete-old-logs",
      "Status": "Enabled",
      "Filter": { "Prefix": "app-logs/" },
      "Expiration": { "Days": 90 },
      "Transitions": [
        { "Days": 30, "StorageClass": "STANDARD_IA" },
        { "Days": 60, "StorageClass": "GLACIER_IR" }
      ]
    }
  ]
}

Gasto recuperable inmediato: $340/mes. Con el lifecycle aplicado, se estabiliza en ~$60/mes.

El plan de ejecución (4 semanas)

  • Semana 1: eliminar Provisioned Concurrency en las funciones ociosas y borrar las funciones sin uso tras validar con el equipo
  • Semana 2: snapshot de RDS, crear instancia db.r5.xlarge, test de carga, migración con ventana de mantenimiento de 5 minutos
  • Semana 3: aplicar lifecycle policies en todos los buckets S3 afectados
  • Semana 4: configurar alertas de presupuesto en AWS Budgets y dashboard de gasto continuo por servicio

Resultado a los 60 días

Factura mensual: de $6.800 a $4.060. Ahorro sostenido: $2.740/mes, o $32.880 anuales. El equipo no notó ninguna degradación de rendimiento. La RDS migrada tuvo mejor latencia media porque la instancia anterior estaba infrautilizando la caché de memoria.

La lección más importante: el gasto en AWS no se gestiona, se audita de forma activa. Los recursos ociosos no desaparecen solos. Cada mes que pasan sin revisión son euros que salen sin retorno.