Module 4 ended with an uncomfortable sentence: it is all built, and nobody is watching. MercadoFresco has a shop that scales on its own, a replicated database, a CDN that serves 96 % of the traffic, minimal identities, encrypted data and two application firewalls. And yet, if on Friday at 19:10 the shop starts returning errors, the first person to find out will be a customer on Twitter.
Amazon CloudWatch is the service that solves that. It is not "the AWS graphs": it is the central
repository where every service you have built writes down what happens to it, the place where
your application's logs are stored, and the engine that decides when to wake somebody up. Marta has
been using it in passing since 02-01 —every time we have looked at CPUUtilization or created an
alarm we were in CloudWatch— but we have never set it up properly.
This lesson sets it up properly. By the end, MercadoFresco will have a dashboard that Marta opens every morning with her coffee, its own business metrics, all of its logs in one place queryable with something SQL-ish, and —most importantly— the verified certainty that a notice reaches a phone at four in the morning.
Cost warning. CloudWatch is one of the AWS services that most easily runs out of control, and hardly ever because of metrics: because of logs. A log group with infinite retention, or an application with
DEBUGon in production, can cost more than the EC2 instances that generate them. Every section of this lesson states its cost, and there is a whole section devoted to it.
Contents
- What CloudWatch is and what it is not
- The three pillars
- Metrics: namespaces and dimensions
- Standard resolution versus high resolution
- Statistics: why the average lies
- Periods, aggregation and retention
- The metrics that really matter from what is already built
- Why EC2 does not publish memory usage
- Publishing custom metrics with boto3
- Batched
PutMetricDataand statistic values - The CloudWatch unified agent on the ASG instances
- Embedded Metric Format from Lambda
- Logs: groups, streams and retention
- The logs MercadoFresco has scattered around
- Logs Insights: syntax and real queries
- Answering "why did that order take eight seconds?"
- Metric filters: from a log pattern to an alarm
- Subscriptions and export to S3
- Alarms: thresholds, evaluation and missing data
- Composite alarms
- Anomaly detection
- Alarm actions
- Checking that the notice really arrives
- The
mercadofresco-producciondashboard - ServiceLens and Synthetics
- CloudWatch cost and how it explodes
- Cleanup
What CloudWatch is and what it is not
CloudWatch is a regional observability service that does three things: store numeric time series (metrics), store timestamped text (logs), and evaluate conditions over those to trigger actions (alarms).
What it is not, and it is worth settling from the start because it saves a lot of confusion:
| CloudWatch is not | That is done by | Covered in |
|---|---|---|
| A record of who called the AWS API | CloudTrail | 05-03 |
| A tracer of requests between services | X-Ray | 05-02 |
| An evaluator of configuration compliance | AWS Config | 05-04 |
| An event bus for automating reactions | EventBridge | 07-03 |
| An analytical store for large historical queries | Athena / Redshift | 05-03, 06-04 |
The most common confusion is the first one. If Luis is looking for "who deleted the bucket", that is not in CloudWatch: it is in CloudTrail. CloudWatch stores what your application and the services say about themselves, not who talked to the AWS API.
Historically the event bus was called "CloudWatch Events" and you will still see that name in old documentation and in some CLI responses. Today it is EventBridge and it is covered in 07-03; this lesson does not touch it.
The three pillars
flowchart LR
subgraph Sources["Sources"]
EC2["EC2 / ASG"]
ALB["ALB"]
RDS["RDS"]
LAM["Lambda"]
CF["CloudFront"]
APP["Shop code"]
end
EC2 --> M["METRICS<br/>time series"]
ALB --> M
RDS --> M
LAM --> M
CF --> M
APP --> M
EC2 --> L["LOGS<br/>timestamped text"]
LAM --> L
APP --> L
ALB -.->|"to S3, not to Logs"| S3["S3"]
L -->|"metric filter"| M
M --> A["ALARMS<br/>threshold + evaluation"]
A --> SNS["SNS alertas-mercadofresco"]
A --> ASG["Auto Scaling policy"]
A --> EC2ACT["Action on the instance"]
Notice the arrow that goes from logs to metrics: a metric filter turns a text pattern into a
numeric series. It is the bridge that lets you alarm on something that only appears in a log,
such as ERROR pago rechazado. And notice too that the ALB writes its access logs to S3,
not to CloudWatch Logs: that will have consequences in the Logs Insights section.
Metrics: namespaces and dimensions
A metric is a time series identified by three things:
- Namespace: the container. The AWS ones start with
AWS/(AWS/EC2,AWS/ApplicationELB,AWS/RDS,AWS/Lambda,AWS/S3,AWS/CloudFront). Yours cannot start withAWS/: MercadoFresco usesMercadoFresco/Tienda. - Metric name:
CPUUtilization,TargetResponseTime,PedidosConfirmados. - Dimensions: up to 30 key/value pairs identifying what the measurement is of.
InstanceId=i-0abc...,LoadBalancer=app/alb-mercadofresco-tienda/...,DBInstanceIdentifier=mercadofresco-pedidos.
The detail that trips most people up at first: every distinct combination of dimensions is a different metric and is billed separately. These are three metrics, not one:
MercadoFresco/Tienda PedidosConfirmados (no dimensions) MercadoFresco/Tienda PedidosConfirmados Entorno=produccion MercadoFresco/Tienda PedidosConfirmados Entorno=produccion, Provincia=Madrid
And CloudWatch does not aggregate automatically across them. If you publish only the third and then query the first, there will be no data. This has two practical consequences:
- If you want the total and the breakdown too, publish both.
- Never use something of high cardinality as a dimension. Putting
PedidoIdas a dimension creates a new metric per order: 900 metrics an hour, 650,000 a month, at 0.30 USD each. That is more than 190,000 USD a month for a one-line slip. The identity of an order belongs in a log or in an X-Ray annotation (05-02), never in a dimension.
MercadoFresco rule: dimensions are small, closed categories —environment, component, payment method, province— never identifiers.
Standard resolution versus high resolution
| Standard | High resolution | |
|---|---|---|
| Minimum granularity | 60 seconds | 1 second |
| How it is requested | By default | StorageResolution=1 when publishing |
| Alarm periods | 60 s and multiples | 10 s and 30 s as well |
| Metric cost | 0.30 USD/month | 0.30 USD/month (the same) |
| Alarm cost | 0.10 USD/month | 0.30 USD/month |
| Retention of 1 s data | — | 3 hours, then aggregated |
High resolution is for processes that change within seconds: a queue filling up, a 20-second latency spike that at minute resolution is diluted into the average. MercadoFresco does not use it: its problems are perfectly visible at one minute, and 10-second alarms generate night-time noise. It is a tool for one-off diagnosis, not a default.
EC2's basic metrics come at 5 minutes unless you enable detailed monitoring (roughly 2.10 USD
per instance a month in eu-west-1), which brings them down to 1 minute. For MercadoFresco's ASG it
is definitely worth it: with 5-minute data, scaling reacts too late to the peak on
Friday evenings.
Statistics: why the average lies
CloudWatch does not store every individual point: it stores aggregates per period. When you query, you choose which aggregate you want:
| Statistic | What it answers | When to use it |
|---|---|---|
Sum |
How much in total? | Counters: RequestCount, PedidosConfirmados, Errors |
Average |
How much on average? | Utilisation: CPUUtilization, CacheHitRate |
Minimum / Maximum |
The extreme | Capacity: minimum FreeStorageSpace, maximum connections |
SampleCount |
How many points? | Diagnosing gaps |
p50, p90, p95, p99 |
What does percentile N experience? | Latency, always |
TM(5%:95%), TC, WM |
Trimmed mean, ignoring extremes | Fine-grained analysis |
The classic mistake —and the one that hides the most incidents— is alarming on the Average of a
latency. One real minute of the MercadoFresco shop:
| Requests | Response time |
|---|---|
| 950 | 0.08 s (cached pages) |
| 40 | 0.4 s (product pages) |
| 10 | 9.5 s (order confirmation) |
Average = 0.17 s. Perfect. Green. Not a single alarm fires.
p99 = 9.4 s. Ten customers a minute are watching a spinner turn for almost ten seconds
while they try to pay. They are exactly the customers who matter: the ones who are buying.
Rule: for any latency metric, alarm on
p95orp99, never onAverage. The average tells you how the server is doing; the percentile tells you how the customer lives it.
One important nuance: percentiles are only available if the metric is published with enough data.
ALB metrics support them natively. For your own metrics, you have to publish individual values or use
StatisticValues carefully (we will see this: StatisticValues do not allow percentiles,
because they arrive already aggregated).
Periods, aggregation and retention
The period is the aggregation window of the query or the alarm: 60 s, 300 s, 3600 s… It is independent of how often you publish. If you publish every 10 seconds and query with a period of 300, CloudWatch aggregates 30 points into one.
Retention is automatic, free and not configurable:
| Age of the data point | Resolution kept |
|---|---|
| 0 – 3 hours | 1 second (high resolution only) |
| 0 – 15 days | 1 minute |
| 15 – 63 days | 5 minutes |
| 63 – 455 days | 1 hour |
| More than 15 months | Deleted |
Two operational consequences:
- You cannot query with a period of 60 a day from two months ago. The data is already aggregated to 5 minutes. If you need to compare last year's Black Friday minute by minute, you must have exported it beforehand.
- Every month Marta exports the business metrics (
PedidosConfirmados) tomercadofresco-informes-analiticawithget-metric-data, precisely so that Sara can compare campaigns from different years.
The metrics that really matter from what is already built
This is the table Marta has printed out. Of the hundreds of metrics that MercadoFresco's services publish, these are the ones that really decide something:
| Service | Metric | Statistic | What it means | MercadoFresco threshold |
|---|---|---|---|---|
| EC2 / ASG | CPUUtilization |
Average |
CPU usage | Scaling at 60 % (cpu-objetivo-60 policy) |
| EC2 / ASG | StatusCheckFailed_Instance |
Maximum |
The instance is broken | ≥ 1 for 2 periods |
| EC2 / ASG | GroupInServiceInstances |
Maximum |
Active instances | = 4 → mercadofresco-asg-al-maximo |
| ALB | TargetResponseTime |
p95 |
Latency as seen by the customer | > 2 s for 3 min |
| ALB | HTTPCode_ELB_5XX_Count |
Sum |
Errors from the load balancer | > 10 in 5 min |
| ALB | HTTPCode_Target_5XX_Count |
Sum |
Errors from your application | > 25 in 5 min |
| ALB | HealthyHostCount |
Minimum |
Healthy instances per target group | < 2 |
| ALB | UnHealthyHostCount |
Maximum |
Instances failing /salud |
≥ 1 |
| ALB | RejectedConnectionCount |
Sum |
The ALB rejects for lack of capacity | > 0 |
| RDS | DatabaseConnections |
Maximum |
Open connections | > 160 of 200 |
| RDS | FreeStorageSpace |
Minimum |
Free disk in bytes | < 20 GB |
| RDS | ReadLatency / WriteLatency |
Average |
Disk latency in seconds | > 0.02 s |
| RDS | CPUUtilization |
Average |
Instance CPU | > 80 % |
| RDS | ReplicaLag |
Maximum |
Lag of the read replica | > 30 s |
| RDS | FreeableMemory |
Minimum |
Available memory | < 500 MB |
| Lambda | Errors |
Sum |
Failed invocations | > 5 in 5 min |
| Lambda | Duration |
p99 |
Execution time | > 80 % of the timeout |
| Lambda | Throttles |
Sum |
Invocations rejected by concurrency | > 0 |
| Lambda | ConcurrentExecutions |
Maximum |
Simultaneous executions | > 70 % of the quota |
| Lambda | IteratorAge |
Maximum |
Lag on stream sources | > 60 s |
| S3 | BucketSizeBytes |
Average |
Size (daily) | Trend, not an alarm |
| S3 | NumberOfObjects |
Average |
Objects (daily) | Trend |
| S3 | 4xxErrors / 5xxErrors |
Sum |
Requires request metrics (paid) | > 1 % |
| CloudFront | CacheHitRate |
Average |
% served from the edge | < 50 % → alarm |
| CloudFront | 5xxErrorRate |
Average |
Errors returned | > 1 % |
| CloudFront | OriginLatency |
p95 |
How long your origin takes | > 1 s |
| WAF | BlockedRequests |
Sum |
Blocked requests | Anomaly |
Three observations that separate whoever understands this from whoever copies thresholds:
HTTPCode_ELB_5XX_CountandHTTPCode_Target_5XX_Countare not the same thing. The first means the load balancer could not deliver the request to anybody (no healthy targets, timeout expired). The second means your application answered 500. Confusing them wastes hours searching in the wrong place.- RDS's
FreeStorageSpaceis in bytes. The 20 GB threshold is written21474836480. One zero too many and the alarm never fires. - Lambda
Throttleswith a threshold of > 0, not > 10. A single throttle means you have hit the concurrency ceiling and there are customers seeing errors. There is no "acceptable amount of throttling".
Why EC2 does not publish memory usage
It is the question everybody asks on getting here, and the answer explains how the cloud works.
The AWS hypervisor sees the virtual machine from the outside. It can measure CPU consumed, network
traffic, disk operations on the EBS volume: all of that goes through its layer. But free memory is
a concept of the guest operating system. Only Linux knows how much RAM is in buff/cache and could
be freed, and the hypervisor cannot know that without going inside.
The same happens with free disk space: EBS is a block device, and only the file system inside it knows what is occupied.
Practical consequence: for EC2 memory and disk you need to install an agent inside the instance. We do that further down. And a design consequence: that is why Lambda, Fargate and RDS —where AWS manages the operating system— do publish memory without you doing anything.
Publishing custom metrics with boto3
Infrastructure metrics tell you how the machine is doing. Business metrics tell you how the company
is doing, and they are the ones that really detect incidents. In 04-04 we saw why: if 5,000 requests a
second arrive and PedidosPorHora does not move, they are not customers.
MercadoFresco already published PedidosPorHora. Now we add the two that are missing:
| Metric | Unit | Useful statistic | What for |
|---|---|---|---|
PedidosConfirmados |
Count |
Sum |
Real-time business volume |
TiempoConfirmacionPedido |
Milliseconds |
p95, p99 |
Buying experience |
The code, in the shop:
"""Publishing MercadoFresco business metrics.
Runs inside the ASG instances, which assume the role
rol-mercadofresco-tienda. That role has cloudwatch:PutMetricData
restricted to the MercadoFresco/Tienda namespace (04-01).
"""
import time
import boto3
from botocore.config import Config
# Adaptive retry mode: PutMetricData is idempotent for
# our case and we do not want a network glitch to sink a sale.
cw = boto3.client(
"cloudwatch",
region_name="eu-west-1",
config=Config(retries={"max_attempts": 3, "mode": "adaptive"}),
)
NAMESPACE = "MercadoFresco/Tienda"
def record_confirmed_order(amount_eur, milliseconds, payment_method, province):
"""Publish the metrics of a just-confirmed order."""
base_dimensions = [
{"Name": "Entorno", "Value": "produccion"},
{"Name": "Componente", "Value": "tienda"},
]
cw.put_metric_data(
Namespace=NAMESPACE,
MetricData=[
# 1. Global counter, with no dimensions beyond the base ones.
{
"MetricName": "PedidosConfirmados",
"Dimensions": base_dimensions,
"Value": 1,
"Unit": "Count",
"Timestamp": time.time(),
},
# 2. Breakdown by payment method: low, closed cardinality.
{
"MetricName": "PedidosConfirmados",
"Dimensions": base_dimensions + [
{"Name": "MetodoPago", "Value": payment_method},
],
"Value": 1,
"Unit": "Count",
},
# 3. Business latency: the time the customer has waited.
{
"MetricName": "TiempoConfirmacionPedido",
"Dimensions": base_dimensions,
"Value": milliseconds,
"Unit": "Milliseconds",
},
# 4. Amount, to detect anomalous orders.
{
"MetricName": "ImportePedido",
"Dimensions": base_dimensions,
"Value": amount_eur,
"Unit": "None",
},
],
)Details that matter and are not obvious:
Timestampis optional; if you leave it out, CloudWatch uses the time of receipt. You can publish data up to 2 weeks old and up to 2 hours into the future. This lets you recover metrics from a batch process that failed.- The unit matters little to CloudWatch but a great deal to alarms. If you publish in
Millisecondsand the alarm expectsSeconds, the alarm finds no data and stays inINSUFFICIENT_DATAfor ever. Be consistent. - Publishing
Value: 1repeatedly is correct. CloudWatch adds up the values in the period when you query withSum. The application does not need to keep a counter. provinceis not used as a dimension in this example. Spain has 52 provinces: it would be acceptable, but it would multiply the number of metrics by 52 (15.60 USD/month for that alone). Marta decided that this breakdown lives in Sara's reports onmercadofresco-informes-analitica, not in CloudWatch.
The performance mistake almost nobody sees coming
The code above makes one HTTPS call to the CloudWatch API inside the critical path of a purchase. With 900 orders/hour it works; with a serious peak, that 40 ms call adds to the response time the customer perceives, and if the CloudWatch API slows down, your shop slows down.
Three solutions, from worst to best:
- Wrap it in
try/exceptand never fail. The bare minimum: a metric must not bring down a sale. - Accumulate in memory and publish in batches every 20 seconds from a separate thread (next section).
- Write the metric to the log in EMF format and let CloudWatch extract it (the Lambda section). Zero cost in the critical path.
Batched PutMetricData and statistic values
PutMetricData accepts up to 1,000 points per call (with a 1 MB body limit). Publishing in
batches reduces API cost (0.01 USD per 1,000 calls) and takes latency out of the critical
path.
"""Batch publisher: accumulates in memory and flushes every 20 seconds."""
import threading
import queue
import boto3
cw = boto3.client("cloudwatch", region_name="eu-west-1")
_q = queue.Queue(maxsize=10000)
def enqueue(name, value, unit="Count", dimensions=None):
"""Never blocks: if the queue is full, the point is dropped."""
try:
_q.put_nowait({
"MetricName": name,
"Value": value,
"Unit": unit,
"Dimensions": dimensions or [
{"Name": "Entorno", "Value": "produccion"},
{"Name": "Componente", "Value": "tienda"},
],
})
except queue.Full:
pass # Losing a metric is better than blocking a sale.
def _flush():
while True:
batch = []
# Blocking wait for the first item; the rest without waiting.
batch.append(_q.get())
while len(batch) < 1000:
try:
batch.append(_q.get_nowait())
except queue.Empty:
break
try:
for i in range(0, len(batch), 1000):
cw.put_metric_data(
Namespace="MercadoFresco/Tienda",
MetricData=batch[i:i + 1000],
)
except Exception as e: # noqa: BLE001
print(f"Could not publish metrics: {e}")
threading.Thread(target=_flush, daemon=True).start()Statistic values: 900 points in one
If you only need Sum, Average, Min and Max, you can pre-aggregate yourself and send a single
point with StatisticValues. One call instead of nine hundred:
cw.put_metric_data(
Namespace="MercadoFresco/Tienda",
MetricData=[{
"MetricName": "TiempoConfirmacionPedido",
"Dimensions": [{"Name": "Entorno", "Value": "produccion"}],
"StatisticValues": {
"SampleCount": 900, # 900 orders in the last hour
"Sum": 1_620_000.0, # sum of milliseconds
"Minimum": 410.0,
"Maximum": 9_480.0,
},
"Unit": "Milliseconds",
}],
)The price of this optimisation: with StatisticValues you lose percentiles. CloudWatch only
receives four numbers; it cannot compute a p99 from them. Since the p95 of
TiempoConfirmacionPedido is precisely what Marta wants to watch, MercadoFresco does not use
StatisticValues for that metric. It does use it for ImportePedido, where Sum and Maximum are
all that matters.
A third, intermediate option is the Values + Counts pair, which sends distinct values with their
multiplicity and does keep 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 samples in total
"Unit": "Milliseconds",
}],
)The CloudWatch unified agent on the ASG instances
For memory and disk you need an agent inside the instance. The CloudWatch unified agent
(amazon-cloudwatch-agent) does two jobs at once: it publishes system metrics and it ships log files
to CloudWatch Logs. It replaces the old Perl scripts and awslogs, which are no longer used.
- Permissions
The rol-mercadofresco-tienda role needs this added to 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"
}
]
}Notice two things. cloudwatch:PutMetricData does not accept a resource ARN —hence "Resource": "*"
with the namespace condition, exactly the pattern we studied in 04-01—. And the agent's
configuration is stored in Parameter Store (04-03), which is the clean way for all the
ASG instances to share the same one without baking it into the AMI.
- The configuration file
{
"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
}
]
}
}
}
}A review of the decisions taken there, because each one has its reason:
- Its own
namespace,MercadoFresco/Sistema, separate fromMercadoFresco/Tienda. Machine metrics and business metrics do not mix: it makes permissions and dashboards easier. append_dimensionswith${aws:AutoScalingGroupName}: the agent resolves those variables by itself, querying the instance metadata. There is no need to generate one file per instance.aggregation_dimensionswith[](an empty list) also publishes the metric aggregated over the whole group. That is the one used on the dashboard: Marta cares about "average ASG memory", not that of instancei-0abc.multi_line_start_pattern: without this, a 30-line Python traceback becomes 30 disconnected log events and Insights queries find nothing.retention_in_daysin the configuration itself: the agent creates the group with finite retention from day one. It is the line in the whole file that saves the most money.procstatwatches the specificgunicornprocess. If the process dies andsystemdrestarts it in a loop, the instance CPU looks normal butprocstatgives it away.
- Storing the configuration and deploying it with
user data
user data# Store the configuration in 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-1And the fragment added to the user data of the lt-mercadofresco-tienda template (02-01):
#!/bin/bash
set -euo pipefail
# Amazon Linux 2023 ships the package in its repositories.
dnf install -y amazon-cloudwatch-agent
mkdir -p /var/log/mercadofresco
# Start the agent reading the configuration from Parameter Store.
# The ssm: prefix tells the agent the argument is a parameter, not a file.
/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
-a fetch-config \
-m ec2 \
-c ssm:/mercadofresco/produccion/cloudwatch-agent \
-s
# Check: if the agent does not start, let the instance fail the health check
# and let the ASG replace it, instead of going blind in silence.
systemctl is-active amazon-cloudwatch-agent || exit 1After updating the user data you have to create a new version of the launch
template and run an instance refresh on the 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-devCost of the agent: the software is free; you pay for what it publishes. With 4 system metrics
aggregated at ASG level plus those of each instance, MercadoFresco publishes about 20 custom
metrics: 6 USD a month. The caveat matters: if you leave the per-instance dimensions
unaggregated and the ASG rotates instances all day, every new InstanceId creates new metrics. They
are metrics that stop receiving data —and therefore stop being billed after the billing period—, but
they clutter up the dashboards.
Embedded Metric Format from Lambda
In Lambda, calling PutMetricData is especially expensive: the function is billed per
millisecond, so waiting 40 ms for the CloudWatch API is literally paid for. And if the Lambda is
invoked 50,000 times a day, that is 50,000 API calls.
The Embedded Metric Format (EMF) solves this elegantly: you write a JSON with a
special structure to stdout, and CloudWatch Logs extracts the metrics automatically when it
ingests it. Cost inside the function: that of a print.
"""EMF in mercadofresco-generar-miniaturas.
Writing the JSON to stdout is enough: CloudWatch Logs detects the _aws block
and publishes the metrics in the MercadoFresco/Miniaturas namespace.
"""
import json
import time
import os
def emit_metrics(milliseconds, source_bytes, s3_key, success):
document = {
"_aws": {
"Timestamp": int(time.time() * 1000), # in milliseconds
"CloudWatchMetrics": [
{
"Namespace": "MercadoFresco/Miniaturas",
"Dimensions": [["Entorno"], ["Entorno", "Formato"]],
"Metrics": [
{"Name": "TiempoProceso", "Unit": "Milliseconds"},
{"Name": "TamanoOrigen", "Unit": "Bytes"},
{"Name": "MiniaturasGeneradas", "Unit": "Count"},
],
}
],
},
# Dimensions: they must also appear as top-level fields.
"Entorno": "produccion",
"Formato": s3_key.rsplit(".", 1)[-1].lower(),
# Metric values.
"TiempoProceso": milliseconds,
"TamanoOrigen": source_bytes,
"MiniaturasGeneradas": 1 if success else 0,
# Properties: they do NOT become metrics, but they stay in the log
# and are queryable with Logs Insights. High cardinality goes here.
"claveS3": s3_key,
"funcion": os.environ.get("AWS_LAMBDA_FUNCTION_NAME"),
"peticionId": os.environ.get("_X_AMZN_TRACE_ID", ""),
}
print(json.dumps(document))The conceptual key is in the last two sections of the document:
| Field | Does it become a metric? | Queryable with Insights? | Cardinality allowed |
|---|---|---|---|
Listed in Metrics |
Yes | Yes | — |
Listed in Dimensions |
It is a dimension | Yes | Low |
Any other (claveS3) |
No | Yes | Any |
In other words: EMF lets you keep the S3 object key and the request ID next to the metric,
without paying the price of cardinality. When the p99 of TiempoProceso shoots up, an Insights
query will tell you exactly which files were the slow ones. That is impossible with PutMetricData.
A practical note: the AWS aws-embedded-metrics library does all of this for you with a decorator. Here
the JSON is written by hand so that the structure is visible, which is what you need to understand.
Cost of EMF: you pay for the log ingestion (about 0.63 USD/GB in eu-west-1) and the metrics
extracted (0.30 USD each). No API calls are paid for. For high-frequency functions it is clearly
worth it.
Logs: groups, streams and retention
The hierarchy is simple and worth being clear about:
- Log group: the container. This is where retention, KMS encryption and
subscriptions are configured. Example:
/mercadofresco/tienda/aplicacion. - Log stream: a sequence of events from a single source. One
instance, one Lambda execution. Example:
i-0abc123def456. - Event: a line with a timestamp and a message. Maximum 256 KB.
flowchart TD
G["/mercadofresco/tienda/aplicacion<br/>30-day retention, KMS"]
G --> S1["i-0abc123<br/>events"]
G --> S2["i-0def456<br/>events"]
G --> S3["i-0ghi789<br/>events"]
G -.->|metric filter| M["MercadoFresco/Tienda<br/>PedidosFallidos"]
G -.->|subscription| F["Lambda / Firehose /<br/>OpenSearch"]
G -.->|export| B["s3://mercadofresco-registros-web"]
Infinite retention, the most expensive mistake in CloudWatch
By default, a log group is created with Never expire retention. Nobody changes it. Three
years later, MercadoFresco would have 400 GB of 2023 nginx logs costing 0.03 USD/GB/month —12 USD
a month for data nobody is ever going to read— and growing.
And there is something worse than the cost: a Logs Insights query scans whatever you ask it to, and at 0.0063 USD per GB scanned, a careless search over three years of logs costs more than the search itself.
MercadoFresco's policy:
| Log group | Source | Retention | Reason |
|---|---|---|---|
/mercadofresco/tienda/aplicacion |
Agent | 30 days | Operational diagnosis |
/mercadofresco/tienda/nginx-acceso |
Agent | 14 days | High volume, little value after a week |
/mercadofresco/tienda/nginx-error |
Agent | 30 days | |
/aws/lambda/mercadofresco-generar-miniaturas |
Lambda | 14 days | |
/aws/lambda/mercadofresco-estado-pedido |
Lambda | 30 days | Touches orders |
/aws/rds/instance/mercadofresco-pedidos/postgresql |
RDS | 7 days | Slow queries |
aws-waf-logs-mercadofresco |
WAF (04-05) | 30 days | False-positive analysis |
flowlogs-mercadofresco |
VPC (03-01) | 7 days | Enormous; archived to S3 |
And the command that must be run the same day a group is created:
aws logs put-retention-policy \
--log-group-name /mercadofresco/tienda/aplicacion \
--retention-in-days 30 \
--profile mercadofresco-dev --region eu-west-1A useful audit that Marta runs once a month, to find the groups that have slipped through without retention:
aws logs describe-log-groups \
--query "logGroups[?retentionInDays==null].[logGroupName,storedBytes]" \
--output table \
--profile mercadofresco-dev --region eu-west-1In 05-04 we will see how AWS Config turns this manual audit into a rule that fires by itself.
Log classes: Standard versus Infrequent Access
CloudWatch Logs has two storage classes:
Standard |
Infrequent Access |
|
|---|---|---|
| Ingestion | ~0.63 USD/GB | ~0.32 USD/GB (half) |
| Logs Insights | Yes | Yes |
| Live Tail, metric filters, alarms | Yes | No |
| Subscriptions | Yes | Limited |
For flowlogs-mercadofresco, which is only queried when there is a network investigation, the
Infrequent Access class saves half. For /mercadofresco/tienda/aplicacion, on which metric filters
and alarms depend, you have to stay on Standard.
The logs MercadoFresco has scattered around
This was one of the five open questions from module 4: "the WAF, VPC, ALB and Lambda logs pile up in five places with no correlation". Let us look at the real map:
| Log | Where it is | In CloudWatch Logs? | How it is queried |
|---|---|---|---|
| Shop application | Agent → Logs | Yes | Logs Insights |
nginx access/error |
Agent → Logs | Yes | Logs Insights |
| Lambdas | Automatic | Yes | Logs Insights |
| WAF (04-05) | aws-waf-logs-mercadofresco |
Yes | Logs Insights |
| VPC Flow Logs (03-01) | flowlogs-mercadofresco |
Yes | Logs Insights |
| RDS PostgreSQL | Exported to Logs | Yes (it must be enabled) | Logs Insights |
| ALB access logs | S3 | No | Athena (05-03) |
| CloudFront logs | S3 | No (or Logs v2) | Athena |
| S3 access logs | S3 | No | Athena |
| AWS API calls | CloudTrail → S3 | Optional | 05-03 |
Honesty matters here: CloudWatch Logs does not centralise everything. The ALB and CloudFront access logs go to S3 by design, because their volume would make ingestion into Logs prohibitive. The real centralisation at MercadoFresco happens at two levels:
- Hot diagnosis (the last few days): CloudWatch Logs Insights.
- Historical and forensic analysis: S3 + Athena, covered in 05-03.
Enabling the PostgreSQL logs on the RDS instance, which is indeed missing:
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
# And in the parameter group, log the queries taking more than 1 second
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-1That log_min_duration_statement=1000 is going to be decisive in the eight-second order section.
Logs Insights: syntax and real queries
CloudWatch Logs Insights is a query language over the logs. It is not SQL, although it looks
like it: it is a pipeline of commands separated by |.
| Command | What for |
|---|---|
fields |
Choose/create fields |
filter |
Filter events |
stats |
Aggregate (count, avg, sum, pct, min, max) |
sort |
Sort |
limit |
Limit results (1,000 by default) |
parse |
Extract fields from unstructured text |
dedup |
Remove duplicates |
display |
Choose what is shown |
The fields @timestamp, @message, @logStream and @log always exist. If your log is JSON,
Insights breaks it apart on its own and you can refer to nivel, pedido_id, duracion_ms directly.
That is reason number one to log in JSON from day one.
Query 1: shop errors, grouped
fields @timestamp, @message, nivel, pedido_id, duracion_ms | filter nivel = "ERROR" | stats count() as errors by bin(5m), tipo_error | sort errors desc
bin(5m) groups by five-minute windows: that is what turns a list of errors into a trend
graph. When you run it, Insights offers "Visualization" and that result can be added
directly to the dashboard.
Query 2: the WAF logs (04-05)
fields @timestamp, httpRequest.clientIp as ip, httpRequest.uri as path,
terminatingRuleId as rule, action
| filter action = "BLOCK"
| stats count() as blocks by rule, path
| sort blocks desc
| limit 25This is the query that in 04-05 decided the move from Count to Block. Now you know exactly where
it is run.
Query 3: the order-status Lambda
fields @timestamp, @message, @duration, @billedDuration, @maxMemoryUsed
| filter @type = "REPORT"
| stats count() as invocations,
avg(@duration) as avg_ms,
pct(@duration, 95) as p95_ms,
pct(@duration, 99) as p99_ms,
max(@maxMemoryUsed) / 1000000 as max_memory_mb
by bin(1h)The fields @duration, @billedDuration, @maxMemoryUsed and @initDuration are generated by Lambda
automatically in the REPORT line of every invocation. @maxMemoryUsed against the configured
memory is the most direct way of right-sizing a function: if it uses 80 MB of the 512
configured, you are paying too much.
Query 4: VPC flow logs (03-01)
fields @timestamp, srcAddr, dstAddr, dstPort, action, bytes | filter action = "REJECT" and dstPort = 5432 | stats count() as attempts, sum(bytes) as total by srcAddr | sort attempts desc
Somebody trying to reach port 5432 and being rejected by sg-mercadofresco-basedatos. If that
IP is internal, it is a configuration error. If it is external, it is a scan.
Query 5: the most useful of all, parse over unstructured logs
parse @message /(?<ip>\d+\.\d+\.\d+\.\d+) .* "(?<metodo>\w+) (?<ruta>\S+).*" (?<estado>\d{3}) (?<bytes>\d+) (?<tiempo>[\d.]+)/
| filter estado >= 500
| stats count() as errors by ruta, estado
| sort errors descparse with a regular expression and named groups turns the nginx access log into queryable
fields. It is the rescue for all the software that does not log in JSON.
Running Insights from the CLI
QUERY_ID=$(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 "$QUERY_ID" \
--profile mercadofresco-dev --region eu-west-1Cost warning: every run charges for GB scanned, not for results returned. Narrowing the time range is what decides the bill: the same query over 1 hour or over 30 days costs 720 times more in the second case. Always shrink the window before refining the query.
Answering "why did that order take eight seconds?"
This is the second question module 4 left open. A customer writes: "on Thursday afternoon it took about eight seconds for my order to be confirmed". Marta has their email and the rough time.
Prerequisite: a correlation identifier that crosses every component. Without it, this
is impossible. In nginx, MercadoFresco adds an X-Peticion-Id header that the application propagates
to the Lambda and writes on every log line.
Step 1. Confirm the problem exists and narrow it down.
fields @timestamp, duracion_ms, ruta, peticion_id, cliente_hash | filter ruta = "/api/pedidos/confirmar" and duracion_ms > 5000 | sort @timestamp desc | limit 50
34 slow requests show up on Thursday between 18:40 and 19:20. It was not one customer's problem.
Step 2. Find the specific request.
fields @timestamp, peticion_id, duracion_ms, pedido_id | filter cliente_hash = "a3f8c1e9" and duracion_ms > 5000
One comes back: peticion_id = 7f3a9b2c, duracion_ms = 8140.
Step 3. Follow that identifier across all the groups at once. Insights can query up to 50 log groups in a single query, and that is the real answer to "the logs are in five places with no correlation":
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-1Step 4. Read the timeline.
| Time | Group | Message | Δ |
|---|---|---|---|
| 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 |
Found it. It was not the shop (27 + 57 + 52 + 10 + 34 = 180 ms), nor the Lambda (550 ms): it was
7.4 seconds inside PostgreSQL. The log_min_duration_statement logs we enabled
earlier are what gave it away.
Step 5. See which query it was.
And up comes a query that runs a SELECT for the price of each order line, in a loop, instead of
a single SELECT ... WHERE id IN (...). It is the N+1 pattern: an order with 38 products runs 39
queries.
What we have just done and what we have not. We have located the problem with correlated logs, and it worked because MercadoFresco had a request identifier propagated by hand. But look at the effort: four queries, a timeline assembled manually, and all of it resting on somebody remembering to propagate the header. There is a tool designed for exactly this, which does step 4 automatically and in a graph: it is X-Ray, and it is lesson 05-02. We will come back to this same 8.14-second order there.
Metric filters: from a log pattern to an alarm
You cannot alarm on a log; you can on a metric. A metric filter is the conversion.
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-1Three details from the field:
- The
--filter-patternsyntax is not the Insights one. For JSON logs use{ $.field = "value" }with&&,||,=,!=,>,<. For plain text, quotes and terms:"ERROR" -"ERROR test". defaultValue=0is essential. Without it, when there are no errors no data point is published, the alarm stays inINSUFFICIENT_DATAand the metric has gaps that break anomaly detection. WithdefaultValue=0the series is continuous.- Filters only apply to new events. There is no retroactive effect on what has already been ingested.
You can also extract a numeric value from the log instead of counting:
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-1And testing the pattern before creating it, against real events, which avoids the classic filter that never matches:
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-1Subscriptions and export to S3
Two ways of getting logs out of CloudWatch, with different purposes:
| Subscription filter | Export to S3 | |
|---|---|---|
| Latency | Real time (seconds) | Batch, up to 12 h |
| Destinations | Lambda, Firehose, OpenSearch, Kinesis | S3 only |
| Typical use | Process, forward, alert | Cheap archiving, Athena |
| Cost | That of the destination | S3 only |
| Limit | 2 filters per log group | — |
A real-time subscription to a Lambda that forwards the critical entries to Slack:
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-1Export to S3 for cheap archiving of the flow logs, which will be looked at once a year:
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-1The bucket needs a policy allowing logs.eu-west-1.amazonaws.com to write. And watch out: there
can only be one active export task per account at a time, and it is not incremental. For continuous
archiving, the modern option is a subscription to Data Firehose with S3 as the destination.
Alarms: thresholds, evaluation and missing data
An alarm has three states:
| State | What it means |
|---|---|
OK |
The condition is not met. All good. |
ALARM |
The condition is met. |
INSUFFICIENT_DATA |
There is not enough data to decide. |
And four parameters that almost nobody configures right first time:
--period: the aggregation window. 60, 300…--evaluation-periods: how many periods are looked at.--datapoints-to-alarm: how many of those must breach. If omitted, all of them.--treat-missing-data: what to do with the gaps.
The evaluation-periods + datapoints-to-alarm combination is called "M out of N" and it is the
tool against noise. Compare:
| Configuration | Behaviour | When |
|---|---|---|
| 1 out of 1, period 60 | Fires on the first bad minute | Only for the binary and serious |
| 3 out of 3, period 60 | Three bad minutes in a row | Sustained, takes 3 min |
| 2 out of 3, period 60 | 2 bad minutes out of the last 3 | Recommended balance |
| 5 out of 5, period 300 | 25 minutes | Slow trends: disk |
With "2 out of 3", a spike lasting a single minute —a deployment, a garbage collector, a backup—
does not wake anybody up, but a real problem that oscillates is detected. The "3 out of 3"
configuration misses exactly those intermittent problems.
And --treat-missing-data, four options:
| Value | Behaviour | When to use it |
|---|---|---|
missing (default) |
The gap does not count | Almost never; it causes confusion |
notBreaching |
The gap is treated as OK |
Metrics with intermittent traffic |
breaching |
The gap is treated as ALARM |
When the absence of data IS the problem |
ignore |
The state does not change | Keep the last known state |
The case that explains everything: the alarm on PedidosConfirmados. If the shop goes down completely,
the metric stops being published. With notBreaching, the alarm stays in OK while the shop is
dead. It is the most common silent failure in CloudWatch, and that is why that particular alarm carries
--treat-missing-data breaching.
The alarms MercadoFresco creates in this lesson:
# 1. Real latency as seen by the customer: p95, not the average.
aws cloudwatch put-metric-alarm \
--alarm-name mercadofresco-alb-latencia-alta \
--alarm-description "The ALB p95 response time exceeds 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. Healthy targets: below 2, we have lost AZ fault tolerance.
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. RDS connections: 160 of the 200 in the parameter group.
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. RDS free space: in 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. Lambda throttling: threshold 0, no tolerance.
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. Instance memory, thanks to the agent installed earlier.
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. Failed orders, on the metric from the log filter.
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. The most important one: the shop has stopped selling.
# treat-missing-data BREACHING because the absence of data IS the incident.
aws cloudwatch put-metric-alarm \
--alarm-name mercadofresco-sin-pedidos \
--alarm-description "No confirmed orders: the shop may be down" \
--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-1A note on --ok-actions in alarm 1: send the recovery too. Without it, somebody gets a
notice in the small hours with no way of knowing whether it is still happening. With --ok-actions,
the second message says "it is fine now". A small detail that changes on-call life a great deal.
And a warning about alarm 8: PedidosConfirmados < 1 in 15 minutes is right for
business hours, but at 4 in the morning on a Sunday it may be perfectly normal to have no
orders. That alarm generates night-time false positives. The right way to solve it is not to raise the
period: it is anomaly detection, two sections further down.
Composite alarms
A composite alarm is evaluated over other alarms, with AND, OR and NOT. It serves two
different purposes:
- Reducing noise: only notify if several signals coincide.
- Suppressing child alarms during a known maintenance window.
MercadoFresco's real problem: when the database saturates on a Friday, mercadofresco-rds-conexiones-altas,
mercadofresco-alb-latencia-alta and mercadofresco-pedidos-fallidos all fire at once.
Three text messages at 19:15 describing the same incident.
aws cloudwatch put-composite-alarm \
--alarm-name mercadofresco-tienda-degradada \
--alarm-description "The shop is degraded: high latency AND order errors" \
--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-1And now the step most people forget: remove the SNS action from the child alarms. Otherwise you carry on receiving four notices instead of three.
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-1The child alarm still exists, still changes state and is still visible on the dashboard: it simply no longer notifies. The notice comes from the composite one, which carries the diagnosis.
Suppression during maintenance: the --actions-suppressor parameter lets you point at another
alarm —for example, one Marta puts into ALARM by hand during a deployment— that silences the
composite while it is active.
Cost: 0.50 USD per composite alarm a month. It usually pays for itself just by not waking somebody up three times.
Anomaly detection
Instead of a fixed threshold, CloudWatch trains a model with up to two weeks of history and learns the pattern: the Friday peaks, the small-hours troughs, the weekly cycle. The alarm fires when the value leaves the expected band.
It is exactly what solves the night-time false positive of mercadofresco-sin-pedidos: at 4 in the
morning the model expects 3 orders, and 0 is anomalous; at 19:00 it expects 900, and 400 is anomalous
too. A fixed threshold cannot express that.
# 1. Create the detector, which starts training.
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. The alarm on the band, with a metric expression.
aws cloudwatch put-metric-alarm \
--alarm-name mercadofresco-pedidos-anomalos \
--alarm-description "Orders are outside the expected band for this time of day" \
--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 (expected band)",
"ReturnData": true
}
]' \
--alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
--profile mercadofresco-dev --region eu-west-1Key details:
LessThanLowerThreshold: it only notifies if there are fewer orders than expected. More is good news, not an alarm. There are alsoGreaterThanUpperThresholdandLessThanLowerOrGreaterThanUpperThreshold.- The
2inANOMALY_DETECTION_BAND(m1, 2)is the number of deviations. Higher = wider band = fewer notices. Start at 2 and adjust with real data. - It needs history. For the first few days the band is enormous and the alarm is useless. You have to create it and wait.
- You can exclude a period from training with
--configurationandExcludedTimeRanges: if there was a 6-hour incident, you do not want the model to learn that that is normal.
Cost: 0.30 USD per analysed metric a month, plus 0.30 USD per anomaly alarm.
And an honest warning: anomaly detection is not magic. It detects deviations from the historical pattern, not problems. A slow, steady degradation over three weeks becomes the new "normal" and stops notifying. It complements fixed thresholds; it does not replace them.
Alarm actions
| Action | ARN / form | Use at MercadoFresco |
|---|---|---|
| Notify via SNS | arn:aws:sns:...:alertas-mercadofresco |
Every alarm |
| Auto Scaling | ARN of a scaling policy | The ASG's cpu-objetivo-60 |
| EC2 action | arn:aws:automate:eu-west-1:ec2:reboot |
recover, stop, terminate, reboot |
| Systems Manager action | ARN of an OpsItem or incident | Create tickets automatically |
| None | — | Child alarms of a composite one |
The automatic EC2 actions are useful but dangerous. arn:aws:automate:eu-west-1:ec2:recover
on StatusCheckFailed_System migrates the instance to another host when the underlying hardware
fails, keeping IP and volumes: that is clearly good. By contrast
arn:aws:automate:eu-west-1:ec2:reboot on high memory is a bad idea: you reboot in a loop an
instance with a memory leak and hide the problem instead of fixing it.
For the ASG instances, moreover, the right action is almost never to reboot: it is to let them fail
the /salud health check and let the ASG itself replace them, as we built in 02-01 and 03-03.
Checking that the notice really arrives
Here is the first open question from module 4, and the most important one in this lesson: nobody has checked that an SNS notice reaches a phone at four in the morning.
An alarm that fires at an SNS topic with no confirmed subscribers is worse than having no alarm: it gives a false sense of coverage.
Step 1. See who is really subscribed.
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-1If the SubscriptionArn column shows PendingConfirmation, that subscription receives
nothing. It is the most frequent finding when this check is done for the first time: somebody created
the subscription months ago, the confirmation email went to the junk folder, and nobody
knew.
Step 2. Subscribe the endpoints that are missing.
# Team email (requires confirming the link in the message received)
aws sns subscribe \
--topic-arn arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
--protocol email --notification-endpoint aws-alertas@mercadofresco.example \
--profile mercadofresco-dev --region eu-west-1
# SMS to the on-call phone: it confirms itself, there is no link to click
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-1A note on SMS. For some years now, sending SMS with SNS in many regions has required leaving the SMS "sandbox" and, for Spanish numbers, registering a sender and a use case. If your test SMS does not arrive, check the sandbox status before writing the alarm off as broken. The full detail of SNS —FIFO topics, delivery policies, subscription filters— is lesson 07-02.
Step 3. The drill. Force the alarm state.
This is the command that closes module 4's question:
aws cloudwatch set-alarm-state \
--alarm-name mercadofresco-alb-latencia-alta \
--state-value ALARM \
--state-reason "DRILL $(date -u +%FT%TZ): quarterly notification check" \
--profile mercadofresco-dev --region eu-west-1This does not touch the metric: it forces the alarm state and fires its actions for real. The SMS and the email go out. It is the only honest way of knowing that the channel works.
Afterwards it is returned to reality:
aws cloudwatch set-alarm-state \
--alarm-name mercadofresco-alb-latencia-alta \
--state-value OK \
--state-reason "End of drill" \
--profile mercadofresco-dev --region eu-west-1On the next evaluation period, CloudWatch recomputes the real state from the metric data and corrects it by itself if need be.
Step 4. The protocol MercadoFresco adopts.
| When | What is checked | Who |
|---|---|---|
| Every quarter | Drill with set-alarm-state on 3 critical alarms |
Marta |
| Every quarter | That there are no subscriptions in PendingConfirmation |
Marta |
| When somebody joins | They are subscribed and a drill is run with their phone | Marta |
| When somebody leaves | Their subscription is deleted | Marta |
| After every incident | Did the right alarm fire? In time? Was anybody there? | Post-mortem |
And one more check, which usually uncovers the real problem: run the drill at 4 in the morning for real, once. Marta did. She discovered that the on-call mobile was on "do not disturb" and that SMS from short numbers did not break the silence. The fix was not an AWS one: it was to configure an exception on the phone. The notification chain includes the phone, and the phone has to be tested too.
The mercadofresco-produccion dashboard
A CloudWatch dashboard is a JSON document with a grid 24 columns wide. Widgets are
positioned with x, y, width, height.
The criterion Marta designed it with —and this is what you should copy, rather than the JSON— is: at the top whatever answers "is the business all right?", in the middle "which component is failing?", at the bottom the detail. When the phone rings, you read down and in 30 seconds you know where to go.
{
"start": "-PT3H",
"periodOverride": "auto",
"widgets": [
{
"type": "text",
"x": 0, "y": 0, "width": 24, "height": 1,
"properties": {
"markdown": "# MercadoFresco - Production (eu-west-1) | On call: aws-alertas@mercadofresco.example"
}
},
{
"type": "metric",
"x": 0, "y": 1, "width": 6, "height": 4,
"properties": {
"title": "Confirmed orders (last hour)",
"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": "Confirmation time 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": "Critical alarm status",
"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 - latency and errors",
"view": "timeSeries",
"stacked": false,
"region": "eu-west-1",
"period": 60,
"yAxis": {
"left": { "label": "seconds", "showUnits": false },
"right": { "label": "errors", "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 application", "color": "#d62728" }],
["AWS/ApplicationELB", "HTTPCode_ELB_5XX_Count",
"LoadBalancer", "app/alb-mercadofresco-tienda/50dc6c495c0c9188",
{ "stat": "Sum", "yAxis": "right", "label": "5XX load balancer", "color": "#ff7f0e" }]
],
"annotations": {
"horizontal": [
{ "label": "p95 target", "value": 2, "color": "#d62728", "fill": "above" }
]
}
}
},
{
"type": "metric",
"x": 12, "y": 5, "width": 12, "height": 6,
"properties": {
"title": "Database - mercadofresco-pedidos",
"view": "timeSeries",
"region": "eu-west-1",
"period": 60,
"metrics": [
["AWS/RDS", "DatabaseConnections",
"DBInstanceIdentifier", "mercadofresco-pedidos",
{ "stat": "Maximum", "label": "Connections" }],
["AWS/RDS", "CPUUtilization",
"DBInstanceIdentifier", "mercadofresco-pedidos",
{ "stat": "Average", "label": "CPU %", "yAxis": "right" }],
["AWS/RDS", "ReplicaLag",
"DBInstanceIdentifier", "mercadofresco-pedidos-lectura",
{ "stat": "Maximum", "label": "Replica lag (s)", "yAxis": "right" }]
],
"annotations": {
"horizontal": [
{ "label": "Connection limit", "value": 200, "color": "#d62728" },
{ "label": "Alarm", "value": 160, "color": "#ff7f0e" }
]
}
}
},
{
"type": "metric",
"x": 0, "y": 11, "width": 8, "height": 6,
"properties": {
"title": "ASG and system",
"view": "timeSeries",
"region": "eu-west-1",
"period": 60,
"metrics": [
["AWS/EC2", "CPUUtilization",
"AutoScalingGroupName", "asg-mercadofresco-tienda",
{ "stat": "Average", "label": "Average CPU %" }],
["MercadoFresco/Sistema", "MemoriaUsadaPorcentaje",
"AutoScalingGroupName", "asg-mercadofresco-tienda",
{ "stat": "Average", "label": "Memory %" }],
["AWS/AutoScaling", "GroupInServiceInstances",
"AutoScalingGroupName", "asg-mercadofresco-tienda",
{ "stat": "Maximum", "label": "Instances", "yAxis": "right" }]
],
"annotations": {
"horizontal": [
{ "label": "ASG maximum", "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": "Errors", "yAxis": "right", "color": "#d62728" }],
["AWS/Lambda", "Throttles", "FunctionName", "mercadofresco-estado-pedido",
{ "stat": "Sum", "label": "Throttles", "yAxis": "right", "color": "#ff7f0e" }],
["AWS/Lambda", "Errors", "FunctionName", "mercadofresco-generar-miniaturas",
{ "stat": "Sum", "label": "Thumbnails: errors", "yAxis": "right" }]
]
}
},
{
"type": "metric",
"x": 16, "y": 11, "width": 8, "height": 6,
"properties": {
"title": "Edge: CloudFront and WAF",
"view": "timeSeries",
"region": "us-east-1",
"period": 300,
"metrics": [
["AWS/CloudFront", "CacheHitRate", "DistributionId", "E2QWERTY123ABC",
"Region", "Global", { "stat": "Average", "label": "Cache hits %" }],
["AWS/WAFV2", "BlockedRequests", "WebACL", "waf-mercadofresco-cdn",
"Rule", "ALL", "Region", "Global",
{ "stat": "Sum", "label": "WAF blocks", "yAxis": "right" }]
]
}
},
{
"type": "log",
"x": 0, "y": 17, "width": 24, "height": 7,
"properties": {
"title": "Latest shop errors",
"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"
}
}
]
}And creating it:
aws cloudwatch put-dashboard \
--dashboard-name mercadofresco-produccion \
--dashboard-body file://panel-produccion.json \
--profile mercadofresco-dev --region eu-west-1Details of the JSON that deserve explanation:
"start": "-PT3H"sets the default window to the last 3 hours. ISO 8601 duration syntax:-PT1H,-P1D,-P7D.["...", { "stat": "p95" }]repeats the previous metric changing only the statistic. It saves repeating the long ARN three times.- The edge widget carries
"region": "us-east-1". CloudFront's metrics and those of the CloudFront web ACL live there, the same trap as in 03-04 and 04-05. A dashboard can mix regions widget by widget, and that makes it the only place where MercadoFresco sees everything together. - The horizontal
annotationsdraw the threshold line. Watching the graph approach the red line before the alarm fires is half the value of a dashboard. - The
logwidget runs an Insights query every time the dashboard loads. Careful: that costs money every time, and if the dashboard sits on an office screen refreshing every minute, that is 1,440 queries a day. Marta gave itlimit 20and a short window for that reason.
Dashboard cost: the first 3 are free; beyond that, 3 USD per dashboard a month.
MercadoFresco has two: mercadofresco-produccion and mercadofresco-negocio (Sara's, with orders
and amounts). Cost: 0 USD.
ServiceLens and Synthetics
Two CloudWatch features mentioned here and put to use in 05-02:
ServiceLens brings together metrics, logs and X-Ray traces in a single service map. It needs X-Ray to be active, so it is covered in the next lesson.
CloudWatch Synthetics runs "canaries": small scripts that behave like a customer, from outside, every N minutes. It is synthetic monitoring: it does not wait for a real customer to detect that the shop is broken. With 900 orders/hour that may not seem important, but at 3 in the morning on a Sunday two hours can go by without a single purchase, and that is exactly the gap through which an outage slips.
MercadoFresco's canary, canario-mercadofresco-compra, runs every 5 minutes and does the
full journey: home page, search for "tomate", open the product page, add to basket, reach the payment
form (without paying). If any step fails, it fires 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-1Two practical warnings:
- A canary that really buys generates real orders. MercadoFresco's stops before confirming, and the test user is flagged so that Sara excludes it from her reports.
- Cost: 0.0012 USD per run. Every 5 minutes is 8,640 runs a month: 10.37 USD, plus the screenshots in S3 and the traces. It is not negligible; it is the most expensive item in this lesson after the logs.
CloudWatch cost and how it explodes
Reference prices in eu-west-1 (approximate; always check the official calculator):
| Item | Price | Note |
|---|---|---|
AWS metrics (AWS/*) |
Free | 5-minute resolution |
| EC2 detailed monitoring | ~2.10 USD/instance/month | Down to 1 min |
| Custom metric | 0.30 USD/month each | First 10,000 |
PutMetricData |
0.01 USD per 1,000 calls | Hence the batching |
Standard log ingestion |
~0.63 USD/GB | The big item |
Infrequent Access ingestion |
~0.32 USD/GB | No filters or alarms |
| Log storage | 0.03 USD/GB/month | Compressed |
| Logs Insights | 0.0063 USD/GB scanned | Per query |
| Standard alarm | 0.10 USD/month | |
| High-resolution alarm | 0.30 USD/month | |
| Composite alarm | 0.50 USD/month | |
| Anomaly detection | 0.30 USD/metric/month | Plus the alarm |
| Dashboard | 3 free, then 3 USD/month | |
| Synthetics canary | 0.0012 USD/run | |
| Contributor Insights | 0.50 USD/rule + queries |
MercadoFresco's calculation:
| Item | Quantity | Monthly cost |
|---|---|---|
| Custom business metrics | 8 | 2.40 USD |
| Agent metrics (system, aggregated) | 20 | 6.00 USD |
| EMF metrics from the thumbnails | 6 | 1.80 USD |
| EC2 detailed monitoring | 2-4 instances | ~6.30 USD |
| Log ingestion | ~22 GB/month | 13.86 USD |
| Log storage | ~12 GB on average | 0.36 USD |
| Logs Insights | ~30 GB scanned | 0.19 USD |
| Standard alarms (13) | 13 | 1.30 USD |
| Composite alarm | 1 | 0.50 USD |
| Anomaly detection | 1 metric + 1 alarm | 0.60 USD |
| Dashboards | 2 (first 3 free) | 0.00 USD |
| Canary every 5 min | 8,640 runs | 10.37 USD |
| Total | ~43.68 USD/month |
The five ways to blow up this bill, in order of how often it really happens:
- Infinite retention on the log groups. It grows non-stop and you never notice it all at once.
DEBUGleft on in production. It multiplies ingestion by ten from one day to the next. Ingestion is the expensive item: 0.63 USD/GB.- A high-cardinality dimension.
PedidoIdas a dimension: tens of thousands of USD. It is the most expensive mistake you can make in CloudWatch, and it is made in one line. - Insights queries over 30 days for debugging. Every refinement of the query scans everything again.
- A dashboard with log widgets on an office screen. It runs queries every minute for ever.
And the defence: an AWS Budgets budget filtered by the CloudWatch service (module 11) with a notice at 80 %. In 05-04 we will also see how AWS Config automatically detects log groups without retention.
Cleanup
# Alarms
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
# The composite one is deleted the same way, but NOT after the children: before.
aws cloudwatch delete-alarms --alarm-names mercadofresco-tienda-degradada \
--profile mercadofresco-dev --region eu-west-1
# Anomaly detector
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
# Dashboard
aws cloudwatch delete-dashboards --dashboard-names mercadofresco-produccion \
--profile mercadofresco-dev --region eu-west-1
# Canary: stop it first, then delete it
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
# Metric filters
aws logs delete-metric-filter \
--log-group-name /mercadofresco/tienda/aplicacion \
--filter-name filtro-pedidos-fallidos \
--profile mercadofresco-dev --region eu-west-1
# Log groups (this DELETES the data: irreversible)
aws logs delete-log-group --log-group-name /mercadofresco/tienda/nginx-acceso \
--profile mercadofresco-dev --region eu-west-1Two warnings:
- Custom metrics cannot be deleted. They stop being billed when they stop receiving data (after the corresponding period), but they remain visible for a while. It is one more reason not to create dimensions lightly.
- Deleting a log group removes its data irreversibly. If there may be a retention obligation, export it to S3 first and check with whoever handles compliance.
Common Mistakes and Tips
1. Alarming on the average of a latency. It is mistake number one. Average of
TargetResponseTime stays flat while p99 is at 9 seconds. Use --extended-statistic p95 or p99 for anything that is a time.
2. Leaving log retention on "Never expire". It is the default value. It is the most
expensive mistake in the medium term. Set retention the same day you create the group, and audit monthly with
describe-log-groups.
3. Using an identifier as a dimension. PedidoId, UserId, RequestId in a dimension create
a new metric every time. Thousands of USD. That data belongs in the log or in an X-Ray annotation.
4. A badly chosen --treat-missing-data. For a metric whose absence is the problem
—PedidosConfirmados, HealthyHostCount— you have to set breaching. With notBreaching the alarm
sits quietly while the shop is dead.
5. Confusing HTTPCode_ELB_5XX_Count with HTTPCode_Target_5XX_Count. The first belongs to the
load balancer (no healthy targets, timeout); the second is your application. Searching in the wrong
place costs hours.
6. Thresholds in the wrong units. FreeStorageSpace is in bytes; Lambda's Duration in
milliseconds; TargetResponseTime in seconds. One zero too many or too few and the alarm never fires.
7. Creating an alarm without checking the SNS subscription. If the ARN is right but nobody has
confirmed the subscription, the alarm fires into the void. Check with list-subscriptions-by-topic and
run a drill with set-alarm-state.
8. Not configuring --ok-actions. Whoever gets a notice in the small hours needs to know when it has
recovered. Without the notification of the return to OK, somebody gets up for nothing or, worse, does not
get up when they should.
9. Looking in CloudWatch for who deleted a resource. That is in CloudTrail (05-03). CloudWatch records what your application says about itself.
10. CloudFront metrics looked for in eu-west-1. They are in us-east-1, always. Just like
ACM certificates (03-04) and the CloudFront web ACL (04-05).
11. Insights queries over enormous ranges while debugging. Start with 15 minutes, refine the query, and only then widen the window. You pay per GB scanned on every run.
12. Logging plain text instead of JSON. With JSON, Insights breaks the fields apart on its own and
queries are trivial. With plain text you have to write regular expressions with parse. Changing the
log format on day one costs an hour; doing it three years later, weeks.
13. A metric filter without defaultValue=0. The series has gaps, the alarm stays in
INSUFFICIENT_DATA and anomaly detection does not work.
14. Forgetting to remove the actions from the child alarms when creating a composite one. You end up getting one notice more, not fewer.
Final tip: the most valuable metric in a system is almost never a technical one. PedidosConfirmados
detects more real incidents than CPUUtilization, because there can be a thousand ways for the shop
to fail with the CPU at 30 %, but there is only one way for orders to drop to zero.
Exercises
Exercise 1: designing the alarm that detects a partial failure
MercadoFresco deploys a new version of the shop on a Tuesday at 11:00. The version has a bug that only affects orders paid with a card from one particular bank: roughly 12 % of purchases. The rest works normally.
Incident data: CPUUtilization unchanged (42 %), HealthyHostCount at 2,
TargetResponseTime p95 stable at 0.6 s, HTTPCode_Target_5XX_Count with 3 errors every 5 minutes
(before: 1). PedidosConfirmados goes from 900/h to 792/h. In the logs, lines appear reading
{"nivel":"ERROR","tipo_error":"PAGO_RECHAZADO","pasarela":"banco-x"}.
No current alarm fires. The bug is found 6 hours later through a complaint on social media.
Design the detection: which metric or metrics you would use, how you would obtain them (metric filter, custom metric, expression), what kind of alarm and with exactly which parameters for period, evaluation and missing data. Write the commands. Justify why your proposal would have fired in less than 30 minutes without generating false positives for the rest of the month.
Exercise 2: the Logs Insights query and the metric filter
Sara asks whether the shop's search is working properly. The application writes one JSON line per search:
{"nivel":"INFO","evento":"busqueda","termino":"tomate rama","resultados":24,"duracion_ms":180,"cliente_hash":"a3f8c1e9"}Write:
- a) An Insights query giving, per hour, the number of searches, the average duration, the p95 and the percentage of searches with no results.
- b) A query listing the 20 most searched terms that return zero results (they are products MercadoFresco ought to have in its catalogue: direct value for Sara).
- c) A metric filter and an alarm that warn if the percentage of searches with no results exceeds 20 % for 15 minutes, which is the sign that the search index has been corrupted. Careful: a metric filter counts, it does not compute percentages. You will need a metric expression in the alarm.
Exercise 3: the cost audit
Marta gets the bill and CloudWatch has gone from 44 to 610 USD in a month. The breakdown:
| Item | Amount |
|---|---|
Log ingestion (Standard) |
412 USD |
| Custom metrics | 96 USD |
| Logs Insights | 71 USD |
| Alarms and dashboards | 4 USD |
| Synthetics | 27 USD |
Investigation clues:
- The
/mercadofresco/tienda/aplicaciongroup has gone from 8 GB to 620 GB in the month. - The
MercadoFresco/Tiendanamespace now has 320 metrics; last month it had 8. - There are 11,200 Insights runs, all with a 30-day window.
- There is a new canary,
canario-mercadofresco-admin, running every minute. - Three weeks ago Luis deployed a "recommended orders" function that logs the activity of every customer.
Diagnose each item, say exactly what Luis did wrong in each case, write the corrective actions with the commands, and propose three preventive controls so that it does not happen again. Estimate the resulting bill.
Solutions
Solution 1
Why nothing fired. All the current alarms look at the infrastructure, and the infrastructure is perfect. A 12 % failure on one particular gateway is invisible to the CPU, to the number of healthy targets and to the latency p95 (a rejected payment answers fast, even faster than a correct one). And the drop in orders from 900 to 792 —12 %— sits within the normal variation of a Tuesday. No reasonable fixed threshold detects 12 %.
This is what is called a partial failure, and it is the kind of incident that takes longest to detect in real systems.
Proposed detection: three layers.
Layer 1 — the direct signal. A metric filter on the specific error, broken down by gateway.
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-1Layer 2 — the ratio alarm, which is the good one. An absolute threshold of "more than N rejections" fails in both directions: at night 3 rejections are a huge number, at the Friday peak they are nothing. The right thing is to alarm on the percentage, with a metric expression:
aws cloudwatch put-metric-alarm \
--alarm-name mercadofresco-tasa-rechazo-pagos \
--alarm-description "More than 5% of payment attempts are rejected" \
--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":"% of payments rejected","ReturnData":true}
]' \
--alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
--profile mercadofresco-dev --region eu-west-1With 12 % rejections against a threshold of 5 %, and "2 out of 3" periods of 5 minutes, this alarm fires
in 10-15 minutes. The requirement is to publish IntentosPago too (a second metric filter or
a custom metric); without a denominator there is no ratio.
Layer 3 — the generic safety net: anomaly detection on PedidosConfirmados. A
12 % drop sustained for 6 hours does leave the anomaly band, because the model knows
the pattern of a Tuesday morning precisely. It is the mercadofresco-pedidos-anomalos alarm we
already created: it would have fired, probably within the first hour. It is slower than layer 2, but it is
the only one that detects problems nobody has anticipated.
Why it does not generate false positives. Layer 2 looks at a ratio, and a ratio is independent of volume: it works the same at 4 in the morning as on Friday at 19:00. With "2 out of 3" periods of 5 minutes, an isolated spike of rejections —a bank restarting its gateway for a minute— triggers nothing.
The underlying lesson: infrastructure alarms detect total outages. Partial failures, which are the most frequent and the most expensive, are only detected by business metrics and ratios.
Solution 2
a) Hourly overview.
fields @timestamp, duracion_ms, resultados, termino
| filter evento = "busqueda"
| stats count() as searches,
avg(duracion_ms) as avg_ms,
pct(duracion_ms, 95) as p95_ms,
sum(resultados = 0) as no_results,
100.0 * sum(resultados = 0) / count() as pct_no_results
by bin(1h)
| sort @timestamp descThe key is in sum(resultados = 0): in Insights, a boolean expression is worth 1 or 0, so
summing it counts the true cases. It is the idiom for computing percentages in this language.
b) Terms with no results: direct business value.
fields termino | filter evento = "busqueda" and resultados = 0 | stats count() as times by termino | sort times desc | limit 20
This is not technical monitoring: it is a list of products customers search for and MercadoFresco does not sell. Marta sends it to Sara every Monday. It is the best example of application logs being worth far more than debugging.
c) Metric filter and ratio alarm.
Two filters, because you need a numerator and a denominator:
# Denominator: all searches
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
# Numerator: the ones returning nothing
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-1And the alarm with an expression:
aws cloudwatch put-metric-alarm \
--alarm-name mercadofresco-buscador-degradado \
--alarm-description "More than 20% of searches return no results" \
--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":"% of searches with no results","ReturnData":true}
]' \
--alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
--profile mercadofresco-dev --region eu-west-1Three justified decisions:
MAX([total, 1])in the denominator avoids the division by zero when there are no searches in the small hours. Without it, the expression produces non-numeric values and the alarm behaves erratically.- 3 out of 3 periods of 5 minutes = 15 minutes, exactly as the brief asked. Here "3 out of 3" is preferable to "2 out of 3": a corrupted index does not fix itself, so the signal will be sustained, and waiting 15 minutes avoids the noise of a search-index deployment.
notBreachingbecause in the small hours there may be no searches, and that is not an incident.
Solution 3
Diagnosis, item by item.
1. Log ingestion: 412 USD (8 GB → 620 GB). Luis's "recommended orders" function
logs the activity of every customer on every visit. It probably writes one line per product
seen, and very probably with nivel: DEBUG because that is how he left it after debugging. 612 GB extra at
0.63 USD/GB is 386 USD. It is the main cause.
Luis's mistake: leaving DEBUG on in production and writing into the application log a stream of
analytics events that is not an application log.
Correction, in two stages:
# Immediate: lower the log level via environment variable / parameter
aws ssm put-parameter --name /mercadofresco/produccion/nivel-log \
--value INFO --type String --overwrite \
--profile mercadofresco-dev --region eu-west-1
# And make sure retention is set, which is very probably still "never expires"
aws logs put-retention-policy \
--log-group-name /mercadofresco/tienda/aplicacion \
--retention-in-days 30 \
--profile mercadofresco-dev --region eu-west-1Underlying fix: customer behaviour events do not go to CloudWatch Logs. They go to
mercadofresco-informes-analitica in S3, via Firehose, where storage costs 0.023 USD/GB instead
of the 0.63 USD/GB of ingestion. It is 27 times cheaper and it is where Sara wants them anyway.
2. Custom metrics: 96 USD (8 → 320 metrics). 320 × 0.30 = 96 USD. Almost certainly,
the recommendations function publishes a metric with a medium-cardinality dimension —CategoriaProducto
with 300 values, or ModeloRecomendador—.
Luis's mistake: using as a dimension something that is not a small, closed category.
Correction: remove that dimension and, if the breakdown is needed, put it in as an EMF property, which is queryable with Insights and does not cost 0.30 USD per value. Important note for the mental exam: the metrics bill does not go down until the following month, because metrics already created are billed while they receive data. They cannot be deleted; you have to stop publishing them.
And the real prevention, in the role's IAM policy —the 04-01 pattern—:
{
"Effect": "Allow",
"Action": "cloudwatch:PutMetricData",
"Resource": "*",
"Condition": {
"StringEquals": { "cloudwatch:namespace": "MercadoFresco/Tienda" }
}
}This does not limit cardinality —IAM cannot—, but it does stop an application creating new namespaces uncontrolled, which is the other half of the problem.
3. Logs Insights: 71 USD (11,200 runs over 30 days). 11,200 queries scanning enormous volumes. Two overlapping causes: somebody debugging with the window on "30 days" without changing it, and very probably a log widget on a dashboard open on a screen that re-runs the query every time it refreshes.
Correction: reduce the default window of the log widgets, set a low limit, and establish
the rule of always starting with 15 minutes. Since the group drops from 620 GB to ~8 GB once point 1
is fixed, this item falls by more than 95 % on its own.
4. Synthetics: 27 USD. canario-mercadofresco-admin every minute is 43,200 runs a month.
For an admin dashboard used by three people during office hours, it is absurd.
Correction:
aws synthetics update-canary \
--name canario-mercadofresco-admin \
--schedule Expression="rate(15 minutes)" \
--profile mercadofresco-dev --region eu-west-1From 43,200 to 2,880 runs: from 51.84 to 3.46 USD. And if it only matters during office hours, it can be
scheduled with a cron expression instead of rate.
5. Alarms and dashboards: 4 USD. Correct. Leave it alone.
Estimated bill after the corrections:
| Item | Before | After | How |
|---|---|---|---|
| Log ingestion | 412 USD | ~14 USD | INFO + analytics to S3 + retention |
| Custom metrics | 96 USD | ~10 USD | Remove the dimension (effective next month) |
| Logs Insights | 71 USD | ~1 USD | Less volume and bounded windows |
| Alarms and dashboards | 4 USD | 4 USD | — |
| Synthetics | 27 USD | ~14 USD | Admin canary every 15 min |
| Total | 610 USD | ~43 USD |
Three preventive controls:
- An AWS Budgets budget filtered by service = CloudWatch, with a threshold of 60 USD and a notice at
80 % to
aws-alertas@mercadofresco.example. It spots the deviation in days, not in next month's bill. Covered in module 11. - An AWS Config rule flagging as non-compliant any log group without
retentionInDays, with automatic remediation setting it to 30 days. It is exactly the use case of lesson 05-04. - A review step in the deployment process: nobody deploys code that writes new logs without somebody else reviewing the estimated volume and the log level. It is a checkbox in code review, not a tool. Module 8 integrates it into the pipeline.
And a fourth, cultural one, which is the one that really works: show people the bill. When Luis saw that
his debug logs cost 386 USD a month —more than the EC2 instances that generated them— he never
left DEBUG on again.
Conclusion
MercadoFresco now has eyes. You know that CloudWatch is three things —metrics, logs and alarms— and you know what it is not: not the record of who called the API (CloudTrail, 05-03), not the request tracer (X-Ray, 05-02), not the compliance evaluator (Config, 05-04), nor the event bus (EventBridge, 07-03).
You have mastered the metrics model: namespace, name and dimensions, with the golden rule that
every combination of dimensions is a billable metric and that an identifier in a dimension
is the most expensive mistake in this service. You know how to choose the statistic —Sum for counters,
Average for utilisation, p95/p99 always for latency— and why the 0.17 s average hid
the fact that ten customers a minute were waiting nine seconds to pay. You know the automatic retention that
aggregates data to 5 minutes after 15 days, and the table of the metrics that really decide something in
every service you have built, with the critical difference between HTTPCode_ELB_5XX_Count and
HTTPCode_Target_5XX_Count.
You have published the business metrics PedidosConfirmados and TiempoConfirmacionPedido in
MercadoFresco/Tienda, first with direct PutMetricData and then in batches from a separate thread
to take the call out of the critical path of a purchase. You know why EC2 does not publish memory —the
hypervisor cannot see inside the guest operating system— and you have deployed the unified agent on
lt-mercadofresco-tienda with its configuration in Parameter Store, its append_dimensions resolved
by themselves and its retention_in_days set from day one. And you know the Embedded Metric Format,
which in Lambda gives you free metrics in the critical path and —this is the important part— lets you keep
the S3 key and the request ID next to the metric without paying for cardinality.
In logs, you have centralised what could be centralised and you know what cannot: the ALB and CloudFront
accesses live in S3 and are queried with Athena. You have the retention policy per group, the
Infrequent Access class for the flow logs, and the Logs Insights queries that answer
real questions —WAF blocks by rule, the p99 of a Lambda with @duration, the rejections
at port 5432, and parse with a regular expression for software that does not log in JSON—. And, above
all, you have answered the question of the eight-second order: four log groups queried
at once with a correlation identifier, a timeline assembled event by event, and the
culprit identified —7,402 ms inside PostgreSQL, an N+1 pattern with 39 queries for an order
of 38 products—.
You know how to turn a log pattern into a metric with a metric filter (and why defaultValue=0
is not optional), and how to get logs out with real-time subscriptions or export to S3.
In alarms, you have mastered what separates a useful alarm from a source of noise: the "M out of N"
combination with --datapoints-to-alarm 2 --evaluation-periods 3, the treatment of missing data —with
breaching for metrics whose absence is the incident, like PedidosConfirmados—, the
composite alarms that turn three text messages into one and that require removing the children's actions,
and the anomaly detection that knows 0 orders at 4 in the morning is normal and at 19:00 is
a catastrophe. You have built eight new alarms —which add to those of modules 2 and 4 up to
thirteen in total—, the composite mercadofresco-tienda-degradada and the anomaly detector on
PedidosConfirmados.
And you have answered the first question module 4 left open, the most uncomfortable of them all:
the notice really arrives. You have checked the subscriptions looking for the dreaded
PendingConfirmation, you have subscribed the email and the on-call phone, and you have run the drill with
set-alarm-state, which fires the real actions without touching the metric. And you know the chain includes
the phone: a mobile's "do not disturb" has silenced more alarms than any AWS
configuration error.
All of it comes together on the mercadofresco-produccion dashboard, ordered top to bottom —business,
components, detail—, with the CloudFront metrics in their us-east-1 widget, the annotation
lines that show how close you are to the threshold before it fires, and the log widget with the
latest errors. And with the canario-mercadofresco-compra canary walking the shop every five
minutes so that on a Sunday in the small hours, without a single real customer, somebody is still checking that
you can buy. All for about 44 USD a month, with the five ways of blowing up that figure
identified and retention set the same day each group is created.
One concrete dissatisfaction remains, however. The investigation of the eight-second order
worked, but it cost four queries, a timeline assembled by hand in a spreadsheet, and it depended
entirely on somebody having remembered to propagate the X-Peticion-Id header across all the
components. If tomorrow the bottleneck is inside the Lambda, or in the call to the payment
gateway, or in the database connection time, the whole job will have to be repeated and the place to look
guessed all over again.
There is a tool that does that work by itself, that draws the timeline in a graph and that says,
for a specific request, how long each leg of the journey took: AWS X-Ray. In lesson
05-02, "AWS X-Ray and distributed tracing", we will see what a trace, a segment and a
subsegment are, how the identifier is propagated with X-Amzn-Trace-Id, why not every request is traced
and how sampling is configured, how to instrument the shop with aws_xray_sdk and enable
tracing on mercadofresco-estado-pedido with a checkbox and a permission, how to read the service
map and search with filters such as annotation.pedido_id, and we will come back to this same 8.14-second
order to see it, this time, in a single graph.
AWS Course
Module 1: Introduction to AWS
- What Is AWS?
- Setting Up Your AWS Account
- AWS Global Infrastructure
- The AWS Management Console
- AWS CLI and SDKs
Module 2: Core AWS Services
Module 3: Networking and Content Delivery
Module 4: Security and Identity
- AWS Identity and Access Management (IAM)
- AWS Key Management Service (KMS)
- Secrets Manager and Parameter Store
- AWS Shield
- AWS WAF
Module 5: Monitoring and Management
Module 6: Databases
Module 7: Application Integration
- Amazon SQS
- Amazon SNS
- Amazon EventBridge
- AWS Step Functions
- Integration Patterns: Idempotency, Retries and Dead-Letter Queues
