El módulo 4 terminó con una frase incómoda: está todo montado, y nadie está mirando. MercadoFresco tiene una tienda que escala sola, una base de datos replicada, una CDN que sirve el 96 % del tráfico, identidades mínimas, datos cifrados y dos cortafuegos de aplicación. Y sin embargo, si el viernes a las 19:10 la tienda empieza a devolver errores, la primera persona en enterarse será un cliente por Twitter.
Amazon CloudWatch es el servicio que resuelve eso. No es «los gráficos de AWS»: es el repositorio
central donde todos los servicios que has montado escriben lo que les pasa, el sitio donde se
almacenan los registros de tu aplicación, y el motor que decide cuándo despertar a alguien. Marta lo
lleva usando de refilón desde 02-01 —cada vez que hemos mirado CPUUtilization o creado una alarma
estábamos en CloudWatch— pero nunca lo hemos montado en serio.
Esta lección lo monta en serio. Al final, MercadoFresco tendrá un panel que Marta abre cada mañana con el café, métricas de negocio propias, todos sus registros en un solo sitio consultables con SQL-ish, y —lo más importante— la certeza comprobada de que un aviso llega a un teléfono a las cuatro de la madrugada.
Aviso de coste. CloudWatch es de los servicios de AWS que más fácilmente se descontrolan, y casi nunca por las métricas: por los registros. Un grupo de registros con retención infinita, o una aplicación con
DEBUGactivado en producción, pueden costar más que las instancias EC2 que los generan. Cada sección de esta lección indica su coste, y hay una sección entera dedicada a ello.
Contenido
- Qué es CloudWatch y qué no es
- Los tres pilares
- Métricas: espacios de nombres y dimensiones
- Resolución estándar frente a alta resolución
- Estadísticas: por qué la media miente
- Periodos, agregación y retención
- Las métricas que importan de verdad de lo que ya está montado
- Por qué EC2 no publica el uso de memoria
- Publicar métricas personalizadas con boto3
PutMetricDatapor lotes y valores estadísticos- El agente unificado de CloudWatch en las instancias del ASG
- Embedded Metric Format desde Lambda
- Registros: grupos, flujos y retención
- Los registros que MercadoFresco tiene dispersos
- Logs Insights: sintaxis y consultas reales
- Responder a «¿por qué tardó ocho segundos ese pedido?»
- Filtros de métricas: de un patrón de log a una alarma
- Suscripciones y exportación a S3
- Alarmas: umbrales, evaluación y datos faltantes
- Alarmas compuestas
- Detección de anomalías
- Acciones de alarma
- Comprobar que el aviso llega de verdad
- El panel
mercadofresco-produccion - ServiceLens y Synthetics
- Coste de CloudWatch y cómo se dispara
- Limpieza
Qué es CloudWatch y qué no es
CloudWatch es un servicio regional de observabilidad que hace tres cosas: almacenar series temporales numéricas (métricas), almacenar texto con marca de tiempo (registros), y evaluar condiciones sobre lo anterior para disparar acciones (alarmas).
Lo que no es, y conviene fijarlo desde el principio porque evita mucha confusión:
| CloudWatch no es | Eso lo hace | Se ve en |
|---|---|---|
| Un registro de quién llamó a la API de AWS | CloudTrail | 05-03 |
| Un rastreador de peticiones entre servicios | X-Ray | 05-02 |
| Un evaluador del cumplimiento de configuraciones | AWS Config | 05-04 |
| Un bus de eventos para automatizar reacciones | EventBridge | 07-03 |
| Un almacén analítico para consultas históricas grandes | Athena / Redshift | 05-03, 06-04 |
La confusión más común es la primera. Si Luis busca «quién borró el bucket», eso no está en CloudWatch: está en CloudTrail. CloudWatch guarda lo que tu aplicación y los servicios dicen de sí mismos, no quién habló con la API de AWS.
Históricamente el bus de eventos se llamó «CloudWatch Events» y todavía verás ese nombre en documentación antigua y en algunas respuestas de la CLI. Hoy es EventBridge y se estudia en 07-03; esta lección no lo toca.
Los tres pilares
flowchart LR
subgraph Origenes["Origenes"]
EC2["EC2 / ASG"]
ALB["ALB"]
RDS["RDS"]
LAM["Lambda"]
CF["CloudFront"]
APP["Codigo de la tienda"]
end
EC2 --> M["METRICAS<br/>series temporales"]
ALB --> M
RDS --> M
LAM --> M
CF --> M
APP --> M
EC2 --> L["REGISTROS<br/>texto con marca de tiempo"]
LAM --> L
APP --> L
ALB -.->|"a S3, no a Logs"| S3["S3"]
L -->|"filtro de metricas"| M
M --> A["ALARMAS<br/>umbral + evaluacion"]
A --> SNS["SNS alertas-mercadofresco"]
A --> ASG["Politica de autoescalado"]
A --> EC2ACT["Accion sobre la instancia"]
Fíjate en la flecha que va de registros a métricas: un filtro de métricas convierte un patrón de
texto en una serie numérica. Es el puente que permite alarmar sobre algo que solo aparece en un log,
como ERROR pago rechazado. Y fíjate también en que el ALB escribe sus registros de acceso en S3,
no en CloudWatch Logs: eso tendrá consecuencias en la sección de Logs Insights.
Métricas: espacios de nombres y dimensiones
Una métrica es una serie temporal identificada por tres cosas:
- Espacio de nombres (namespace): el contenedor. Los de AWS empiezan por
AWS/(AWS/EC2,AWS/ApplicationELB,AWS/RDS,AWS/Lambda,AWS/S3,AWS/CloudFront). El tuyo no puede empezar porAWS/: MercadoFresco usaMercadoFresco/Tienda. - Nombre de la métrica:
CPUUtilization,TargetResponseTime,PedidosConfirmados. - Dimensiones: hasta 30 pares clave/valor que identifican de qué es la medida.
InstanceId=i-0abc...,LoadBalancer=app/alb-mercadofresco-tienda/...,DBInstanceIdentifier=mercadofresco-pedidos.
El detalle que más despista al principio: cada combinación distinta de dimensiones es una métrica diferente y se factura por separado. Estas son tres métricas, no una:
MercadoFresco/Tienda PedidosConfirmados (sin dimensiones) MercadoFresco/Tienda PedidosConfirmados Entorno=produccion MercadoFresco/Tienda PedidosConfirmados Entorno=produccion, Provincia=Madrid
Y CloudWatch no agrega automáticamente entre ellas. Si publicas solo la tercera y luego consultas la primera, no habrá datos. Esto tiene dos consecuencias prácticas:
- Si quieres el total y también el desglose, publica ambos.
- Nunca uses como dimensión algo de cardinalidad alta. Poner
PedidoIdcomo dimensión crea una métrica nueva por pedido: 900 métricas a la hora, 650.000 al mes, a 0,30 USD cada una. Eso son más de 190.000 USD mensuales por un descuido de una línea. La identidad de un pedido va en un registro o en una anotación de X-Ray (05-02), nunca en una dimensión.
Regla de MercadoFresco: las dimensiones son categorías cerradas y pequeñas —entorno, componente, tipo de pago, provincia— nunca identificadores.
Resolución estándar frente a alta resolución
| Estándar | Alta resolución | |
|---|---|---|
| Granularidad mínima | 60 segundos | 1 segundo |
| Cómo se pide | Por defecto | StorageResolution=1 al publicar |
| Periodos de alarma | 60 s y múltiplos | 10 s y 30 s también |
| Coste de la métrica | 0,30 USD/mes | 0,30 USD/mes (igual) |
| Coste de la alarma | 0,10 USD/mes | 0,30 USD/mes |
| Retención de los datos de 1 s | — | 3 horas, luego se agrega |
La alta resolución sirve para procesos que cambian en segundos: una cola que se llena, un pico de latencia de 20 segundos que a resolución de minuto queda diluido en la media. MercadoFresco no la usa: sus problemas se ven perfectamente a un minuto, y las alarmas de 10 segundos generan ruido nocturno. Es una herramienta de diagnóstico puntual, no un valor por defecto.
Las métricas básicas de EC2 son de 5 minutos salvo que actives la monitorización detallada
(2,10 USD por instancia y mes en eu-west-1, aproximadamente), que las baja a 1 minuto. Para el ASG
de MercadoFresco sí merece la pena: con datos de 5 minutos, el escalado reacciona tarde al pico de los
viernes.
Estadísticas: por qué la media miente
CloudWatch no guarda cada punto individual: guarda agregados por periodo. Al consultar eliges qué agregado quieres:
| Estadística | Qué responde | Cuándo usarla |
|---|---|---|
Sum |
¿Cuánto en total? | Contadores: RequestCount, PedidosConfirmados, Errors |
Average |
¿Cuánto de media? | Utilización: CPUUtilization, CacheHitRate |
Minimum / Maximum |
El extremo | Capacidad: FreeStorageSpace mínimo, conexiones máximas |
SampleCount |
¿Cuántos puntos? | Diagnóstico de huecos |
p50, p90, p95, p99 |
¿Qué experimenta el percentil N? | Latencia, siempre |
TM(5%:95%), TC, WM |
Media recortada, ignorando extremos | Análisis fino |
El error clásico —y el que más incidentes esconde— es alarmar sobre Average de una latencia. Un
minuto real de la tienda de MercadoFresco:
| Peticiones | Tiempo de respuesta |
|---|---|
| 950 | 0,08 s (páginas cacheadas) |
| 40 | 0,4 s (fichas de producto) |
| 10 | 9,5 s (confirmación de pedido) |
Average = 0,17 s. Perfecto. Verde. Ninguna alarma salta.
p99 = 9,4 s. Diez clientes por minuto están viendo una rueda girar durante casi diez segundos
mientras intentan pagar. Son exactamente los clientes que importan: los que están comprando.
Regla: para cualquier métrica de latencia, alarma sobre
p95op99, nunca sobreAverage. La media te dice cómo va el servidor; el percentil te dice cómo lo vive el cliente.
Un matiz importante: los percentiles solo están disponibles si la métrica se publica con datos
suficientes. Las métricas del ALB los soportan de forma nativa. Para métricas propias, hay que
publicar valores individuales o usar StatisticValues con cuidado (lo veremos: los StatisticValues
no permiten percentiles, porque ya llegan agregados).
Periodos, agregación y retención
El periodo es la ventana de agregación de la consulta o la alarma: 60 s, 300 s, 3600 s… Es independiente de la frecuencia con la que publicas. Si publicas cada 10 segundos y consultas con periodo 300, CloudWatch agrega 30 puntos en uno.
La retención es automática, gratuita y no configurable:
| Antigüedad del dato | Resolución conservada |
|---|---|
| 0 – 3 horas | 1 segundo (solo alta resolución) |
| 0 – 15 días | 1 minuto |
| 15 – 63 días | 5 minutos |
| 63 – 455 días | 1 hora |
| Más de 15 meses | Se borra |
Dos consecuencias operativas:
- No puedes consultar con periodo 60 un día de hace dos meses. Los datos ya están agregados a 5 minutos. Si necesitas comparar el Black Friday del año pasado al minuto, tienes que haberlo exportado antes.
- Marta exporta cada mes las métricas de negocio (
PedidosConfirmados) amercadofresco-informes-analiticaconget-metric-data, precisamente para que Sara pueda comparar campañas de años distintos.
Las métricas que importan de verdad de lo que ya está montado
Esta es la tabla que Marta se ha impreso. De los cientos de métricas que publican los servicios de MercadoFresco, estas son las que de verdad deciden algo:
| Servicio | Métrica | Estadística | Qué significa | Umbral de MercadoFresco |
|---|---|---|---|---|
| EC2 / ASG | CPUUtilization |
Average |
Uso de CPU | Escalado a 60 % (política cpu-objetivo-60) |
| EC2 / ASG | StatusCheckFailed_Instance |
Maximum |
La instancia está rota | ≥ 1 durante 2 periodos |
| EC2 / ASG | GroupInServiceInstances |
Maximum |
Instancias activas | = 4 → mercadofresco-asg-al-maximo |
| ALB | TargetResponseTime |
p95 |
Latencia vista por el cliente | > 2 s durante 3 min |
| ALB | HTTPCode_ELB_5XX_Count |
Sum |
Errores del balanceador | > 10 en 5 min |
| ALB | HTTPCode_Target_5XX_Count |
Sum |
Errores de tu aplicación | > 25 en 5 min |
| ALB | HealthyHostCount |
Minimum |
Instancias sanas por grupo | < 2 |
| ALB | UnHealthyHostCount |
Maximum |
Instancias fallando /salud |
≥ 1 |
| ALB | RejectedConnectionCount |
Sum |
El ALB rechaza por falta de capacidad | > 0 |
| RDS | DatabaseConnections |
Maximum |
Conexiones abiertas | > 160 de 200 |
| RDS | FreeStorageSpace |
Minimum |
Disco libre en bytes | < 20 GB |
| RDS | ReadLatency / WriteLatency |
Average |
Latencia de disco en segundos | > 0,02 s |
| RDS | CPUUtilization |
Average |
CPU de la instancia | > 80 % |
| RDS | ReplicaLag |
Maximum |
Retraso de la réplica de lectura | > 30 s |
| RDS | FreeableMemory |
Minimum |
Memoria disponible | < 500 MB |
| Lambda | Errors |
Sum |
Invocaciones fallidas | > 5 en 5 min |
| Lambda | Duration |
p99 |
Tiempo de ejecución | > 80 % del timeout |
| Lambda | Throttles |
Sum |
Invocaciones rechazadas por concurrencia | > 0 |
| Lambda | ConcurrentExecutions |
Maximum |
Ejecuciones simultáneas | > 70 % de la cuota |
| Lambda | IteratorAge |
Maximum |
Retraso en orígenes de flujo | > 60 s |
| S3 | BucketSizeBytes |
Average |
Tamaño (diario) | Tendencia, no alarma |
| S3 | NumberOfObjects |
Average |
Objetos (diario) | Tendencia |
| S3 | 4xxErrors / 5xxErrors |
Sum |
Requiere métricas de petición (de pago) | > 1 % |
| CloudFront | CacheHitRate |
Average |
% servido desde el borde | < 50 % → alarma |
| CloudFront | 5xxErrorRate |
Average |
Errores devueltos | > 1 % |
| CloudFront | OriginLatency |
p95 |
Lo que tarda tu origen | > 1 s |
| WAF | BlockedRequests |
Sum |
Peticiones bloqueadas | Anomalía |
Tres observaciones que separan a quien entiende esto de quien copia umbrales:
HTTPCode_ELB_5XX_CountyHTTPCode_Target_5XX_Countno son lo mismo. El primero significa que el balanceador no pudo entregar la petición a nadie (no hay destinos sanos, tiempo de espera agotado). El segundo significa que tu aplicación respondió 500. Confundirlos hace perder horas buscando en el sitio equivocado.FreeStorageSpacede RDS está en bytes. El umbral de 20 GB se escribe21474836480. Un cero de más y la alarma no salta nunca.Throttlesde Lambda con umbral > 0, no > 10. Un solo estrangulamiento significa que has tocado el techo de concurrencia y hay clientes viendo errores. No hay un «poco de throttling aceptable».
Por qué EC2 no publica el uso de memoria
Es la pregunta que hace todo el mundo al llegar aquí, y la respuesta explica cómo funciona la nube.
El hipervisor de AWS ve la máquina virtual desde fuera. Puede medir CPU consumida, tráfico de red,
operaciones de disco del volumen EBS: todo eso pasa por su capa. Pero la memoria libre es un
concepto del sistema operativo invitado. Solo Linux sabe cuánta RAM está en buff/cache y podría
liberarse, y eso el hipervisor no lo puede saber sin entrar dentro.
Lo mismo pasa con el espacio libre del disco: EBS es un dispositivo de bloques, y solo el sistema de ficheros de dentro sabe qué está ocupado.
Consecuencia práctica: para memoria y disco de EC2 necesitas instalar un agente dentro de la instancia. Se hace más abajo. Y consecuencia de diseño: por eso Lambda, Fargate y RDS —donde AWS gestiona el sistema operativo— sí publican memoria sin que hagas nada.
Publicar métricas personalizadas con boto3
Las métricas de infraestructura dicen cómo va la máquina. Las de negocio dicen cómo va la empresa,
y son las que de verdad detectan incidentes. En 04-04 vimos por qué: si llegan 5.000 peticiones por
segundo y PedidosPorHora no se mueve, no son clientes.
MercadoFresco ya publicaba PedidosPorHora. Ahora añadimos las dos que faltan:
| Métrica | Unidad | Estadística útil | Para qué |
|---|---|---|---|
PedidosConfirmados |
Count |
Sum |
Volumen de negocio en tiempo real |
TiempoConfirmacionPedido |
Milliseconds |
p95, p99 |
Experiencia de compra |
El código, en la tienda:
"""Publicacion de metricas de negocio de MercadoFresco.
Se ejecuta dentro de las instancias del ASG, que asumen el rol
rol-mercadofresco-tienda. Ese rol tiene cloudwatch:PutMetricData
restringido al espacio de nombres MercadoFresco/Tienda (04-01).
"""
import time
import boto3
from botocore.config import Config
# Reintentos en modo adaptativo: PutMetricData es idempotente para
# nuestro caso y no queremos que un fallo de red tumbe una compra.
cw = boto3.client(
"cloudwatch",
region_name="eu-west-1",
config=Config(retries={"max_attempts": 3, "mode": "adaptive"}),
)
ESPACIO = "MercadoFresco/Tienda"
def registrar_pedido_confirmado(importe_eur, milisegundos, metodo_pago, provincia):
"""Publica las metricas de un pedido recien confirmado."""
dimensiones_base = [
{"Name": "Entorno", "Value": "produccion"},
{"Name": "Componente", "Value": "tienda"},
]
cw.put_metric_data(
Namespace=ESPACIO,
MetricData=[
# 1. Contador global, sin dimensiones ademas de las base.
{
"MetricName": "PedidosConfirmados",
"Dimensions": dimensiones_base,
"Value": 1,
"Unit": "Count",
"Timestamp": time.time(),
},
# 2. Desglose por metodo de pago: cardinalidad baja y cerrada.
{
"MetricName": "PedidosConfirmados",
"Dimensions": dimensiones_base + [
{"Name": "MetodoPago", "Value": metodo_pago},
],
"Value": 1,
"Unit": "Count",
},
# 3. Latencia de negocio: el tiempo que el cliente ha esperado.
{
"MetricName": "TiempoConfirmacionPedido",
"Dimensions": dimensiones_base,
"Value": milisegundos,
"Unit": "Milliseconds",
},
# 4. Importe, para detectar pedidos anomalos.
{
"MetricName": "ImportePedido",
"Dimensions": dimensiones_base,
"Value": importe_eur,
"Unit": "None",
},
],
)Detalles que importan y que no son obvios:
Timestampes opcional; si no lo pones, CloudWatch usa la hora de recepción. Puedes publicar datos con hasta 2 semanas de antigüedad y hasta 2 horas en el futuro. Esto permite recuperar métricas de un proceso por lotes que falló.- La unidad importa poco a CloudWatch pero mucho a las alarmas. Si publicas en
Millisecondsy la alarma esperaSeconds, la alarma no encuentra datos y se queda enINSUFFICIENT_DATApara siempre. Sé consistente. - Publicar
Value: 1repetidamente es correcto. CloudWatch suma los valores del periodo cuando consultas conSum. No hace falta que la aplicación mantenga un contador. provinciano se usa como dimensión en este ejemplo. España tiene 52 provincias: sería aceptable, pero multiplicaría por 52 el número de métricas (15,60 USD/mes solo por eso). Marta decidió que ese desglose vive en los informes de Sara sobremercadofresco-informes-analitica, no en CloudWatch.
El error de rendimiento que casi nadie ve venir
El código anterior hace una llamada HTTPS a la API de CloudWatch dentro de la ruta crítica de una compra. Con 900 pedidos/hora funciona; con un pico serio, esa llamada de 40 ms se suma al tiempo de respuesta que el cliente percibe, y si la API de CloudWatch se ralentiza, tu tienda se ralentiza.
Tres soluciones, de peor a mejor:
- Envolver en
try/excepty no fallar nunca. Mínimo indispensable: una métrica no debe tumbar una venta. - Acumular en memoria y publicar por lotes cada 20 segundos desde un hilo aparte (siguiente sección).
- Escribir la métrica en el log en formato EMF y dejar que CloudWatch la extraiga (sección de Lambda). Coste cero en la ruta crítica.
PutMetricData por lotes y valores estadísticos
PutMetricData acepta hasta 1.000 puntos por llamada (con un límite de 1 MB de cuerpo). Publicar
por lotes reduce el coste de API (0,01 USD por cada 1.000 llamadas) y saca la latencia de la ruta
crítica.
"""Publicador por lotes: acumula en memoria y vuelca cada 20 segundos."""
import threading
import queue
import boto3
cw = boto3.client("cloudwatch", region_name="eu-west-1")
_cola = queue.Queue(maxsize=10000)
def encolar(nombre, valor, unidad="Count", dimensiones=None):
"""No bloquea nunca: si la cola esta llena, se descarta el punto."""
try:
_cola.put_nowait({
"MetricName": nombre,
"Value": valor,
"Unit": unidad,
"Dimensions": dimensiones or [
{"Name": "Entorno", "Value": "produccion"},
{"Name": "Componente", "Value": "tienda"},
],
})
except queue.Full:
pass # Perder una metrica es preferible a bloquear una compra.
def _volcar():
while True:
lote = []
# Espera bloqueante para el primer elemento; el resto sin esperar.
lote.append(_cola.get())
while len(lote) < 1000:
try:
lote.append(_cola.get_nowait())
except queue.Empty:
break
try:
for i in range(0, len(lote), 1000):
cw.put_metric_data(
Namespace="MercadoFresco/Tienda",
MetricData=lote[i:i + 1000],
)
except Exception as e: # noqa: BLE001
print(f"No se pudieron publicar metricas: {e}")
threading.Thread(target=_volcar, daemon=True).start()Valores estadísticos: 900 puntos en uno
Si solo necesitas Sum, Average, Min y Max, puedes preagregarlo tú y enviar un único punto con
StatisticValues. Una llamada en lugar de novecientas:
cw.put_metric_data(
Namespace="MercadoFresco/Tienda",
MetricData=[{
"MetricName": "TiempoConfirmacionPedido",
"Dimensions": [{"Name": "Entorno", "Value": "produccion"}],
"StatisticValues": {
"SampleCount": 900, # 900 pedidos en la ultima hora
"Sum": 1_620_000.0, # suma de milisegundos
"Minimum": 410.0,
"Maximum": 9_480.0,
},
"Unit": "Milliseconds",
}],
)El precio de esta optimización: con StatisticValues pierdes los percentiles. CloudWatch solo
recibe cuatro números; no puede calcular un p99 a partir de ellos. Como el p95 de
TiempoConfirmacionPedido es precisamente lo que Marta quiere vigilar, MercadoFresco no usa
StatisticValues para esa métrica. Sí la usa para ImportePedido, donde el Sum y el Maximum son
todo lo que interesa.
Una tercera opción, intermedia, es el campo Values + Counts, que envía valores distintos con su
multiplicidad y sí conserva percentiles:
cw.put_metric_data(
Namespace="MercadoFresco/Tienda",
MetricData=[{
"MetricName": "TiempoConfirmacionPedido",
"Values": [410.0, 520.0, 610.0, 9480.0],
"Counts": [300.0, 450.0, 140.0, 10.0], # 900 muestras en total
"Unit": "Milliseconds",
}],
)El agente unificado de CloudWatch en las instancias del ASG
Para memoria y disco hace falta un agente dentro de la instancia. El agente unificado de CloudWatch
(amazon-cloudwatch-agent) hace dos trabajos a la vez: publica métricas del sistema y envía ficheros
de log a CloudWatch Logs. Sustituye a los antiguos scripts de Perl y al awslogs, que ya no se usan.
- Permisos
El rol rol-mercadofresco-tienda necesita esto añadido a pol-mercadofresco-tienda:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicarMetricasDelAgente",
"Effect": "Allow",
"Action": "cloudwatch:PutMetricData",
"Resource": "*",
"Condition": {
"StringEquals": { "cloudwatch:namespace": "MercadoFresco/Sistema" }
}
},
{
"Sid": "EscribirRegistros",
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogStreams"
],
"Resource": "arn:aws:logs:eu-west-1:111122223333:log-group:/mercadofresco/*"
},
{
"Sid": "LeerLaConfiguracionDelAgente",
"Effect": "Allow",
"Action": "ssm:GetParameter",
"Resource": "arn:aws:ssm:eu-west-1:111122223333:parameter/mercadofresco/produccion/cloudwatch-agent"
}
]
}Fíjate en dos cosas. cloudwatch:PutMetricData no admite ARN de recurso —por eso "Resource": "*"
con la condición de espacio de nombres, exactamente el patrón que estudiamos en 04-01—. Y la
configuración del agente se guarda en Parameter Store (04-03), que es la forma limpia de que todas
las instancias del ASG compartan la misma sin hornearla en la AMI.
- El fichero de configuración
{
"agent": {
"metrics_collection_interval": 60,
"run_as_user": "cwagent",
"region": "eu-west-1"
},
"metrics": {
"namespace": "MercadoFresco/Sistema",
"append_dimensions": {
"AutoScalingGroupName": "${aws:AutoScalingGroupName}",
"InstanceId": "${aws:InstanceId}"
},
"aggregation_dimensions": [
["AutoScalingGroupName"],
[]
],
"metrics_collected": {
"mem": {
"measurement": [
{ "name": "mem_used_percent", "rename": "MemoriaUsadaPorcentaje", "unit": "Percent" },
{ "name": "mem_available", "unit": "Bytes" }
],
"metrics_collection_interval": 60
},
"disk": {
"resources": ["/", "/var/log"],
"measurement": [
{ "name": "used_percent", "rename": "DiscoUsadoPorcentaje", "unit": "Percent" },
{ "name": "inodes_free" }
],
"ignore_file_system_types": ["sysfs", "devtmpfs", "tmpfs", "overlay"],
"metrics_collection_interval": 300
},
"swap": {
"measurement": ["swap_used_percent"]
},
"procstat": [
{
"pattern": "gunicorn",
"measurement": ["cpu_usage", "memory_rss"]
}
]
}
},
"logs": {
"logs_collected": {
"files": {
"collect_list": [
{
"file_path": "/var/log/mercadofresco/aplicacion.log",
"log_group_name": "/mercadofresco/tienda/aplicacion",
"log_stream_name": "{instance_id}",
"retention_in_days": 30,
"timestamp_format": "%Y-%m-%d %H:%M:%S",
"timezone": "UTC",
"multi_line_start_pattern": "{timestamp_format}"
},
{
"file_path": "/var/log/nginx/access.log",
"log_group_name": "/mercadofresco/tienda/nginx-acceso",
"log_stream_name": "{instance_id}",
"retention_in_days": 14
},
{
"file_path": "/var/log/nginx/error.log",
"log_group_name": "/mercadofresco/tienda/nginx-error",
"log_stream_name": "{instance_id}",
"retention_in_days": 30
}
]
}
}
}
}Repaso de las decisiones tomadas ahí, porque cada una tiene su motivo:
namespacepropioMercadoFresco/Sistema, separado deMercadoFresco/Tienda. Métricas de máquina y métricas de negocio no se mezclan: facilita los permisos y los paneles.append_dimensionscon${aws:AutoScalingGroupName}: el agente resuelve esas variables solo, consultando los metadatos de la instancia. No hay que generar un fichero por instancia.aggregation_dimensionscon[](lista vacía) publica también la métrica agregada de todo el grupo. Es la que se usa en el panel: a Marta le interesa «memoria media del ASG», no la de la instanciai-0abc.multi_line_start_pattern: sin esto, una traza de Python de 30 líneas se convierte en 30 eventos de log inconexos y las consultas de Insights no encuentran nada.retention_in_daysen la propia configuración: el agente crea el grupo con retención finita desde el primer día. Es la línea que más dinero ahorra de todo el fichero.procstatvigila el procesogunicornconcreto. Si el proceso muere ysystemdlo reinicia en bucle, la CPU de la instancia parece normal peroprocstatlo delata.
- Guardar la configuración y desplegarla con
user data
user data# Guardar la configuracion en Parameter Store (04-03)
aws ssm put-parameter \
--name /mercadofresco/produccion/cloudwatch-agent \
--type String \
--tier Standard \
--value file://cloudwatch-agent.json \
--overwrite \
--profile mercadofresco-dev --region eu-west-1Y el fragmento que se añade al user data de la plantilla lt-mercadofresco-tienda (02-01):
#!/bin/bash
set -euo pipefail
# Amazon Linux 2023 trae el paquete en sus repositorios.
dnf install -y amazon-cloudwatch-agent
mkdir -p /var/log/mercadofresco
# Arrancar el agente leyendo la configuracion desde Parameter Store.
# El prefijo ssm: le dice al agente que el argumento es un parametro, no un fichero.
/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
-a fetch-config \
-m ec2 \
-c ssm:/mercadofresco/produccion/cloudwatch-agent \
-s
# Comprobacion: si el agente no arranca, que la instancia falle el health check
# y el ASG la reemplace, en lugar de quedarse ciega en silencio.
systemctl is-active amazon-cloudwatch-agent || exit 1Después de actualizar el user data hay que crear una nueva versión de la plantilla de
lanzamiento y hacer un instance refresh del ASG:
aws ec2 create-launch-template-version \
--launch-template-name lt-mercadofresco-tienda \
--source-version '$Latest' \
--launch-template-data file://datos-plantilla.json \
--profile mercadofresco-dev
aws autoscaling start-instance-refresh \
--auto-scaling-group-name asg-mercadofresco-tienda \
--preferences '{"MinHealthyPercentage": 50, "InstanceWarmup": 180}' \
--profile mercadofresco-devCoste del agente: el software es gratuito; se paga por lo que publica. Con 4 métricas del sistema
agregadas a nivel de ASG más las de cada instancia, MercadoFresco publica unas 20 métricas
personalizadas: 6 USD al mes. La verificación importa: si dejas las dimensiones por instancia sin
agregar y el ASG rota instancias todo el día, cada InstanceId nuevo crea métricas nuevas. Son
métricas que dejan de recibir datos —y por tanto de facturarse tras el periodo de facturación—, pero
ensucian los paneles.
Embedded Metric Format desde Lambda
En Lambda, llamar a PutMetricData es especialmente caro: la función está facturada por
milisegundo, así que esperar 40 ms a la API de CloudWatch se paga literalmente. Y si la Lambda se
invoca 50.000 veces al día, son 50.000 llamadas de API.
El Embedded Metric Format (EMF) resuelve esto de forma elegante: escribes un JSON con una
estructura especial en stdout, y CloudWatch Logs extrae las métricas automáticamente al
ingerirlo. Coste en la función: el de un print.
"""EMF en mercadofresco-generar-miniaturas.
Escribir el JSON en stdout basta: CloudWatch Logs detecta el bloque _aws
y publica las metricas en el espacio MercadoFresco/Miniaturas.
"""
import json
import time
import os
def emitir_metricas(milisegundos, bytes_origen, clave_s3, exito):
documento = {
"_aws": {
"Timestamp": int(time.time() * 1000), # en milisegundos
"CloudWatchMetrics": [
{
"Namespace": "MercadoFresco/Miniaturas",
"Dimensions": [["Entorno"], ["Entorno", "Formato"]],
"Metrics": [
{"Name": "TiempoProceso", "Unit": "Milliseconds"},
{"Name": "TamanoOrigen", "Unit": "Bytes"},
{"Name": "MiniaturasGeneradas", "Unit": "Count"},
],
}
],
},
# Dimensiones: deben aparecer tambien como campos de primer nivel.
"Entorno": "produccion",
"Formato": clave_s3.rsplit(".", 1)[-1].lower(),
# Valores de las metricas.
"TiempoProceso": milisegundos,
"TamanoOrigen": bytes_origen,
"MiniaturasGeneradas": 1 if exito else 0,
# Propiedades: NO se convierten en metricas, pero quedan en el log
# y son consultables con Logs Insights. Aqui va lo de cardinalidad alta.
"claveS3": clave_s3,
"funcion": os.environ.get("AWS_LAMBDA_FUNCTION_NAME"),
"peticionId": os.environ.get("_X_AMZN_TRACE_ID", ""),
}
print(json.dumps(documento))La clave conceptual está en las dos últimas secciones del documento:
| Campo | ¿Se convierte en métrica? | ¿Consultable con Insights? | Cardinalidad admitida |
|---|---|---|---|
Listado en Metrics |
Sí | Sí | — |
Listado en Dimensions |
Es dimensión | Sí | Baja |
Cualquier otro (claveS3) |
No | Sí | Cualquiera |
Es decir: EMF te permite tener la clave del objeto de S3 y el ID de la petición junto a la métrica,
sin pagar el precio de la cardinalidad. Cuando el p99 de TiempoProceso se dispare, una consulta de
Insights te dirá exactamente qué ficheros fueron los lentos. Eso con PutMetricData es imposible.
Nota práctica: la librería aws-embedded-metrics de AWS hace todo esto por ti con un decorador. Aquí
se ha escrito el JSON a mano para que se vea la estructura, que es lo que hay que entender.
Coste de EMF: se paga la ingesta del log (unos 0,63 USD/GB en eu-west-1) y las métricas
extraídas (0,30 USD cada una). No se pagan llamadas de API. Para funciones de alta frecuencia sale
claramente a cuenta.
Registros: grupos, flujos y retención
La jerarquía es simple y conviene tenerla clara:
- Grupo de registros (log group): el contenedor. Aquí se configura la retención, el
cifrado con KMS y las suscripciones. Ejemplo:
/mercadofresco/tienda/aplicacion. - Flujo de registros (log stream): una secuencia de eventos de una sola fuente. Una
instancia, una ejecución de Lambda. Ejemplo:
i-0abc123def456. - Evento: una línea con marca de tiempo y mensaje. Máximo 256 KB.
flowchart TD
G["/mercadofresco/tienda/aplicacion<br/>retencion 30 dias, KMS"]
G --> S1["i-0abc123<br/>eventos"]
G --> S2["i-0def456<br/>eventos"]
G --> S3["i-0ghi789<br/>eventos"]
G -.->|filtro de metricas| M["MercadoFresco/Tienda<br/>PedidosFallidos"]
G -.->|suscripcion| F["Lambda / Firehose /<br/>OpenSearch"]
G -.->|exportacion| B["s3://mercadofresco-registros-web"]
La retención infinita, el error más caro de CloudWatch
Por defecto, un grupo de registros se crea con retención Never expire. Nadie lo cambia. Tres
años después, MercadoFresco tendría 400 GB de logs de nginx de 2023 costando 0,03 USD/GB/mes —12 USD
mensuales por datos que nadie va a leer jamás— y creciendo.
Y hay algo peor que el coste: una consulta de Logs Insights escanea lo que le pidas, y a 0,0063 USD por GB escaneado, una búsqueda descuidada sobre tres años de logs cuesta más que la búsqueda misma.
La política de MercadoFresco:
| Grupo de registros | Origen | Retención | Motivo |
|---|---|---|---|
/mercadofresco/tienda/aplicacion |
Agente | 30 días | Diagnóstico operativo |
/mercadofresco/tienda/nginx-acceso |
Agente | 14 días | Volumen alto, poco valor pasada la semana |
/mercadofresco/tienda/nginx-error |
Agente | 30 días | |
/aws/lambda/mercadofresco-generar-miniaturas |
Lambda | 14 días | |
/aws/lambda/mercadofresco-estado-pedido |
Lambda | 30 días | Toca pedidos |
/aws/rds/instance/mercadofresco-pedidos/postgresql |
RDS | 7 días | Consultas lentas |
aws-waf-logs-mercadofresco |
WAF (04-05) | 30 días | Análisis de falsos positivos |
flowlogs-mercadofresco |
VPC (03-01) | 7 días | Enorme; se archiva a S3 |
Y el comando que hay que ejecutar el mismo día que se crea un grupo:
aws logs put-retention-policy \
--log-group-name /mercadofresco/tienda/aplicacion \
--retention-in-days 30 \
--profile mercadofresco-dev --region eu-west-1Una auditoría útil que Marta ejecuta una vez al mes, para encontrar los grupos que se le han colado sin retención:
aws logs describe-log-groups \
--query "logGroups[?retentionInDays==null].[logGroupName,storedBytes]" \
--output table \
--profile mercadofresco-dev --region eu-west-1En 05-04 veremos cómo AWS Config convierte esta auditoría manual en una regla que salta sola.
Clases de log: Standard frente a Infrequent Access
CloudWatch Logs tiene dos clases de almacenamiento:
Standard |
Infrequent Access |
|
|---|---|---|
| Ingesta | ~0,63 USD/GB | ~0,32 USD/GB (la mitad) |
| Logs Insights | Sí | Sí |
| Live Tail, filtros de métricas, alarmas | Sí | No |
| Suscripciones | Sí | Limitadas |
Para flowlogs-mercadofresco, que se consulta solo cuando hay una investigación de red, la clase
Infrequent Access ahorra la mitad. Para /mercadofresco/tienda/aplicacion, del que dependen filtros
de métricas y alarmas, hay que quedarse en Standard.
Los registros que MercadoFresco tiene dispersos
Esta era una de las cinco preguntas abiertas del módulo 4: «los registros de WAF, VPC, ALB y Lambda se acumulan en cinco sitios sin correlacionar». Vamos a ver el mapa real:
| Registro | Dónde está | ¿En CloudWatch Logs? | Cómo se consulta |
|---|---|---|---|
| Aplicación de la tienda | Agente → Logs | Sí | Logs Insights |
nginx acceso/error |
Agente → Logs | Sí | Logs Insights |
| Lambdas | Automático | Sí | Logs Insights |
| WAF (04-05) | aws-waf-logs-mercadofresco |
Sí | Logs Insights |
| VPC Flow Logs (03-01) | flowlogs-mercadofresco |
Sí | Logs Insights |
| PostgreSQL de RDS | Exportado a Logs | Sí (hay que activarlo) | Logs Insights |
| Registros de acceso del ALB | S3 | No | Athena (05-03) |
| Registros de CloudFront | S3 | No (o Logs v2) | Athena |
| Registros de acceso de S3 | S3 | No | Athena |
| Llamadas a la API de AWS | CloudTrail → S3 | Opcional | 05-03 |
La honestidad importa aquí: CloudWatch Logs no lo centraliza todo. Los registros de acceso del ALB y de CloudFront van a S3 por diseño, porque su volumen haría prohibitiva la ingesta en Logs. La centralización real de MercadoFresco es a dos niveles:
- Diagnóstico en caliente (últimos días): CloudWatch Logs Insights.
- Análisis histórico y forense: S3 + Athena, que se ve en 05-03.
Activar los registros de PostgreSQL en la instancia RDS, que sí falta:
aws rds modify-db-instance \
--db-instance-identifier mercadofresco-pedidos \
--cloudwatch-logs-export-configuration '{"EnableLogTypes":["postgresql","upgrade"]}' \
--apply-immediately \
--profile mercadofresco-dev --region eu-west-1
# Y en el grupo de parametros, registrar las consultas de mas de 1 segundo
aws rds modify-db-parameter-group \
--db-parameter-group-name pg-mercadofresco-pedidos \
--parameters "ParameterName=log_min_duration_statement,ParameterValue=1000,ApplyMethod=immediate" \
--profile mercadofresco-dev --region eu-west-1Ese log_min_duration_statement=1000 va a ser decisivo en la sección del pedido de ocho segundos.
Logs Insights: sintaxis y consultas reales
CloudWatch Logs Insights es un lenguaje de consulta sobre los registros. No es SQL, aunque se le
parece: es una tubería de comandos separados por |.
| Comando | Para qué |
|---|---|
fields |
Elegir/crear campos |
filter |
Filtrar eventos |
stats |
Agregar (count, avg, sum, pct, min, max) |
sort |
Ordenar |
limit |
Limitar resultados (por defecto 1.000) |
parse |
Extraer campos de texto no estructurado |
dedup |
Eliminar duplicados |
display |
Elegir qué se muestra |
Los campos @timestamp, @message, @logStream y @log existen siempre. Si tu log es JSON,
Insights lo descompone solo y puedes referirte a nivel, pedido_id, duracion_ms directamente. Esa
es la razón número uno para loguear en JSON desde el primer día.
Consulta 1: errores de la tienda agrupados
fields @timestamp, @message, nivel, pedido_id, duracion_ms | filter nivel = "ERROR" | stats count() as errores by bin(5m), tipo_error | sort errores desc
bin(5m) agrupa por ventanas de cinco minutos: es lo que convierte una lista de errores en un gráfico
de tendencia. Al ejecutarla, Insights ofrece «Visualization» y ese resultado se puede añadir
directamente al panel.
Consulta 2: los registros de WAF (04-05)
fields @timestamp, httpRequest.clientIp as ip, httpRequest.uri as ruta,
terminatingRuleId as regla, action
| filter action = "BLOCK"
| stats count() as bloqueos by regla, ruta
| sort bloqueos desc
| limit 25Esta es la consulta que en 04-05 decidía el paso de Count a Block. Ahora sabes exactamente dónde
se ejecuta.
Consulta 3: la Lambda de estado de pedido
fields @timestamp, @message, @duration, @billedDuration, @maxMemoryUsed
| filter @type = "REPORT"
| stats count() as invocaciones,
avg(@duration) as media_ms,
pct(@duration, 95) as p95_ms,
pct(@duration, 99) as p99_ms,
max(@maxMemoryUsed) / 1000000 as memoria_max_mb
by bin(1h)Los campos @duration, @billedDuration, @maxMemoryUsed y @initDuration los genera Lambda
automáticamente en la línea REPORT de cada invocación. @maxMemoryUsed frente a la memoria
configurada es la forma más directa de ajustar el tamaño de una función: si usa 80 MB de 512
configurados, estás pagando de más.
Consulta 4: flow logs de la VPC (03-01)
fields @timestamp, srcAddr, dstAddr, dstPort, action, bytes | filter action = "REJECT" and dstPort = 5432 | stats count() as intentos, sum(bytes) as total by srcAddr | sort intentos desc
Alguien intentando llegar al puerto 5432 y siendo rechazado por sg-mercadofresco-basedatos. Si esa
IP es interna, es un error de configuración. Si es externa, es un escaneo.
Consulta 5: la más útil de todas, parse sobre logs sin estructura
parse @message /(?<ip>\d+\.\d+\.\d+\.\d+) .* "(?<metodo>\w+) (?<ruta>\S+).*" (?<estado>\d{3}) (?<bytes>\d+) (?<tiempo>[\d.]+)/
| filter estado >= 500
| stats count() as errores by ruta, estado
| sort errores descparse con expresión regular y grupos con nombre convierte el log de acceso de nginx en campos
consultables. Es el rescate para todo el software que no loguea en JSON.
Ejecutar Insights desde la CLI
ID_CONSULTA=$(aws logs start-query \
--log-group-names /mercadofresco/tienda/aplicacion \
--start-time $(date -d '2 hours ago' +%s) \
--end-time $(date +%s) \
--query-string 'fields @timestamp, @message | filter nivel = "ERROR" | limit 50' \
--query 'queryId' --output text \
--profile mercadofresco-dev --region eu-west-1)
sleep 5
aws logs get-query-results --query-id "$ID_CONSULTA" \
--profile mercadofresco-dev --region eu-west-1Aviso de coste: cada ejecución cobra por GB escaneados, no por resultados devueltos. Acotar el rango temporal es lo que decide la factura: la misma consulta sobre 1 hora o sobre 30 días cuesta 720 veces más en el segundo caso. Reduce siempre la ventana antes de refinar la consulta.
Responder a «¿por qué tardó ocho segundos ese pedido?»
Esta es la segunda pregunta que dejó abierta el módulo 4. Un cliente escribe: «el jueves por la tarde tardé como ocho segundos en que se confirmara el pedido». Marta tiene su correo y la hora aproximada.
Requisito previo: un identificador de correlación que atraviese todos los componentes. Sin él, esto
es imposible. MercadoFresco añade en nginx una cabecera X-Peticion-Id que la aplicación propaga a
la Lambda y escribe en cada línea de log.
Paso 1. Confirmar que el problema existe y acotarlo.
fields @timestamp, duracion_ms, ruta, peticion_id, cliente_hash | filter ruta = "/api/pedidos/confirmar" and duracion_ms > 5000 | sort @timestamp desc | limit 50
Aparecen 34 peticiones lentas el jueves entre las 18:40 y las 19:20. No era cosa de un cliente.
Paso 2. Encontrar la petición concreta.
fields @timestamp, peticion_id, duracion_ms, pedido_id | filter cliente_hash = "a3f8c1e9" and duracion_ms > 5000
Sale una: peticion_id = 7f3a9b2c, duracion_ms = 8140.
Paso 3. Seguir ese identificador por todos los grupos a la vez. Insights permite consultar hasta 50 grupos de registros en una sola consulta, y esa es la respuesta real a «los registros están en cinco sitios sin correlacionar»:
aws logs start-query \
--log-group-names \
/mercadofresco/tienda/aplicacion \
/mercadofresco/tienda/nginx-acceso \
/aws/lambda/mercadofresco-estado-pedido \
/aws/rds/instance/mercadofresco-pedidos/postgresql \
--start-time $(date -d '2026-07-30 18:55' +%s) \
--end-time $(date -d '2026-07-30 19:05' +%s) \
--query-string 'fields @timestamp, @log, @message
| filter @message like /7f3a9b2c/
| sort @timestamp asc' \
--profile mercadofresco-dev --region eu-west-1Paso 4. Leer la cronología.
| Hora | Grupo | Mensaje | Δ |
|---|---|---|---|
| 18:58:12,004 | nginx-acceso |
POST /api/pedidos/confirmar |
— |
| 18:58:12,031 | aplicacion |
inicio confirmacion pedido=48213 |
+27 ms |
| 18:58:12,088 | aplicacion |
validacion de stock ok |
+57 ms |
| 18:58:12,140 | aplicacion |
llamada a lambda estado-pedido |
+52 ms |
| 18:58:12,690 | estado-pedido |
REPORT Duration: 480 ms |
+550 ms |
| 18:58:12,700 | aplicacion |
insertando lineas de pedido |
+10 ms |
| 18:58:20,110 | postgresql |
duration: 7402.115 ms statement: SELECT ... |
+7.410 ms |
| 18:58:20,144 | aplicacion |
pedido confirmado en 8140 ms |
+34 ms |
Encontrado. No era la tienda (27 + 57 + 52 + 10 + 34 = 180 ms), ni la Lambda (550 ms): eran
7,4 segundos dentro de PostgreSQL. Los registros de log_min_duration_statement que activamos
antes son los que han cantado.
Paso 5. Ver qué consulta era.
Y aparece una consulta que hace un SELECT de precio por cada línea del pedido, en un bucle, en lugar
de un solo SELECT ... WHERE id IN (...). Es el patrón N+1: un pedido con 38 productos ejecuta 39
consultas.
Qué acabamos de hacer y qué no. Hemos localizado el problema con registros correlacionados, y ha funcionado porque MercadoFresco tenía un identificador de petición propagado a mano. Pero fíjate en el esfuerzo: cuatro consultas, una cronología montada manualmente, y todo apoyado en que alguien se acordó de propagar la cabecera. Existe una herramienta diseñada exactamente para esto, que hace el paso 4 automáticamente y en un gráfico: es X-Ray, y es la lección 05-02. Volveremos a este mismo pedido de 8,14 segundos allí.
Filtros de métricas: de un patrón de log a una alarma
No se puede alarmar sobre un log; sí sobre una métrica. Un filtro de métricas es la conversión.
aws logs put-metric-filter \
--log-group-name /mercadofresco/tienda/aplicacion \
--filter-name filtro-pedidos-fallidos \
--filter-pattern '{ $.nivel = "ERROR" && $.tipo_error = "PAGO_RECHAZADO" }' \
--metric-transformations \
metricName=PedidosFallidos,\
metricNamespace=MercadoFresco/Tienda,\
metricValue=1,\
defaultValue=0 \
--profile mercadofresco-dev --region eu-west-1Tres detalles de campo:
- La sintaxis de
--filter-patternno es la de Insights. Para logs JSON usa{ $.campo = "valor" }con&&,||,=,!=,>,<. Para texto plano, comillas y términos:"ERROR" -"ERROR de prueba". defaultValue=0es imprescindible. Sin él, cuando no hay errores no se publica ningún dato, la alarma se queda enINSUFFICIENT_DATAy la métrica tiene huecos que rompen la detección de anomalías. CondefaultValue=0la serie es continua.- Los filtros solo se aplican a eventos nuevos. No hay efecto retroactivo sobre lo ya ingerido.
También se puede extraer un valor numérico del log en lugar de contar:
aws logs put-metric-filter \
--log-group-name /mercadofresco/tienda/aplicacion \
--filter-name filtro-duracion-checkout \
--filter-pattern '{ $.ruta = "/api/pedidos/confirmar" && $.duracion_ms > 0 }' \
--metric-transformations \
metricName=DuracionCheckout,\
metricNamespace=MercadoFresco/Tienda,\
metricValue='$.duracion_ms',\
unit=Milliseconds \
--profile mercadofresco-dev --region eu-west-1Y probar el patrón antes de crearlo, contra eventos reales, que evita el clásico filtro que nunca coincide:
aws logs test-metric-filter \
--filter-pattern '{ $.nivel = "ERROR" && $.tipo_error = "PAGO_RECHAZADO" }' \
--log-event-messages \
'{"nivel":"ERROR","tipo_error":"PAGO_RECHAZADO","pedido_id":48213}' \
'{"nivel":"INFO","mensaje":"pedido ok"}' \
--profile mercadofresco-dev --region eu-west-1Suscripciones y exportación a S3
Dos formas de sacar registros de CloudWatch, con propósitos distintos:
| Filtro de suscripción | Exportación a S3 | |
|---|---|---|
| Latencia | Tiempo real (segundos) | Por lotes, hasta 12 h |
| Destinos | Lambda, Firehose, OpenSearch, Kinesis | Solo S3 |
| Uso típico | Procesar, reenviar, alertar | Archivar barato, Athena |
| Coste | Del destino | Solo S3 |
| Límite | 2 filtros por grupo de registros | — |
Suscripción en tiempo real hacia una Lambda que reenvía a Slack lo crítico:
aws logs put-subscription-filter \
--log-group-name /mercadofresco/tienda/aplicacion \
--filter-name suscripcion-criticos \
--filter-pattern '{ $.nivel = "CRITICAL" }' \
--destination-arn arn:aws:lambda:eu-west-1:111122223333:function:mercadofresco-reenviar-criticos \
--profile mercadofresco-dev --region eu-west-1Exportación a S3 para archivo barato de los flow logs, que se van a mirar una vez al año:
aws logs create-export-task \
--task-name export-flowlogs-2026-07 \
--log-group-name flowlogs-mercadofresco \
--from $(date -d '2026-07-01' +%s)000 \
--to $(date -d '2026-08-01' +%s)000 \
--destination mercadofresco-registros-web \
--destination-prefix flowlogs/2026/07 \
--profile mercadofresco-dev --region eu-west-1El bucket necesita una política que permita a logs.eu-west-1.amazonaws.com escribir. Y ojo: solo
puede haber una tarea de exportación activa por cuenta a la vez, y no es incremental. Para archivado
continuo, la opción moderna es una suscripción hacia Data Firehose con destino S3.
Alarmas: umbrales, evaluación y datos faltantes
Una alarma tiene tres estados:
| Estado | Significa |
|---|---|
OK |
La condición no se cumple. Todo bien. |
ALARM |
La condición se cumple. |
INSUFFICIENT_DATA |
No hay datos suficientes para decidir. |
Y cuatro parámetros que casi nadie configura bien a la primera:
--period: la ventana de agregación. 60, 300…--evaluation-periods: cuántos periodos se miran.--datapoints-to-alarm: cuántos de esos deben incumplir. Si se omite, son todos.--treat-missing-data: qué hacer con los huecos.
La combinación evaluation-periods + datapoints-to-alarm se llama «M de N» y es la herramienta
contra el ruido. Compara:
| Configuración | Comportamiento | Cuándo |
|---|---|---|
| 1 de 1, periodo 60 | Salta al primer minuto malo | Solo para lo binario y grave |
| 3 de 3, periodo 60 | Tres minutos seguidos malos | Sostenido, tarda 3 min |
| 2 de 3, periodo 60 | 2 minutos malos de los últimos 3 | Equilibrio recomendado |
| 5 de 5, periodo 300 | 25 minutos | Tendencias lentas: disco |
Con «2 de 3», un pico de un solo minuto —un despliegue, un garbage collector, una copia de
seguridad— no despierta a nadie, pero un problema real que oscila sí se detecta. La configuración
«3 de 3» se pierde exactamente esos problemas intermitentes.
Y --treat-missing-data, cuatro opciones:
| Valor | Comportamiento | Cuándo usarlo |
|---|---|---|
missing (defecto) |
El hueco no cuenta | Casi nunca; genera confusión |
notBreaching |
El hueco se trata como OK |
Métricas con tráfico intermitente |
breaching |
El hueco se trata como ALARM |
Cuando la ausencia de datos ES el problema |
ignore |
El estado no cambia | Mantener el último estado conocido |
El caso que lo explica todo: la alarma sobre PedidosConfirmados. Si la tienda se cae del todo, deja
de publicarse la métrica. Con notBreaching, la alarma se queda en OK mientras la tienda está
muerta. Es el fallo silencioso más común de CloudWatch, y por eso esa alarma concreta lleva
--treat-missing-data breaching.
Las alarmas que MercadoFresco crea en esta lección:
# 1. Latencia real vista por el cliente: p95, no media.
aws cloudwatch put-metric-alarm \
--alarm-name mercadofresco-alb-latencia-alta \
--alarm-description "El p95 de respuesta del ALB supera 2 s" \
--namespace AWS/ApplicationELB --metric-name TargetResponseTime \
--dimensions Name=LoadBalancer,Value=app/alb-mercadofresco-tienda/50dc6c495c0c9188 \
--extended-statistic p95 \
--period 60 --evaluation-periods 3 --datapoints-to-alarm 2 \
--threshold 2 --comparison-operator GreaterThanThreshold \
--treat-missing-data notBreaching \
--alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
--ok-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
--profile mercadofresco-dev --region eu-west-1
# 2. Destinos sanos: si baja de 2, hemos perdido la tolerancia a fallos de AZ.
aws cloudwatch put-metric-alarm \
--alarm-name mercadofresco-alb-destinos-sanos \
--namespace AWS/ApplicationELB --metric-name HealthyHostCount \
--dimensions Name=LoadBalancer,Value=app/alb-mercadofresco-tienda/50dc6c495c0c9188 \
Name=TargetGroup,Value=targetgroup/tg-mercadofresco-tienda/73e2d6bc24d8a067 \
--statistic Minimum --period 60 --evaluation-periods 2 --datapoints-to-alarm 2 \
--threshold 2 --comparison-operator LessThanThreshold \
--treat-missing-data breaching \
--alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
--profile mercadofresco-dev --region eu-west-1
# 3. Conexiones de RDS: 160 de las 200 del grupo de parametros.
aws cloudwatch put-metric-alarm \
--alarm-name mercadofresco-rds-conexiones-altas \
--namespace AWS/RDS --metric-name DatabaseConnections \
--dimensions Name=DBInstanceIdentifier,Value=mercadofresco-pedidos \
--statistic Maximum --period 60 --evaluation-periods 3 --datapoints-to-alarm 2 \
--threshold 160 --comparison-operator GreaterThanThreshold \
--alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
--profile mercadofresco-dev --region eu-west-1
# 4. Espacio libre de RDS: en BYTES. 20 GiB = 21474836480.
aws cloudwatch put-metric-alarm \
--alarm-name mercadofresco-rds-disco-bajo \
--namespace AWS/RDS --metric-name FreeStorageSpace \
--dimensions Name=DBInstanceIdentifier,Value=mercadofresco-pedidos \
--statistic Minimum --period 300 --evaluation-periods 2 \
--threshold 21474836480 --comparison-operator LessThanThreshold \
--alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
--profile mercadofresco-dev --region eu-west-1
# 5. Estrangulamiento de Lambda: umbral 0, sin tolerancia.
aws cloudwatch put-metric-alarm \
--alarm-name mercadofresco-lambda-estrangulada \
--namespace AWS/Lambda --metric-name Throttles \
--dimensions Name=FunctionName,Value=mercadofresco-estado-pedido \
--statistic Sum --period 60 --evaluation-periods 1 \
--threshold 0 --comparison-operator GreaterThanThreshold \
--treat-missing-data notBreaching \
--alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
--profile mercadofresco-dev --region eu-west-1
# 6. Memoria de las instancias, gracias al agente instalado antes.
aws cloudwatch put-metric-alarm \
--alarm-name mercadofresco-tienda-memoria-alta \
--namespace MercadoFresco/Sistema --metric-name MemoriaUsadaPorcentaje \
--dimensions Name=AutoScalingGroupName,Value=asg-mercadofresco-tienda \
--statistic Average --period 300 --evaluation-periods 3 --datapoints-to-alarm 2 \
--threshold 85 --comparison-operator GreaterThanThreshold \
--alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
--profile mercadofresco-dev --region eu-west-1
# 7. Pedidos fallidos, sobre la metrica del filtro de registros.
aws cloudwatch put-metric-alarm \
--alarm-name mercadofresco-pedidos-fallidos \
--namespace MercadoFresco/Tienda --metric-name PedidosFallidos \
--statistic Sum --period 300 --evaluation-periods 2 --datapoints-to-alarm 2 \
--threshold 10 --comparison-operator GreaterThanThreshold \
--treat-missing-data notBreaching \
--alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
--profile mercadofresco-dev --region eu-west-1
# 8. La mas importante: la tienda ha dejado de vender.
# treat-missing-data BREACHING porque la ausencia de dato ES el incidente.
aws cloudwatch put-metric-alarm \
--alarm-name mercadofresco-sin-pedidos \
--alarm-description "No hay pedidos confirmados: la tienda puede estar caida" \
--namespace MercadoFresco/Tienda --metric-name PedidosConfirmados \
--dimensions Name=Entorno,Value=produccion Name=Componente,Value=tienda \
--statistic Sum --period 900 --evaluation-periods 1 \
--threshold 1 --comparison-operator LessThanThreshold \
--treat-missing-data breaching \
--alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
--profile mercadofresco-dev --region eu-west-1Nota sobre --ok-actions en la alarma 1: enviar también la recuperación. Sin ella, alguien recibe un
aviso de madrugada y no tiene forma de saber si sigue pasando. Con --ok-actions, el segundo mensaje
dice «ya está bien». Es un detalle pequeño que cambia mucho la experiencia de estar de guardia.
Y una advertencia sobre la alarma 8: PedidosConfirmados < 1 en 15 minutos es correcto para el
horario comercial, pero a las 4 de la mañana un domingo puede ser perfectamente normal que no haya
pedidos. Esa alarma genera falsos positivos nocturnos. La forma correcta de resolverlo no es subir el
periodo: es la detección de anomalías, dos secciones más abajo.
Alarmas compuestas
Una alarma compuesta se evalúa sobre otras alarmas, con AND, OR y NOT. Sirve para dos cosas
distintas:
- Reducir ruido: solo avisar si varias señales coinciden.
- Suprimir alarmas hijas durante un mantenimiento conocido.
El problema real de MercadoFresco: cuando la base de datos se satura un viernes, saltan a la vez
mercadofresco-rds-conexiones-altas, mercadofresco-alb-latencia-alta y mercadofresco-pedidos-fallidos.
Tres SMS a las 19:15 describiendo el mismo incidente.
aws cloudwatch put-composite-alarm \
--alarm-name mercadofresco-tienda-degradada \
--alarm-description "La tienda esta degradada: latencia alta Y errores de pedido" \
--alarm-rule "ALARM(mercadofresco-alb-latencia-alta) AND (ALARM(mercadofresco-pedidos-fallidos) OR ALARM(mercadofresco-rds-conexiones-altas))" \
--alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
--actions-enabled \
--profile mercadofresco-dev --region eu-west-1Y ahora el paso que la mayoría olvida: quitar la acción de SNS de las alarmas hijas. Si no, sigues recibiendo cuatro avisos en lugar de tres.
aws cloudwatch put-metric-alarm \
--alarm-name mercadofresco-alb-latencia-alta \
--namespace AWS/ApplicationELB --metric-name TargetResponseTime \
--dimensions Name=LoadBalancer,Value=app/alb-mercadofresco-tienda/50dc6c495c0c9188 \
--extended-statistic p95 --period 60 --evaluation-periods 3 --datapoints-to-alarm 2 \
--threshold 2 --comparison-operator GreaterThanThreshold \
--treat-missing-data notBreaching \
--alarm-actions "" \
--profile mercadofresco-dev --region eu-west-1La alarma hija sigue existiendo, sigue cambiando de estado y sigue viéndose en el panel: simplemente ya no notifica. El aviso lo da la compuesta, que contiene el diagnóstico.
Supresión durante mantenimiento: el parámetro --actions-suppressor permite indicar otra alarma
—por ejemplo, una que Marta pone en ALARM a mano durante un despliegue— que silencia la compuesta
mientras esté activa.
Coste: 0,50 USD por alarma compuesta y mes. Suele salir a cuenta solo por dejar de despertar a alguien tres veces.
Detección de anomalías
En lugar de un umbral fijo, CloudWatch entrena un modelo con hasta dos semanas de historia y aprende el patrón: los picos de los viernes, los valles de la madrugada, el ciclo semanal. La alarma salta cuando el valor sale de la banda esperada.
Es exactamente lo que resuelve el falso positivo nocturno de mercadofresco-sin-pedidos: a las 4 de la
mañana el modelo espera 3 pedidos, y 0 es anómalo; a las 19:00 espera 900, y 400 también lo es. Un
umbral fijo no puede expresar eso.
# 1. Crear el detector, que empieza a entrenar.
aws cloudwatch put-anomaly-detector \
--namespace MercadoFresco/Tienda \
--metric-name PedidosConfirmados \
--dimensions Name=Entorno,Value=produccion Name=Componente,Value=tienda \
--stat Sum \
--profile mercadofresco-dev --region eu-west-1
# 2. La alarma sobre la banda, con expresion de metrica.
aws cloudwatch put-metric-alarm \
--alarm-name mercadofresco-pedidos-anomalos \
--alarm-description "Los pedidos se salen de la banda esperada para esta hora" \
--comparison-operator LessThanLowerThreshold \
--evaluation-periods 2 --datapoints-to-alarm 2 \
--threshold-metric-id ad1 \
--treat-missing-data breaching \
--metrics '[
{
"Id": "m1",
"MetricStat": {
"Metric": {
"Namespace": "MercadoFresco/Tienda",
"MetricName": "PedidosConfirmados",
"Dimensions": [
{"Name": "Entorno", "Value": "produccion"},
{"Name": "Componente", "Value": "tienda"}
]
},
"Period": 300,
"Stat": "Sum"
},
"ReturnData": true
},
{
"Id": "ad1",
"Expression": "ANOMALY_DETECTION_BAND(m1, 2)",
"Label": "PedidosConfirmados (banda esperada)",
"ReturnData": true
}
]' \
--alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
--profile mercadofresco-dev --region eu-west-1Detalles clave:
LessThanLowerThreshold: solo avisa si hay menos pedidos de lo esperado. Que haya más es una buena noticia, no una alarma. Existen tambiénGreaterThanUpperThresholdyLessThanLowerOrGreaterThanUpperThreshold.- El
2deANOMALY_DETECTION_BAND(m1, 2)es el número de desviaciones. Más alto = banda más ancha = menos avisos. Empieza en 2 y ajusta con datos reales. - Necesita historia. Los primeros días la banda es enorme y la alarma no sirve de nada. Hay que crearla y esperar.
- Se puede excluir un periodo del entrenamiento con
--configurationyExcludedTimeRanges: si hubo un incidente de 6 horas, no quieres que el modelo aprenda que eso es normal.
Coste: 0,30 USD por métrica analizada y mes, más 0,30 USD por alarma de anomalía.
Y una advertencia honesta: la detección de anomalías no es magia. Detecta desviaciones del patrón histórico, no problemas. Una degradación lenta y constante durante tres semanas se convierte en el nuevo «normal» y deja de avisar. Complementa a los umbrales fijos; no los sustituye.
Acciones de alarma
| Acción | ARN / forma | Uso en MercadoFresco |
|---|---|---|
| Notificar por SNS | arn:aws:sns:...:alertas-mercadofresco |
Todas las alarmas |
| Autoescalado | ARN de política de escalado | cpu-objetivo-60 del ASG |
| Acción sobre EC2 | arn:aws:automate:eu-west-1:ec2:reboot |
recover, stop, terminate, reboot |
| Acción de Systems Manager | ARN de OpsItem o incidente | Crear tickets automáticos |
| Ninguna | — | Alarmas hijas de una compuesta |
Las acciones automáticas de EC2 son útiles pero peligrosas. arn:aws:automate:eu-west-1:ec2:recover
sobre StatusCheckFailed_System migra la instancia a otro anfitrión cuando falla el hardware
subyacente, conservando IP y volúmenes: eso es claramente bueno. En cambio
arn:aws:automate:eu-west-1:ec2:reboot sobre memoria alta es una mala idea: reinicias en bucle una
instancia con una fuga de memoria y ocultas el problema en lugar de arreglarlo.
Para las instancias del ASG, además, la acción correcta casi nunca es reiniciar: es dejar que fallen el
health check /salud y que el propio ASG las reemplace, que es lo que montamos en 02-01 y 03-03.
Comprobar que el aviso llega de verdad
Aquí está la primera pregunta abierta del módulo 4, y la más importante de esta lección: nadie ha comprobado que un aviso de SNS llegue a un teléfono a las cuatro de la madrugada.
Una alarma que dispara hacia un tema de SNS sin suscriptores confirmados es peor que no tener alarma: da una falsa sensación de cobertura.
Paso 1. Ver quién está suscrito de verdad.
aws sns list-subscriptions-by-topic \
--topic-arn arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
--query 'Subscriptions[].[Protocol,Endpoint,SubscriptionArn]' \
--output table \
--profile mercadofresco-dev --region eu-west-1Si en la columna SubscriptionArn aparece PendingConfirmation, esa suscripción no recibe
nada. Es el hallazgo más frecuente al hacer esta comprobación por primera vez: alguien creó la
suscripción hace meses, el correo de confirmación se fue a la carpeta de correo no deseado, y nadie lo
supo.
Paso 2. Suscribir los destinos que faltan.
# Correo del equipo (requiere confirmar el enlace del mensaje recibido)
aws sns subscribe \
--topic-arn arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
--protocol email --notification-endpoint [email protected] \
--profile mercadofresco-dev --region eu-west-1
# SMS al telefono de guardia: se confirma solo, no hay enlace que pulsar
aws sns subscribe \
--topic-arn arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
--protocol sms --notification-endpoint +34600111222 \
--profile mercadofresco-dev --region eu-west-1Nota sobre SMS. Desde hace unos años, enviar SMS con SNS en muchas regiones exige salir del «sandbox» de SMS y, para números españoles, registrar remitente y caso de uso. Si tu SMS de prueba no llega, revisa el estado del sandbox antes de dar por rota la alarma. El detalle completo de SNS —temas FIFO, políticas de entrega, filtros de suscripción— es la lección 07-02.
Paso 3. El simulacro. Forzar el estado de la alarma.
Este es el comando que cierra la pregunta del módulo 4:
aws cloudwatch set-alarm-state \
--alarm-name mercadofresco-alb-latencia-alta \
--state-value ALARM \
--state-reason "SIMULACRO $(date -u +%FT%TZ): comprobacion trimestral de avisos" \
--profile mercadofresco-dev --region eu-west-1Esto no toca la métrica: fuerza el estado de la alarma y dispara sus acciones de verdad. El SMS y el correo salen. Es la única forma honesta de saber que el canal funciona.
Después se devuelve a la realidad:
aws cloudwatch set-alarm-state \
--alarm-name mercadofresco-alb-latencia-alta \
--state-value OK \
--state-reason "Fin del simulacro" \
--profile mercadofresco-dev --region eu-west-1En el siguiente periodo de evaluación, CloudWatch recalcula el estado real con los datos de la métrica y lo corrige solo si hiciera falta.
Paso 4. El protocolo que adopta MercadoFresco.
| Cuándo | Qué se comprueba | Quién |
|---|---|---|
| Cada trimestre | Simulacro con set-alarm-state sobre 3 alarmas críticas |
Marta |
| Cada trimestre | Que no haya suscripciones en PendingConfirmation |
Marta |
| Al incorporar a alguien | Se le suscribe y se hace un simulacro con su teléfono | Marta |
| Al salir alguien | Se elimina su suscripción | Marta |
| Tras cada incidente | ¿Saltó la alarma correcta? ¿A tiempo? ¿Había alguien? | Post-mortem |
Y una comprobación más, que suele descubrir el problema real: hacer el simulacro a las 4 de la madrugada de verdad, una vez. Marta lo hizo. Descubrió que el móvil de guardia estaba en «no molestar» y que los SMS de números cortos no rompían el silencio. La solución no fue de AWS: fue configurar una excepción en el teléfono. La cadena de aviso incluye el teléfono, y el teléfono también hay que probarlo.
El panel mercadofresco-produccion
Un panel de CloudWatch es un documento JSON con una cuadrícula de 24 columnas de ancho. Los widgets se
posicionan con x, y, width, height.
El criterio con el que Marta lo ha diseñado —y que es lo que hay que copiar, más que el JSON— es: arriba lo que responde «¿está bien el negocio?», en medio «¿qué componente falla?», abajo el detalle. Cuando suena el teléfono, se mira de arriba abajo y en 30 segundos se sabe dónde ir.
{
"start": "-PT3H",
"periodOverride": "auto",
"widgets": [
{
"type": "text",
"x": 0, "y": 0, "width": 24, "height": 1,
"properties": {
"markdown": "# MercadoFresco - Produccion (eu-west-1) | Guardia: [email protected]"
}
},
{
"type": "metric",
"x": 0, "y": 1, "width": 6, "height": 4,
"properties": {
"title": "Pedidos confirmados (ultima hora)",
"view": "singleValue",
"region": "eu-west-1",
"sparkline": true,
"stat": "Sum",
"period": 3600,
"metrics": [
["MercadoFresco/Tienda", "PedidosConfirmados",
"Entorno", "produccion", "Componente", "tienda"]
]
}
},
{
"type": "metric",
"x": 6, "y": 1, "width": 6, "height": 4,
"properties": {
"title": "Tiempo de confirmacion p95 (ms)",
"view": "singleValue",
"region": "eu-west-1",
"sparkline": true,
"stat": "p95",
"period": 300,
"metrics": [
["MercadoFresco/Tienda", "TiempoConfirmacionPedido",
"Entorno", "produccion", "Componente", "tienda"]
]
}
},
{
"type": "alarm",
"x": 12, "y": 1, "width": 12, "height": 4,
"properties": {
"title": "Estado de las alarmas criticas",
"alarms": [
"arn:aws:cloudwatch:eu-west-1:111122223333:alarm:mercadofresco-tienda-degradada",
"arn:aws:cloudwatch:eu-west-1:111122223333:alarm:mercadofresco-alb-latencia-alta",
"arn:aws:cloudwatch:eu-west-1:111122223333:alarm:mercadofresco-alb-destinos-sanos",
"arn:aws:cloudwatch:eu-west-1:111122223333:alarm:mercadofresco-rds-conexiones-altas",
"arn:aws:cloudwatch:eu-west-1:111122223333:alarm:mercadofresco-pedidos-anomalos"
]
}
},
{
"type": "metric",
"x": 0, "y": 5, "width": 12, "height": 6,
"properties": {
"title": "ALB - latencia y errores",
"view": "timeSeries",
"stacked": false,
"region": "eu-west-1",
"period": 60,
"yAxis": {
"left": { "label": "segundos", "showUnits": false },
"right": { "label": "errores", "showUnits": false }
},
"metrics": [
["AWS/ApplicationELB", "TargetResponseTime",
"LoadBalancer", "app/alb-mercadofresco-tienda/50dc6c495c0c9188",
{ "stat": "p50", "label": "p50" }],
["...", { "stat": "p95", "label": "p95" }],
["...", { "stat": "p99", "label": "p99" }],
["AWS/ApplicationELB", "HTTPCode_Target_5XX_Count",
"LoadBalancer", "app/alb-mercadofresco-tienda/50dc6c495c0c9188",
{ "stat": "Sum", "yAxis": "right", "label": "5XX aplicacion", "color": "#d62728" }],
["AWS/ApplicationELB", "HTTPCode_ELB_5XX_Count",
"LoadBalancer", "app/alb-mercadofresco-tienda/50dc6c495c0c9188",
{ "stat": "Sum", "yAxis": "right", "label": "5XX balanceador", "color": "#ff7f0e" }]
],
"annotations": {
"horizontal": [
{ "label": "Objetivo p95", "value": 2, "color": "#d62728", "fill": "above" }
]
}
}
},
{
"type": "metric",
"x": 12, "y": 5, "width": 12, "height": 6,
"properties": {
"title": "Base de datos - mercadofresco-pedidos",
"view": "timeSeries",
"region": "eu-west-1",
"period": 60,
"metrics": [
["AWS/RDS", "DatabaseConnections",
"DBInstanceIdentifier", "mercadofresco-pedidos",
{ "stat": "Maximum", "label": "Conexiones" }],
["AWS/RDS", "CPUUtilization",
"DBInstanceIdentifier", "mercadofresco-pedidos",
{ "stat": "Average", "label": "CPU %", "yAxis": "right" }],
["AWS/RDS", "ReplicaLag",
"DBInstanceIdentifier", "mercadofresco-pedidos-lectura",
{ "stat": "Maximum", "label": "Retraso replica (s)", "yAxis": "right" }]
],
"annotations": {
"horizontal": [
{ "label": "Limite de conexiones", "value": 200, "color": "#d62728" },
{ "label": "Alarma", "value": 160, "color": "#ff7f0e" }
]
}
}
},
{
"type": "metric",
"x": 0, "y": 11, "width": 8, "height": 6,
"properties": {
"title": "ASG y sistema",
"view": "timeSeries",
"region": "eu-west-1",
"period": 60,
"metrics": [
["AWS/EC2", "CPUUtilization",
"AutoScalingGroupName", "asg-mercadofresco-tienda",
{ "stat": "Average", "label": "CPU media %" }],
["MercadoFresco/Sistema", "MemoriaUsadaPorcentaje",
"AutoScalingGroupName", "asg-mercadofresco-tienda",
{ "stat": "Average", "label": "Memoria %" }],
["AWS/AutoScaling", "GroupInServiceInstances",
"AutoScalingGroupName", "asg-mercadofresco-tienda",
{ "stat": "Maximum", "label": "Instancias", "yAxis": "right" }]
],
"annotations": {
"horizontal": [
{ "label": "Maximo del ASG", "value": 4, "color": "#d62728", "yAxis": "right" }
]
}
}
},
{
"type": "metric",
"x": 8, "y": 11, "width": 8, "height": 6,
"properties": {
"title": "Lambda",
"view": "timeSeries",
"region": "eu-west-1",
"period": 60,
"metrics": [
["AWS/Lambda", "Duration", "FunctionName", "mercadofresco-estado-pedido",
{ "stat": "p99", "label": "estado-pedido p99 (ms)" }],
["AWS/Lambda", "Errors", "FunctionName", "mercadofresco-estado-pedido",
{ "stat": "Sum", "label": "Errores", "yAxis": "right", "color": "#d62728" }],
["AWS/Lambda", "Throttles", "FunctionName", "mercadofresco-estado-pedido",
{ "stat": "Sum", "label": "Estrangulamientos", "yAxis": "right", "color": "#ff7f0e" }],
["AWS/Lambda", "Errors", "FunctionName", "mercadofresco-generar-miniaturas",
{ "stat": "Sum", "label": "Miniaturas: errores", "yAxis": "right" }]
]
}
},
{
"type": "metric",
"x": 16, "y": 11, "width": 8, "height": 6,
"properties": {
"title": "Borde: CloudFront y WAF",
"view": "timeSeries",
"region": "us-east-1",
"period": 300,
"metrics": [
["AWS/CloudFront", "CacheHitRate", "DistributionId", "E2QWERTY123ABC",
"Region", "Global", { "stat": "Average", "label": "Aciertos de cache %" }],
["AWS/WAFV2", "BlockedRequests", "WebACL", "waf-mercadofresco-cdn",
"Rule", "ALL", "Region", "Global",
{ "stat": "Sum", "label": "Bloqueos WAF", "yAxis": "right" }]
]
}
},
{
"type": "log",
"x": 0, "y": 17, "width": 24, "height": 7,
"properties": {
"title": "Ultimos errores de la tienda",
"region": "eu-west-1",
"view": "table",
"query": "SOURCE '/mercadofresco/tienda/aplicacion'\n| fields @timestamp, nivel, tipo_error, ruta, pedido_id, duracion_ms\n| filter nivel = \"ERROR\"\n| sort @timestamp desc\n| limit 20"
}
}
]
}Y la creación:
aws cloudwatch put-dashboard \
--dashboard-name mercadofresco-produccion \
--dashboard-body file://panel-produccion.json \
--profile mercadofresco-dev --region eu-west-1Detalles del JSON que merecen explicación:
"start": "-PT3H"fija la ventana por defecto en las últimas 3 horas. Sintaxis ISO 8601 de duración:-PT1H,-P1D,-P7D.["...", { "stat": "p95" }]repite la métrica anterior cambiando solo la estadística. Ahorra repetir el ARN largo tres veces.- El widget del borde lleva
"region": "us-east-1". Las métricas de CloudFront y de la Web ACL de CloudFront viven ahí, la misma trampa de 03-04 y 04-05. Un panel puede mezclar regiones widget a widget, y eso lo convierte en el único sitio donde MercadoFresco lo ve todo junto. - Las
annotationshorizontales dibujan la línea del umbral. Ver el gráfico acercarse a la línea roja antes de que salte la alarma es la mitad del valor de un panel. - El widget
logejecuta una consulta de Insights cada vez que se carga el panel. Cuidado: eso cuesta dinero cada vez, y si el panel está en una pantalla de la oficina refrescando cada minuto, son 1.440 consultas al día. Marta le pusolimit 20y una ventana corta por esa razón.
Coste de los paneles: los 3 primeros son gratuitos; a partir de ahí, 3 USD por panel y mes.
MercadoFresco tiene dos: mercadofresco-produccion y mercadofresco-negocio (el de Sara, con pedidos
e importes). Coste: 0 USD.
ServiceLens y Synthetics
Dos funciones de CloudWatch que se mencionan aquí y se aprovechan en 05-02:
ServiceLens une métricas, registros y trazas de X-Ray en un mapa de servicios único. Necesita que X-Ray esté activo, así que se ve en la lección siguiente.
CloudWatch Synthetics ejecuta «canarios»: pequeños scripts que se comportan como un cliente, desde fuera, cada N minutos. Es monitorización sintética: no espera a que haya un cliente real para detectar que la tienda está rota. Con 900 pedidos/hora eso no parece importante, pero a las 3 de la madrugada un domingo pueden pasar dos horas sin una sola compra, y ese es exactamente el hueco por el que se cuela una caída.
El canario de MercadoFresco, canario-mercadofresco-compra, se ejecuta cada 5 minutos y hace el
recorrido completo: portada, buscar «tomate», abrir la ficha, añadir al carrito, llegar al formulario
de pago (sin pagar). Si algún paso falla, dispara alertas-mercadofresco.
aws synthetics create-canary \
--name canario-mercadofresco-compra \
--artifact-s3-location s3://mercadofresco-registros-web/canarios/ \
--execution-role-arn arn:aws:iam::111122223333:role/rol-canario-mercadofresco \
--runtime-version syn-nodejs-puppeteer-9.0 \
--schedule Expression="rate(5 minutes)" \
--code S3Bucket=mercadofresco-registros-web,S3Key=canarios/compra.zip,Handler=compra.handler \
--run-config MemoryInMB=1024,TimeoutInSeconds=120,ActiveTracing=true \
--profile mercadofresco-dev --region eu-west-1Dos advertencias prácticas:
- Un canario que compra de verdad genera pedidos de verdad. El de MercadoFresco se detiene antes de confirmar, y el usuario de prueba está marcado para que Sara lo excluya de sus informes.
- Coste: 0,0012 USD por ejecución. Cada 5 minutos son 8.640 ejecuciones al mes: 10,37 USD, más las capturas de pantalla en S3 y las trazas. No es despreciable; es la partida más cara de esta lección después de los registros.
Coste de CloudWatch y cómo se dispara
Precios de referencia en eu-west-1 (aproximados; consulta siempre la calculadora oficial):
| Concepto | Precio | Nota |
|---|---|---|
Métricas de AWS (AWS/*) |
Gratis | Resolución de 5 min |
| Monitorización detallada EC2 | ~2,10 USD/instancia/mes | Baja a 1 min |
| Métrica personalizada | 0,30 USD/mes cada una | Primeras 10.000 |
PutMetricData |
0,01 USD por 1.000 llamadas | Por eso los lotes |
Ingesta de registros Standard |
~0,63 USD/GB | La partida grande |
Ingesta Infrequent Access |
~0,32 USD/GB | Sin filtros ni alarmas |
| Almacenamiento de registros | 0,03 USD/GB/mes | Comprimido |
| Logs Insights | 0,0063 USD/GB escaneado | Por consulta |
| Alarma estándar | 0,10 USD/mes | |
| Alarma de alta resolución | 0,30 USD/mes | |
| Alarma compuesta | 0,50 USD/mes | |
| Detección de anomalías | 0,30 USD/métrica/mes | Más la alarma |
| Panel | 3 gratis, luego 3 USD/mes | |
| Canario de Synthetics | 0,0012 USD/ejecución | |
| Contributor Insights | 0,50 USD/regla + consultas |
El cálculo de MercadoFresco:
| Concepto | Cantidad | Coste mensual |
|---|---|---|
| Métricas personalizadas de negocio | 8 | 2,40 USD |
| Métricas del agente (sistema, agregadas) | 20 | 6,00 USD |
| Métricas de EMF de las miniaturas | 6 | 1,80 USD |
| Monitorización detallada de EC2 | 2-4 instancias | ~6,30 USD |
| Ingesta de registros | ~22 GB/mes | 13,86 USD |
| Almacenamiento de registros | ~12 GB medios | 0,36 USD |
| Logs Insights | ~30 GB escaneados | 0,19 USD |
| Alarmas estándar (13) | 13 | 1,30 USD |
| Alarma compuesta | 1 | 0,50 USD |
| Detección de anomalías | 1 métrica + 1 alarma | 0,60 USD |
| Paneles | 2 (gratis los 3 primeros) | 0,00 USD |
| Canario cada 5 min | 8.640 ejecuciones | 10,37 USD |
| Total | ~43,68 USD/mes |
Las cinco formas de disparar esta factura, por orden de frecuencia real:
- Retención infinita en los grupos de registros. Crece sin parar y nunca lo notas de golpe.
DEBUGactivado en producción. Multiplica la ingesta por diez de un día para otro. La ingesta es la partida cara: 0,63 USD/GB.- Una dimensión de cardinalidad alta.
PedidoIdcomo dimensión: decenas de miles de USD. Es el error más caro que se puede cometer en CloudWatch, y se comete en una línea. - Consultas de Insights sobre 30 días para depurar. Cada refinamiento de la consulta vuelve a escanear todo.
- Un panel con widgets de log en una pantalla de oficina. Ejecuta consultas cada minuto para siempre.
Y la defensa: un presupuesto de AWS Budgets filtrado por el servicio CloudWatch (módulo 11) con aviso al 80 %. En 05-04 veremos además cómo AWS Config detecta automáticamente los grupos de registros sin retención.
Limpieza
# Alarmas
aws cloudwatch delete-alarms --alarm-names \
mercadofresco-alb-latencia-alta mercadofresco-alb-destinos-sanos \
mercadofresco-rds-conexiones-altas mercadofresco-rds-disco-bajo \
mercadofresco-lambda-estrangulada mercadofresco-tienda-memoria-alta \
mercadofresco-pedidos-fallidos mercadofresco-sin-pedidos \
mercadofresco-pedidos-anomalos \
--profile mercadofresco-dev --region eu-west-1
# La compuesta se borra igual, pero DESPUES de las hijas no: antes.
aws cloudwatch delete-alarms --alarm-names mercadofresco-tienda-degradada \
--profile mercadofresco-dev --region eu-west-1
# Detector de anomalias
aws cloudwatch delete-anomaly-detector \
--namespace MercadoFresco/Tienda --metric-name PedidosConfirmados \
--dimensions Name=Entorno,Value=produccion Name=Componente,Value=tienda \
--stat Sum --profile mercadofresco-dev --region eu-west-1
# Panel
aws cloudwatch delete-dashboards --dashboard-names mercadofresco-produccion \
--profile mercadofresco-dev --region eu-west-1
# Canario: primero detener, luego borrar
aws synthetics stop-canary --name canario-mercadofresco-compra \
--profile mercadofresco-dev --region eu-west-1
aws synthetics delete-canary --name canario-mercadofresco-compra \
--profile mercadofresco-dev --region eu-west-1
# Filtros de metricas
aws logs delete-metric-filter \
--log-group-name /mercadofresco/tienda/aplicacion \
--filter-name filtro-pedidos-fallidos \
--profile mercadofresco-dev --region eu-west-1
# Grupos de registros (esto BORRA los datos: irreversible)
aws logs delete-log-group --log-group-name /mercadofresco/tienda/nginx-acceso \
--profile mercadofresco-dev --region eu-west-1Dos avisos:
- Las métricas personalizadas no se pueden borrar. Dejan de facturarse cuando dejan de recibir datos (tras el periodo correspondiente), pero permanecen visibles un tiempo. Es una razón más para no crear dimensiones a la ligera.
- Borrar un grupo de registros elimina sus datos de forma irreversible. Si puede haber una obligación de conservación, expórtalo a S3 primero y consúltalo con quien lleve el cumplimiento.
Errores Comunes y Consejos
1. Alarmar sobre la media de una latencia. Es el error número uno. Average de
TargetResponseTime se queda plano mientras el p99 está en 9 segundos. Usa --extended-statistic p95 o p99 para todo lo que sea tiempo.
2. Dejar la retención de registros en «Never expire». Es el valor por defecto. Es el error más
caro a medio plazo. Pon retención el mismo día que creas el grupo, y audita mensualmente con
describe-log-groups.
3. Usar un identificador como dimensión. PedidoId, UserId, RequestId en una dimensión crean
una métrica nueva cada vez. Miles de USD. Esos datos van en el log o en una anotación de X-Ray.
4. --treat-missing-data mal elegido. Para una métrica cuya ausencia es el problema
—PedidosConfirmados, HealthyHostCount— hay que poner breaching. Con notBreaching la alarma
está tranquila mientras la tienda está muerta.
5. Confundir HTTPCode_ELB_5XX_Count con HTTPCode_Target_5XX_Count. El primero es del
balanceador (no hay destinos sanos, tiempo de espera); el segundo es tu aplicación. Buscar en el sitio
equivocado cuesta horas.
6. Umbrales en unidades equivocadas. FreeStorageSpace está en bytes; Duration de Lambda en
milisegundos; TargetResponseTime en segundos. Un cero de más o de menos y la alarma nunca salta.
7. Crear una alarma sin comprobar la suscripción de SNS. Si el ARN es correcto pero nadie ha
confirmado la suscripción, la alarma dispara al vacío. Comprueba con list-subscriptions-by-topic y
haz un simulacro con set-alarm-state.
8. No configurar --ok-actions. Quien recibe un aviso de madrugada necesita saber cuándo se ha
recuperado. Sin la notificación de vuelta a OK, alguien se levanta para nada o, peor, no se levanta
cuando debe.
9. Buscar en CloudWatch quién borró un recurso. Eso está en CloudTrail (05-03). CloudWatch registra lo que tu aplicación dice de sí misma.
10. Métricas de CloudFront buscadas en eu-west-1. Están en us-east-1, siempre. Igual que los
certificados de ACM (03-04) y la Web ACL de CloudFront (04-05).
11. Consultas de Insights sobre rangos enormes mientras se depura. Empieza por 15 minutos, afina la consulta, y solo entonces amplía la ventana. Se paga por GB escaneado en cada ejecución.
12. Loguear texto plano en lugar de JSON. Con JSON, Insights descompone los campos solos y las
consultas son triviales. Con texto plano hay que escribir expresiones regulares con parse. Cambiar el
formato del log el primer día cuesta una hora; hacerlo tres años después, semanas.
13. Un filtro de métricas sin defaultValue=0. La serie tiene huecos, la alarma se queda en
INSUFFICIENT_DATA y la detección de anomalías no funciona.
14. Olvidar quitar las acciones de las alarmas hijas al crear una compuesta. Se acaba recibiendo un aviso más, no menos.
Consejo final: la métrica más valiosa de un sistema casi nunca es técnica. PedidosConfirmados
detecta más incidentes reales que CPUUtilization, porque puede haber mil formas de que la tienda
falle con la CPU al 30 %, pero solo hay una forma de que los pedidos caigan a cero.
Ejercicios
Ejercicio 1: diseñar la alarma que detecta un fallo parcial
MercadoFresco despliega una versión nueva de la tienda un martes a las 11:00. La versión tiene un error que solo afecta a los pedidos pagados con tarjeta de un banco concreto: aproximadamente el 12 % de las compras. El resto funciona con normalidad.
Datos del incidente: CPUUtilization sin cambios (42 %), HealthyHostCount en 2,
TargetResponseTime p95 estable en 0,6 s, HTTPCode_Target_5XX_Count con 3 errores cada 5 minutos
(antes: 1). PedidosConfirmados pasa de 900/h a 792/h. En los logs aparecen líneas
{"nivel":"ERROR","tipo_error":"PAGO_RECHAZADO","pasarela":"banco-x"}.
Ninguna alarma actual salta. El error se descubre 6 horas después por una queja en redes sociales.
Diseña la detección: qué métrica o métricas usarías, cómo las obtendrías (filtro de métricas, métrica personalizada, expresión), qué tipo de alarma y con qué parámetros exactos de periodo, evaluación y datos faltantes. Escribe los comandos. Justifica por qué tu propuesta habría saltado en menos de 30 minutos sin generar falsos positivos el resto del mes.
Ejercicio 2: la consulta de Logs Insights y el filtro de métricas
Sara pregunta si el buscador de la tienda está funcionando bien. La aplicación escribe una línea JSON por búsqueda:
{"nivel":"INFO","evento":"busqueda","termino":"tomate rama","resultados":24,"duracion_ms":180,"cliente_hash":"a3f8c1e9"}Escribe:
- a) Una consulta de Insights que dé, por hora, el número de búsquedas, la duración media, el p95 y el porcentaje de búsquedas sin resultados.
- b) Una consulta que liste los 20 términos más buscados que devuelven cero resultados (son productos que MercadoFresco debería tener en catálogo: valor directo para Sara).
- c) Un filtro de métricas y una alarma que avisen si el porcentaje de búsquedas sin resultados supera el 20 % durante 15 minutos, que es la señal de que el índice de búsqueda se ha corrompido. Ojo: un filtro de métricas cuenta, no calcula porcentajes. Necesitarás una expresión de métrica en la alarma.
Ejercicio 3: la auditoría de coste
Marta recibe la factura y CloudWatch ha pasado de 44 a 610 USD en un mes. El desglose:
| Partida | Importe |
|---|---|
Ingesta de registros (Standard) |
412 USD |
| Métricas personalizadas | 96 USD |
| Logs Insights | 71 USD |
| Alarmas y paneles | 4 USD |
| Synthetics | 27 USD |
Pistas de la investigación:
- El grupo
/mercadofresco/tienda/aplicacionha pasado de 8 GB a 620 GB en el mes. - El espacio
MercadoFresco/Tiendatiene ahora 320 métricas; el mes pasado tenía 8. - Hay 11.200 ejecuciones de Insights, todas con ventana de 30 días.
- Hay un canario nuevo,
canario-mercadofresco-admin, ejecutándose cada minuto. - Luis desplegó hace tres semanas una función de «pedidos recomendados» que registra la actividad de cada cliente.
Diagnostica cada partida, di exactamente qué hizo Luis mal en cada caso, escribe las acciones correctivas con los comandos, y propón tres controles preventivos para que no vuelva a pasar. Estima la factura resultante.
Soluciones
Solución 1
Por qué no saltó nada. Todas las alarmas actuales miran a la infraestructura, y la infraestructura está perfecta. Un fallo del 12 % en una pasarela concreta es invisible para la CPU, para el número de destinos sanos y para el p95 de latencia (un pago rechazado responde rápido, incluso más rápido que uno correcto). Y la caída de pedidos de 900 a 792 —un 12 %— queda dentro de la variación normal de un martes. Ningún umbral fijo razonable detecta un 12 %.
Esto es lo que se llama un fallo parcial, y es el tipo de incidente que más tarda en detectarse en sistemas reales.
Detección propuesta: tres capas.
Capa 1 — la señal directa. Filtro de métricas sobre el error concreto, desglosado por pasarela.
aws logs put-metric-filter \
--log-group-name /mercadofresco/tienda/aplicacion \
--filter-name filtro-pago-rechazado-banco-x \
--filter-pattern '{ $.tipo_error = "PAGO_RECHAZADO" && $.pasarela = "banco-x" }' \
--metric-transformations \
metricName=PagosRechazadosBancoX,\
metricNamespace=MercadoFresco/Tienda,\
metricValue=1,defaultValue=0 \
--profile mercadofresco-dev --region eu-west-1Capa 2 — la alarma de proporción, que es la buena. Un umbral absoluto de «más de N rechazos» falla en los dos sentidos: de noche 3 rechazos son muchísimo, en el pico del viernes son nada. Lo correcto es alarmar sobre el porcentaje, con una expresión de métrica:
aws cloudwatch put-metric-alarm \
--alarm-name mercadofresco-tasa-rechazo-pagos \
--alarm-description "Mas del 5% de los intentos de pago son rechazados" \
--evaluation-periods 3 --datapoints-to-alarm 2 \
--threshold 5 --comparison-operator GreaterThanThreshold \
--treat-missing-data notBreaching \
--metrics '[
{"Id":"rechazos","MetricStat":{"Metric":{"Namespace":"MercadoFresco/Tienda",
"MetricName":"PagosRechazadosBancoX"},"Period":300,"Stat":"Sum"},
"ReturnData":false},
{"Id":"intentos","MetricStat":{"Metric":{"Namespace":"MercadoFresco/Tienda",
"MetricName":"IntentosPago"},"Period":300,"Stat":"Sum"},
"ReturnData":false},
{"Id":"tasa","Expression":"100 * rechazos / intentos",
"Label":"% de pagos rechazados","ReturnData":true}
]' \
--alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
--profile mercadofresco-dev --region eu-west-1Con 12 % de rechazos frente a un umbral del 5 %, y «2 de 3» periodos de 5 minutos, esta alarma salta
en 10-15 minutos. El requisito es publicar también IntentosPago (un segundo filtro de métricas o
una métrica personalizada); sin denominador no hay proporción.
Capa 3 — la red de seguridad genérica: detección de anomalías sobre PedidosConfirmados. Una
caída del 12 % sostenida durante 6 horas sí sale de la banda de anomalía, porque el modelo conoce
el patrón de un martes por la mañana con precisión. Es la alarma mercadofresco-pedidos-anomalos que
ya creamos: habría saltado, probablemente en la primera hora. Es más lenta que la capa 2, pero es la
única que detecta problemas que nadie ha anticipado.
Por qué no genera falsos positivos. La capa 2 mira una proporción, y una proporción es independiente del volumen: funciona igual a las 4 de la mañana que el viernes a las 19:00. Con «2 de 3» periodos de 5 minutos, un pico aislado de rechazos —un banco que reinicia su pasarela un minuto— no dispara nada.
La lección de fondo: las alarmas de infraestructura detectan caídas totales. Los fallos parciales, que son los más frecuentes y los que más dinero cuestan, solo los detectan las métricas de negocio y las proporciones.
Solución 2
a) Panorama por horas.
fields @timestamp, duracion_ms, resultados, termino
| filter evento = "busqueda"
| stats count() as busquedas,
avg(duracion_ms) as media_ms,
pct(duracion_ms, 95) as p95_ms,
sum(resultados = 0) as sin_resultados,
100.0 * sum(resultados = 0) / count() as pct_sin_resultados
by bin(1h)
| sort @timestamp descLa clave está en sum(resultados = 0): en Insights, una expresión booleana vale 1 o 0, así que
sumarla cuenta los casos verdaderos. Es el idioma para calcular porcentajes en este lenguaje.
b) Términos sin resultados: valor de negocio directo.
fields termino | filter evento = "busqueda" and resultados = 0 | stats count() as veces by termino | sort veces desc | limit 20
Esto no es monitorización técnica: es una lista de productos que los clientes buscan y MercadoFresco no vende. Marta la envía a Sara cada lunes. Es el mejor ejemplo de que los registros de una aplicación valen para mucho más que depurar.
c) Filtro de métricas y alarma de proporción.
Dos filtros, porque hacen falta numerador y denominador:
# Denominador: todas las busquedas
aws logs put-metric-filter \
--log-group-name /mercadofresco/tienda/aplicacion \
--filter-name filtro-busquedas-total \
--filter-pattern '{ $.evento = "busqueda" }' \
--metric-transformations \
metricName=Busquedas,metricNamespace=MercadoFresco/Tienda,\
metricValue=1,defaultValue=0 \
--profile mercadofresco-dev --region eu-west-1
# Numerador: las que no devuelven nada
aws logs put-metric-filter \
--log-group-name /mercadofresco/tienda/aplicacion \
--filter-name filtro-busquedas-vacias \
--filter-pattern '{ $.evento = "busqueda" && $.resultados = 0 }' \
--metric-transformations \
metricName=BusquedasSinResultados,metricNamespace=MercadoFresco/Tienda,\
metricValue=1,defaultValue=0 \
--profile mercadofresco-dev --region eu-west-1Y la alarma con expresión:
aws cloudwatch put-metric-alarm \
--alarm-name mercadofresco-buscador-degradado \
--alarm-description "Mas del 20% de las busquedas no devuelven resultados" \
--evaluation-periods 3 --datapoints-to-alarm 3 \
--threshold 20 --comparison-operator GreaterThanThreshold \
--treat-missing-data notBreaching \
--metrics '[
{"Id":"vacias","MetricStat":{"Metric":{"Namespace":"MercadoFresco/Tienda",
"MetricName":"BusquedasSinResultados"},"Period":300,"Stat":"Sum"},"ReturnData":false},
{"Id":"total","MetricStat":{"Metric":{"Namespace":"MercadoFresco/Tienda",
"MetricName":"Busquedas"},"Period":300,"Stat":"Sum"},"ReturnData":false},
{"Id":"pct","Expression":"100 * vacias / MAX([total, 1])",
"Label":"% busquedas sin resultados","ReturnData":true}
]' \
--alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
--profile mercadofresco-dev --region eu-west-1Tres decisiones justificadas:
MAX([total, 1])en el denominador evita la división por cero cuando no hay búsquedas de madrugada. Sin eso, la expresión produce valores no numéricos y la alarma se comporta de forma errática.- 3 de 3 periodos de 5 minutos = 15 minutos, tal como pedía el enunciado. Aquí sí conviene «3 de 3» y no «2 de 3»: un índice corrompido no se arregla solo, así que la señal será sostenida, y esperar 15 minutos evita el ruido de un despliegue del índice de búsqueda.
notBreachingporque de madrugada puede no haber búsquedas, y eso no es un incidente.
Solución 3
Diagnóstico partida por partida.
1. Ingesta de registros: 412 USD (8 GB → 620 GB). La función de «pedidos recomendados» de Luis
registra la actividad de cada cliente en cada visita. Probablemente escribe una línea por producto
visto, y muy probablemente con nivel: DEBUG porque así lo dejó tras depurar. 612 GB extra a 0,63
USD/GB son 386 USD. Es la causa principal.
Error de Luis: dejar DEBUG en producción y registrar en el log de aplicación un flujo de eventos
de analítica que no es un log de aplicación.
Corrección, en dos tiempos:
# Inmediato: bajar el nivel de log por variable de entorno / parametro
aws ssm put-parameter --name /mercadofresco/produccion/nivel-log \
--value INFO --type String --overwrite \
--profile mercadofresco-dev --region eu-west-1
# Y asegurar la retencion, que muy probablemente sigue en "nunca expira"
aws logs put-retention-policy \
--log-group-name /mercadofresco/tienda/aplicacion \
--retention-in-days 30 \
--profile mercadofresco-dev --region eu-west-1De fondo: los eventos de comportamiento de cliente no van a CloudWatch Logs. Van a
mercadofresco-informes-analitica en S3, vía Firehose, donde el almacenamiento cuesta 0,023 USD/GB en
lugar de 0,63 USD/GB de ingesta. Es 27 veces más barato y es donde Sara los quiere de todos modos.
2. Métricas personalizadas: 96 USD (8 → 320 métricas). 320 × 0,30 = 96 USD. Casi con seguridad,
la función de recomendaciones publica una métrica con una dimensión de cardinalidad media —CategoriaProducto
con 300 valores, o ModeloRecomendador—.
Error de Luis: usar como dimensión algo que no es una categoría pequeña y cerrada.
Corrección: quitar esa dimensión y, si el desglose se necesita, ponerlo como propiedad EMF, que es consultable con Insights y no cuesta 0,30 USD por valor. Nota importante para el examen mental: la factura de métricas no baja hasta el mes siguiente, porque las métricas ya creadas se facturan mientras reciban datos. No se pueden borrar; hay que dejar de publicarlas.
Y la prevención de verdad, en la política IAM del rol —el patrón de 04-01—:
{
"Effect": "Allow",
"Action": "cloudwatch:PutMetricData",
"Resource": "*",
"Condition": {
"StringEquals": { "cloudwatch:namespace": "MercadoFresco/Tienda" }
}
}Esto no limita la cardinalidad —IAM no puede—, pero sí impide que una aplicación cree espacios de nombres nuevos sin control, que es la otra mitad del problema.
3. Logs Insights: 71 USD (11.200 ejecuciones a 30 días). 11.200 consultas escaneando volúmenes enormes. Dos causas superpuestas: alguien depurando con la ventana en «30 días» sin cambiarla, y muy probablemente un widget de log en un panel abierto en una pantalla que reejecuta la consulta cada vez que se refresca.
Corrección: reducir la ventana por defecto de los widgets de log, poner limit bajo, y establecer
la norma de empezar siempre por 15 minutos. Como el grupo baja de 620 GB a ~8 GB al arreglar el punto
1, esta partida cae sola en más de un 95 %.
4. Synthetics: 27 USD. canario-mercadofresco-admin cada minuto son 43.200 ejecuciones al mes.
Para un panel de administración que usan tres personas en horario de oficina, es absurdo.
Corrección:
aws synthetics update-canary \
--name canario-mercadofresco-admin \
--schedule Expression="rate(15 minutes)" \
--profile mercadofresco-dev --region eu-west-1De 43.200 a 2.880 ejecuciones: de 51,84 a 3,46 USD. Y si solo importa en horario de oficina, se puede
programar con una expresión cron en lugar de rate.
5. Alarmas y paneles: 4 USD. Correcto. No se toca.
Factura estimada tras las correcciones:
| Partida | Antes | Después | Cómo |
|---|---|---|---|
| Ingesta de registros | 412 USD | ~14 USD | INFO + analítica a S3 + retención |
| Métricas personalizadas | 96 USD | ~10 USD | Quitar la dimensión (efecto el mes siguiente) |
| Logs Insights | 71 USD | ~1 USD | Menos volumen y ventanas acotadas |
| Alarmas y paneles | 4 USD | 4 USD | — |
| Synthetics | 27 USD | ~14 USD | Canario de admin cada 15 min |
| Total | 610 USD | ~43 USD |
Tres controles preventivos:
- Presupuesto de AWS Budgets filtrado por servicio = CloudWatch, con umbral de 60 USD y aviso al
80 % a
[email protected]. Detecta la desviación en días, no en la factura del mes siguiente. Se ve en el módulo 11. - Regla de AWS Config que marque como no conforme cualquier grupo de registros sin
retentionInDays, con remediación automática que le ponga 30 días. Es exactamente el caso de uso de la lección 05-04. - Revisión en el proceso de despliegue: nadie despliega código que escribe logs nuevos sin que otra persona revise el volumen estimado y el nivel de log. Es una casilla en la revisión de código, no una herramienta. El módulo 8 la integra en el pipeline.
Y un cuarto, cultural, que es el que de verdad funciona: enseñar la factura. Cuando Luis vio que
sus logs de depuración costaban 386 USD al mes —más que las instancias EC2 que los generaban— no
volvió a dejar DEBUG activado.
Conclusión
MercadoFresco ya tiene una mirada. Sabes que CloudWatch son tres cosas —métricas, registros y alarmas— y sabes qué no es: ni el registro de quién llamó a la API (CloudTrail, 05-03), ni el rastreador de peticiones (X-Ray, 05-02), ni el evaluador de cumplimiento (Config, 05-04), ni el bus de eventos (EventBridge, 07-03).
Dominas el modelo de métricas: espacio de nombres, nombre y dimensiones, con la regla de oro de que
cada combinación de dimensiones es una métrica facturable y que un identificador en una dimensión
es el error más caro de este servicio. Sabes elegir la estadística —Sum para contadores,
Average para utilización, p95/p99 siempre para latencia— y por qué la media de 0,17 s ocultaba
que diez clientes por minuto esperaban nueve segundos para pagar. Conoces la retención automática que
agrega los datos a 5 minutos tras 15 días, y la tabla de las métricas que de verdad deciden algo en
cada servicio que has montado, con la diferencia crítica entre HTTPCode_ELB_5XX_Count y
HTTPCode_Target_5XX_Count.
Has publicado las métricas de negocio PedidosConfirmados y TiempoConfirmacionPedido en
MercadoFresco/Tienda, primero con PutMetricData directo y después por lotes desde un hilo aparte
para sacar la llamada de la ruta crítica de una compra. Sabes por qué EC2 no publica memoria —el
hipervisor no ve dentro del sistema operativo invitado— y has desplegado el agente unificado en
lt-mercadofresco-tienda con su configuración en Parameter Store, sus append_dimensions resueltas
solas y su retention_in_days puesta desde el primer día. Y conoces el Embedded Metric Format,
que en Lambda te da métricas gratis en la ruta crítica y —esto es lo importante— te permite guardar la
clave de S3 y el ID de petición junto a la métrica sin pagar cardinalidad.
En registros, has centralizado lo que se podía centralizar y sabes lo que no: los accesos del ALB
y de CloudFront viven en S3 y se consultan con Athena. Tienes la política de retención por grupo, la
clase Infrequent Access para los flow logs, y las consultas de Logs Insights que responden
preguntas reales —los bloqueos de WAF por regla, el p99 de una Lambda con @duration, los rechazos
al puerto 5432, y parse con expresión regular para el software que no loguea en JSON—. Y, sobre
todo, has respondido a la pregunta del pedido de ocho segundos: cuatro grupos de registros consultados
a la vez con un identificador de correlación, una cronología montada evento a evento, y el
culpable identificado —7.402 ms dentro de PostgreSQL, un patrón N+1 con 39 consultas para un pedido
de 38 productos—.
Sabes convertir un patrón de log en métrica con un filtro de métricas (y por qué defaultValue=0
no es opcional), y sacar registros con suscripciones en tiempo real o exportación a S3.
En alarmas, dominas lo que separa una alarma útil de una fuente de ruido: la combinación «M de N»
con --datapoints-to-alarm 2 --evaluation-periods 3, el tratamiento de datos faltantes —con
breaching para las métricas cuya ausencia es el incidente, como PedidosConfirmados—, las
alarmas compuestas que convierten tres SMS en uno y que exigen quitar las acciones de las hijas, y
la detección de anomalías que sabe que 0 pedidos a las 4 de la mañana es normal y a las 19:00 es
una catástrofe. Has montado ocho alarmas nuevas —que se suman a las de los módulos 2 y 4 hasta
trece en total—, la compuesta mercadofresco-tienda-degradada y el detector de anomalías sobre
PedidosConfirmados.
Y has respondido a la primera pregunta que dejó abierta el módulo 4, que era la más incómoda de todas:
el aviso llega de verdad. Has comprobado las suscripciones buscando el temido
PendingConfirmation, has suscrito el correo y el teléfono de guardia, y has hecho el simulacro con
set-alarm-state, que dispara las acciones reales sin tocar la métrica. Y sabes que la cadena incluye
el teléfono: el «no molestar» de un móvil ha silenciado más alarmas que cualquier error de
configuración de AWS.
Todo ello se ve junto en el panel mercadofresco-produccion, ordenado de arriba abajo —negocio,
componentes, detalle—, con las métricas de CloudFront en su widget de us-east-1, las líneas de
anotación que enseñan lo cerca que estás del umbral antes de que salte, y el widget de log con los
últimos errores. Y con el canario canario-mercadofresco-compra recorriendo la tienda cada cinco
minutos para que un domingo de madrugada, sin un solo cliente real, alguien siga comprobando que se
puede comprar. Todo por unos 44 USD al mes, con las cinco formas de disparar esa cifra
identificadas y la retención puesta el mismo día que se crea cada grupo.
Queda, sin embargo, una insatisfacción concreta. La investigación del pedido de ocho segundos
funcionó, pero costó cuatro consultas, una cronología montada a mano en una hoja de cálculo, y dependió
por completo de que alguien se hubiera acordado de propagar la cabecera X-Peticion-Id por todos los
componentes. Si mañana el cuello de botella está dentro de la Lambda, o en la llamada a la pasarela de
pago, o en el tiempo de conexión a la base de datos, habrá que repetir el trabajo entero y adivinar
otra vez dónde mirar.
Existe una herramienta que hace ese trabajo sola, que dibuja la cronología en un gráfico y que dice,
para una petición concreta, cuánto tardó cada tramo del recorrido: AWS X-Ray. En la lección
05-02, «AWS X-Ray y trazabilidad distribuida», veremos qué es una traza, un segmento y un
subsegmento, cómo se propaga el identificador con X-Amzn-Trace-Id, por qué no se trazan todas las
peticiones y cómo se configura el muestreo, cómo instrumentar la tienda con aws_xray_sdk y activar
el rastreo en mercadofresco-estado-pedido con una casilla y un permiso, cómo leer el mapa de
servicios y buscar con filtros como annotation.pedido_id, y volveremos a este mismo pedido de 8,14
segundos para verlo, esta vez, en un solo gráfico.
Curso de AWS
Módulo 1: Introducción a AWS
- ¿Qué es AWS?
- Configuración de tu cuenta de AWS
- Infraestructura global de AWS
- Consola de administración de AWS
- AWS CLI y SDKs
Módulo 2: Servicios principales de AWS
Módulo 3: Redes y entrega de contenido
- Amazon VPC
- Grupos de seguridad y listas de control de acceso
- Elastic Load Balancing
- Amazon CloudFront
- Route 53
Módulo 4: Seguridad e identidad
- AWS Identity and Access Management (IAM)
- AWS Key Management Service (KMS)
- Secrets Manager y Parameter Store
- AWS Shield
- AWS WAF
Módulo 5: Monitorización y gestión
- Amazon CloudWatch
- AWS X-Ray y trazabilidad distribuida
- AWS CloudTrail
- AWS Config
- AWS Trusted Advisor
Módulo 6: Bases de datos
- Cómo elegir la base de datos adecuada
- Amazon DynamoDB
- Amazon Aurora
- Amazon Redshift
- Amazon ElastiCache
Módulo 7: Integración de aplicaciones
- Amazon SQS
- Amazon SNS
- Amazon EventBridge
- AWS Step Functions
- Patrones de integración: idempotencia, reintentos y colas de mensajes fallidos
