Skip to main content

Configure AWS CloudWatch to Send Webhooks

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 → SNS Topic → HTTPS Subscription → 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

  1. Open AWS Console and go to Simple Notification Service (SNS)
  2. Click TopicsCreate 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

  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

  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

  1. Open AWS Console and go to CloudWatch
  2. Click AlarmsCreate alarm

Select metric

  1. Click Select metric
  2. Choose EC2Per-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

  • 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

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

  1. Go to CloudWatchAlarms
  2. Select your alarm
  3. Click ActionsSet alarm state
  4. Choose 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

  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 not confirming

Cause: Harness webhook not responding to subscription confirmation request.

Solution:

  • Check Harness webhook logs for SubscribeURL in payload
  • Configure Harness to automatically confirm subscriptions:
// Detect SNS subscription confirmation
filter: webhook.Type != "SubscriptionConfirmation"

// Or handle it separately by fetching SubscribeURL
  • Manually confirm via AWS Console: SNS → Subscriptions → Confirm

Message field not parsing

Cause: Message is double-encoded or in unexpected format.

Solution:

  • Check raw webhook payload in Harness logs
  • Verify Message is a JSON string: webhook.Message.parseJson()
  • Handle parsing errors:
parsed_message: webhook.Message.parseJson()

title: has(parsed_message.AlarmName) ? parsed_message.AlarmName : webhook.Subject

Alarms not triggering

Cause: CloudWatch alarm not configured with SNS topic.

Solution:

  • Edit alarm in CloudWatch
  • Go to Actions section
  • Add notification action with your SNS topic ARN
  • Ensure alarm state matches trigger (In alarm, OK, Insufficient data)

Signature verification failing

Cause: SNS message signature verification issues.

Solution:

  • SNS signs all messages with certificates
  • Verify signature using AWS SDK or certificate URL
  • Or accept messages without verification if within VPC/private network

Example: Complete integration

AWS SNS topic

  • 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

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

Harness webhook field mapping

# 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


Further reading

AWS Official Documentation