AWS is the one estate where you cannot get away with a single ingest path. CloudWatch alarms come through SNS; everything else worth paging on — ECS task failures, GuardDuty findings, AWS Health events, Auto Scaling actions — does not come through CloudWatch at all. Wire only the first and you will miss the events that matter most.

Two ingest paths, and why you need both

graph LR subgraph P1["Path 1 — alarms"] CW["CloudWatch
alarm"] --> SNS["SNS topic"] end subgraph P2["Path 2 — events"] ECS["ECS task
state change"] --> EB["EventBridge
rule"] GD["GuardDuty
finding"] --> EB HL["AWS Health
event"] --> EB AS["Auto Scaling"] --> EB end SNS --> W1["/api/webhooks/cloudwatch"] EB --> W2["/api/webhooks/eventbridge"] EB -.->|"or via SNS"| W2 W1 & W2 --> N["One normalized incident"] style N fill:#f4f6ff,stroke:#a78bfa,stroke-width:2px

Both endpoints accept the SNS envelope or a bare event, because EventBridge can deliver either directly or through a topic and you should not have to care which you chose. The alarm is unwrapped from the SNS Message string automatically.

The subscription handshake is deliberately not automatic

When you subscribe a topic, SNS first posts a SubscriptionConfirmation carrying a SubscribeURL. We do not fetch it. Fetching a URL that arrived in a request body is how a service becomes someone else's HTTP client — anyone who can reach the endpoint could make the API issue a GET wherever they liked. The confirmation is reported instead, and a person opens it once they recognise the topic ARN.

What arrives, and what it becomes

AWS conceptBecomesNote
AlarmNamealert nameAlso mined for severity — an alarm named *-critical or sev1-* is read as critical.
NewStateValueseverity / resolvedOK resolves the incident; INSUFFICIENT_DATA is a warning, not an alarm.
Trigger.DimensionslabelsThis is what your match rules bind to. LoadBalancer, FunctionName, ClusterName.
AlarmArnexternal id + regionThe region is parsed out of the ARN when Region is absent.
GuardDuty severityseverityNumeric 0–10, mapped: ≥7 critical, ≥4 high, else warning.
detail-type + sourcealert nameEventBridge events have no severity of their own, so it comes from what the event is.

The dimensions row is the one to get right. CloudWatch's service identity lives in the dimensions rather than in a field called "service", so the normalizer picks the dimension that usually identifies a service — ServiceName, then FunctionName, DBInstanceIdentifier, LoadBalancer, ClusterName, QueueName — and falls back to the namespace. If your naming differs, a match rule on the raw dimension label still works.

Reading AWS back

Six step types, and credentials come from boto3's own chain — task role, instance role, environment — never from a step's parameters. That constraint matters more than it sounds: runbook editing is a much lower bar than infrastructure access, so if a step could carry credentials, anyone who could edit a runbook could point the agent at another account.

StepWhat it answers
cloudwatch.metricOne metric over a window, with dimensions and a statistic. The AWS equivalent of prometheus.range.
cloudwatch.alarmsWhat else is in ALARM right now — often the fastest way to see that this is one symptom of something wider.
cloudwatch.logsRecent matching lines from a log group, with a filter pattern.
ecs.servicesDesired versus running, deployments in flight with their rollout state, and the last five service events. The ECS answer to k8s.pods.
elb.target_healthWhich targets are healthy and why the unhealthy ones are not. Often a better first signal than a scrape.
cloud.healthIs AWS having a problem of its own? One step, and occasionally the entire answer.

A caveat on AWS Health

The Health API needs a Business or Enterprise support plan. On accounts without one the step reports exactly that and is skipped — it does not silently return "nothing wrong", which would be a dangerous thing to conclude from an API you cannot call.

A worked ECS incident

The same error-rate shape as the Kubernetes version, translated. Note that only the platform steps change; the reasoning steps are identical, which is the point of keeping the runbook model platform-agnostic.

sequenceDiagram autonumber participant CW as CloudWatch participant IR as InsightResolve participant AWS as AWS APIs CW->>IR: ALARM · prod-api-5xx-critical · ALB target 5xx > 5 IR->>IR: matched on dimension LoadBalancer=app/prod-api IR->>AWS: cloudwatch.metric — 5xx rate, last 15m IR->>AWS: ecs.services — desired vs running, rollout state IR->>AWS: elb.target_health — which targets, and why IR->>AWS: cloudwatch.logs — /aws/ecs/prod-api, pattern ERROR IR->>AWS: cloud.health — anything open on AWS's side? AWS-->>IR: 2 of 6 targets unhealthy · deployment IN_PROGRESS · nothing on AWS Health IR->>IR: classify, then verify against the evidence Note over IR: "a deployment is in progress and two targets
are failing health checks — established.
That the deployment caused it — assumed."

That last note is the same discipline as everywhere else: a rollout in progress and failing targets in the same window is correlation. It caps at 0.75 until something establishes the link — which, on AWS, the deployment's own rolloutStateReason often does, because ECS will say "tasks failed to start" in as many words.

Getting it wired

  1. One SNS topic for alarms, subscribed to the webhook with your token in the query string. Confirm the subscription by hand the first time.
  2. One EventBridge rule with a pattern covering the sources you care about — start with aws.ecs and aws.health, add aws.guardduty if security wants the findings triaged.
  3. A read-only IAM role for the task: cloudwatch:GetMetricData, cloudwatch:DescribeAlarms, logs:FilterLogEvents, ecs:DescribeServices, elasticloadbalancing:DescribeTargetHealth, and health:DescribeEvents if you have the support plan. Nothing that writes.
  4. Check the Integrations page before writing runbooks. It probes each system and tells you what is reachable, so a step that will come back unavailable is visible before you depend on it.

Worth stating plainly

The AWS connectors are built against the documented APIs and tested end to end through the product, but they have not been run against a live production account of yours or anyone else's. The read steps fail safe — an unreachable or misconfigured system reports that rather than guessing. The normalizers are the part worth checking against one real payload from your estate, and we would rather you did that in week one than discover a field mismatch during an incident.

Next in this series

InsightResolve on Azure — the easiest of the clouds to wire in, because Azure actually standardised its alert payload.

Keep reading