Skip to main content

AWS CloudWatch Integration Guide

Last updated on

Configure AWS CloudWatch alarms to send webhook notifications to Harness AI SRE using Amazon SNS with HTTPS subscription.

Before you begin

  • Harness webhook endpoint: Create an AWS CloudWatch webhook in Harness AI SRE using the AWS CloudWatch webhook template.
  • AWS permissions: Access to create SNS topics, CloudWatch alarms, and manage subscriptions.
  • Webhook URL: Copy the webhook URL from your Harness webhook configuration.
  • CloudWatch alarms documentation: Go to CloudWatch Alarms to understand alarm configuration.
  • SNS HTTPS subscriptions: Go to SNS HTTPS Subscriptions for subscription setup details.

Architecture overview

CloudWatch alarms to SNS topic to HTTPS subscription to Harness webhook.

CloudWatch does not send webhooks directly. Use Amazon SNS as an intermediary:

  1. CloudWatch alarm state changes
  2. Alarm publishes to SNS topic
  3. SNS sends HTTPS POST to Harness webhook
  4. Harness processes the alert

Create SNS topic

Open the SNS topic creation page:

  1. Open AWS Console and go to Simple Notification Service (SNS)
  2. Click Topics, then click Create topic

Configure topic

  • Type: Standard
  • Name: harness-ai-sre-alerts
  • Display name: Harness AI SRE Alerts
  • Encryption: (Optional) Enable encryption at rest
  • Access policy: Default (allow publishers)

Save topic

Click Create topic and note the Topic ARN.


Create HTTPS subscription

Add subscription to topic

Start a new subscription from the topic details:

  1. In the SNS topic details, click Create subscription

Configure subscription

  • Protocol: HTTPS
  • Endpoint: Your Harness webhook URL
    https://<your-harness-instance>/gateway/ai-sre/api/webhooks/<webhook-id>
  • Enable raw message delivery: Uncheck (keep message wrapper)
  • Redrive policy: (Optional) Configure DLQ for failed deliveries

Confirm subscription

Complete the subscription confirmation handshake:

  1. Click Create subscription
  2. SNS sends a confirmation request to your Harness webhook
  3. Harness must respond with subscription confirmation

Note: Configure your Harness webhook to automatically confirm SNS subscriptions by responding to SubscribeURL in the payload.


Create CloudWatch alarm

Open the alarm creation page:

  1. Open AWS Console and go to CloudWatch
  2. Click Alarms, then click Create alarm

Select metric

  1. Click Select metric
  2. Select EC2, then select Per-Instance Metrics
  3. Select CPUUtilization for your instance
  4. Click Select metric

Conditions:

  • Threshold type: Static
  • Whenever CPUUtilization is...: Greater than 80
  • Datapoints to alarm: 2 out of 2

Configure actions

In the Configure actions step:

  • Notification:
    • Alarm state trigger: In alarm
    • Send notification to: Select your SNS topic harness-ai-sre-alerts
  • Additional actions: (Optional) Configure Auto Scaling or EC2 actions

Set alarm details

Name and describe the alarm:

  • Alarm name: Production-EC2-HighCPU
  • Alarm description: EC2 instance CPU usage above 80%
  • Treat missing data as: Choose appropriate option (default: missing)

Create alarm

Click Create alarm.


Configure field mapping in Harness

In your Harness webhook configuration, map CloudWatch/SNS payload fields to alert properties.

CloudWatch alarm SNS payload structure

{
"Type": "Notification",
"MessageId": "abc-123-def-456",
"TopicArn": "arn:aws:sns:us-east-1:123456789012:harness-ai-sre-alerts",
"Subject": "ALARM: \"Production-EC2-HighCPU\" in US East (N. Virginia)",
"Message": "{\"AlarmName\":\"Production-EC2-HighCPU\",\"AlarmDescription\":\"EC2 instance CPU usage above 80%\",\"AWSAccountId\":\"123456789012\",\"AlarmConfigurationUpdatedTimestamp\":\"2025-07-01T10:00:00.000Z\",\"NewStateValue\":\"ALARM\",\"NewStateReason\":\"Threshold Crossed: 2 datapoints [85.0 (01/07/25 10:00:00), 90.0 (01/07/25 10:05:00)] were greater than the threshold (80.0).\",\"StateChangeTime\":\"2025-07-01T10:10:00.000Z\",\"Region\":\"US East (N. Virginia)\",\"AlarmArn\":\"arn:aws:cloudwatch:us-east-1:123456789012:alarm:Production-EC2-HighCPU\",\"OldStateValue\":\"OK\",\"OKActions\":[],\"AlarmActions\":[\"arn:aws:sns:us-east-1:123456789012:harness-ai-sre-alerts\"],\"InsufficientDataActions\":[],\"Trigger\":{\"MetricName\":\"CPUUtilization\",\"Namespace\":\"AWS/EC2\",\"StatisticType\":\"Statistic\",\"Statistic\":\"AVERAGE\",\"Unit\":null,\"Dimensions\":[{\"value\":\"i-1234567890abcdef0\",\"name\":\"InstanceId\"}],\"Period\":300,\"EvaluationPeriods\":2,\"ComparisonOperator\":\"GreaterThanThreshold\",\"Threshold\":80.0,\"TreatMissingData\":\"missing\",\"EvaluateLowSampleCountPercentile\":\"\"}}",
"Timestamp": "2025-07-01T10:10:00.000Z",
"SignatureVersion": "1",
"Signature": "...",
"SigningCertURL": "...",
"UnsubscribeURL": "..."
}

Note: The Message field contains a JSON string that must be parsed.

Basic field mapping example

CloudWatch alarm data is nested in the Message field as a JSON string. Use CEL to parse:

title: "{{webhook.Subject}}"
message: "{{webhook.Message}}"
severity: "ALARM"
source: "cloudwatch"
link: "https://console.aws.amazon.com/cloudwatch"
tags:
- "source:cloudwatch"
- "topic:{{webhook.TopicArn}}"

Advanced field mapping with CEL

Parse the nested JSON message:

// Parse the Message JSON string
parsed_message: webhook.Message.parseJson()

// Extract alarm details
title: parsed_message.AlarmName + " - " + parsed_message.NewStateValue
message: parsed_message.AlarmDescription + "\n\n" +
"State: " + parsed_message.OldStateValue + " → " + parsed_message.NewStateValue + "\n" +
"Reason: " + parsed_message.NewStateReason + "\n" +
"Region: " + parsed_message.Region + "\n" +
"Account: " + parsed_message.AWSAccountId

// Map CloudWatch state to Harness severity
severity: parsed_message.NewStateValue == "ALARM" ? "critical" :
parsed_message.NewStateValue == "INSUFFICIENT_DATA" ? "medium" : "info"

source: "cloudwatch"
link: "https://console.aws.amazon.com/cloudwatch/home?region=" +
parsed_message.Region.toLowerCase().replace(" ", "-").replace("(", "").replace(")", "") +
"#alarmsV2:alarm/" + parsed_message.AlarmName

// Extract metric dimensions as tags
tags: ["source:cloudwatch",
"alarm:" + parsed_message.AlarmName,
"state:" + parsed_message.NewStateValue,
"region:" + parsed_message.Region,
"account:" + parsed_message.AWSAccountId,
"metric:" + parsed_message.Trigger.MetricName,
"namespace:" + parsed_message.Trigger.Namespace] +
(has(parsed_message.Trigger.Dimensions)
? parsed_message.Trigger.Dimensions.map(d, d.name + ":" + d.value)
: [])

// Filter: only ALARM states
filter: parsed_message.NewStateValue == "ALARM"

custom_fields: {
"alarm_arn": parsed_message.AlarmArn,
"account_id": parsed_message.AWSAccountId,
"region": parsed_message.Region,
"metric_name": parsed_message.Trigger.MetricName,
"threshold": string(parsed_message.Trigger.Threshold),
"comparison_operator": parsed_message.Trigger.ComparisonOperator
}

Test the integration

Test with CloudWatch console

Manually set the alarm state to trigger a test notification:

  1. Go to CloudWatch, then select Alarms
  2. Select your alarm
  3. Click Actions, then click Set alarm state
  4. Select In alarm
  5. Click Confirm

This manually triggers the alarm to test the integration.

Test SNS subscription

aws sns publish \
--topic-arn arn:aws:sns:us-east-1:123456789012:harness-ai-sre-alerts \
--message '{"AlarmName":"Test","NewStateValue":"ALARM","NewStateReason":"Test"}' \
--subject "TEST: Alarm"

Verify in Harness

Confirm the alarm arrived and parsed correctly:

  1. Navigate to Alerts in Harness AI SRE
  2. Check that the alarm appears
  3. Verify field mapping parsed the message correctly

Available CloudWatch alarm fields

Fields in the parsed Message JSON:

FieldDescriptionExample
AlarmNameAlarm nameProduction-EC2-HighCPU
AlarmDescriptionAlarm descriptionEC2 instance CPU usage above 80%
AWSAccountIdAWS account ID123456789012
NewStateValueNew alarm stateALARM, OK, INSUFFICIENT_DATA
OldStateValuePrevious alarm stateOK, ALARM
NewStateReasonState change reasonThreshold Crossed: ...
StateChangeTimeWhen state changed2025-07-01T10:10:00.000Z
RegionAWS regionUS East (N. Virginia)
AlarmArnAlarm ARNarn:aws:cloudwatch:...
Trigger.MetricNameMetric nameCPUUtilization
Trigger.NamespaceMetric namespaceAWS/EC2
Trigger.StatisticStatistic typeAVERAGE, SUM, MAXIMUM
Trigger.DimensionsMetric dimensions[{name: "InstanceId", value: "i-..."}]
Trigger.PeriodEvaluation period (seconds)300
Trigger.EvaluationPeriodsNumber of periods2
Trigger.ThresholdAlarm threshold80.0
Trigger.ComparisonOperatorComparison operatorGreaterThanThreshold

Advanced configuration

Filter by namespace

Only process specific AWS service alarms:

filter: parsed_message.NewStateValue == "ALARM" &&
parsed_message.Trigger.Namespace in ["AWS/EC2", "AWS/RDS", "AWS/Lambda"]

Route by region

Tag and route by AWS region:

tags: ["source:cloudwatch",
"region:" + parsed_message.Region,
"environment:" + (parsed_message.Region.contains("us-east") ? "production" : "staging")]

Extract EC2 instance details

For EC2 alarms, extract instance ID:

instance_id: has(parsed_message.Trigger.Dimensions)
? parsed_message.Trigger.Dimensions.filter(d, d.name == "InstanceId")[0].value
: ""

message: parsed_message.AlarmDescription + "\n\n" +
"Instance: " + instance_id + "\n" +
"Metric: " + parsed_message.Trigger.MetricName

Create composite alert messages

Combine multiple alarm details:

message: "**CloudWatch Alarm**: " + parsed_message.AlarmName + "\n\n" +
"**Description**: " + parsed_message.AlarmDescription + "\n" +
"**State Change**: " + parsed_message.OldStateValue + " → " + parsed_message.NewStateValue + "\n" +
"**Reason**: " + parsed_message.NewStateReason + "\n\n" +
"**Metric Details**:\n" +
"- Namespace: " + parsed_message.Trigger.Namespace + "\n" +
"- Metric: " + parsed_message.Trigger.MetricName + "\n" +
"- Statistic: " + parsed_message.Trigger.Statistic + "\n" +
"- Threshold: " + string(parsed_message.Trigger.Threshold) + "\n" +
"- Comparison: " + parsed_message.Trigger.ComparisonOperator + "\n\n" +
"**AWS Details**:\n" +
"- Account: " + parsed_message.AWSAccountId + "\n" +
"- Region: " + parsed_message.Region + "\n" +
"- Time: " + parsed_message.StateChangeTime

Troubleshooting

SNS subscription is not confirming for the Harness AI SRE webhook

Check the Harness webhook logs for SubscribeURL in the payload, configure Harness to automatically confirm subscriptions, or manually confirm the subscription in the AWS Console under SNS Subscriptions.

CloudWatch SNS Message field is not parsing in Harness AI SRE

Check the raw webhook payload in the Harness logs, verify the Message is a JSON string parsed with webhook.Message.parseJson(), and handle parsing errors before accessing nested fields.

CloudWatch alarms are not triggering the SNS webhook

Edit the alarm in CloudWatch, add a notification action with your SNS topic ARN, and ensure the alarm state matches the trigger (In alarm, OK, or Insufficient data).

SNS message signature verification is failing

SNS signs all messages with certificates. Verify the signature using the AWS SDK or the certificate URL, or accept messages without verification within a VPC or private network.


Example: complete integration

AWS SNS topic

This example uses the following SNS topic and subscription:

  • Name: harness-ai-sre-alerts
  • Type: Standard
  • Subscription:
    • Protocol: HTTPS
    • Endpoint: https://app.harness.io/gateway/ai-sre/api/webhooks/wh_abc123
    • Status: Confirmed

CloudWatch alarm

This example uses the following alarm configuration:

  • Name: Production-RDS-HighConnections
  • Metric: DatabaseConnections in the AWS/RDS namespace
  • Condition: Greater than 100 for 3 out of 5 datapoints
  • Actions: Notify harness-ai-sre-alerts when in ALARM state

Harness webhook field mapping example

# Parse nested Message JSON
parsed_message: webhook.Message.parseJson()

title: |
parsed_message.AlarmName + " - " + parsed_message.NewStateValue

message: |
**CloudWatch Alarm**: {{parsed_message.AlarmName}}

**Description**: {{parsed_message.AlarmDescription}}
**State**: {{parsed_message.OldStateValue}}{{parsed_message.NewStateValue}}
**Reason**: {{parsed_message.NewStateReason}}

**Metric**: {{parsed_message.Trigger.MetricName}} ({{parsed_message.Trigger.Namespace}})
**Threshold**: {{parsed_message.Trigger.ComparisonOperator}} {{parsed_message.Trigger.Threshold}}
**Statistic**: {{parsed_message.Trigger.Statistic}} over {{parsed_message.Trigger.Period}}s

**AWS Account**: {{parsed_message.AWSAccountId}}
**Region**: {{parsed_message.Region}}
**Time**: {{parsed_message.StateChangeTime}}

**View in AWS Console**: https://console.aws.amazon.com/cloudwatch/home#alarmsV2:alarm/{{parsed_message.AlarmName}}

severity: |
parsed_message.NewStateValue == "ALARM" ? "critical" :
parsed_message.NewStateValue == "INSUFFICIENT_DATA" ? "medium" : "info"

source: "cloudwatch"

link: |
"https://console.aws.amazon.com/cloudwatch/home#alarmsV2:alarm/" + parsed_message.AlarmName

tags:
- "source:cloudwatch"
- "alarm:{{parsed_message.AlarmName}}"
- "state:{{parsed_message.NewStateValue}}"
- "account:{{parsed_message.AWSAccountId}}"
- "region:{{parsed_message.Region}}"
- "namespace:{{parsed_message.Trigger.Namespace}}"
- "metric:{{parsed_message.Trigger.MetricName}}"

filter: |
webhook.Type == "Notification" &&
parsed_message.NewStateValue == "ALARM"

custom_fields:
alarm_arn: "{{parsed_message.AlarmArn}}"
aws_account: "{{parsed_message.AWSAccountId}}"
aws_region: "{{parsed_message.Region}}"
metric_namespace: "{{parsed_message.Trigger.Namespace}}"
metric_name: "{{parsed_message.Trigger.MetricName}}"
threshold: "{{parsed_message.Trigger.Threshold}}"

Next steps


AWS official documentation