> For the complete documentation index, see [llms.txt](https://developer.harness.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developer.harness.io/ai-sre/ai-sre-for-administrators/set-up-alert-management/webhooks/integration-guides/cloud/aws-cloudwatch.md).

# AWS CloudWatch Integration Guide

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

### Before you begin <a href="#before-you-begin" id="before-you-begin"></a>

* **Harness webhook endpoint**: Create an AWS CloudWatch webhook in Harness AI SRE using the [AWS CloudWatch webhook template](/ai-sre/ai-sre-for-administrators/set-up-alert-management/webhooks/templates/cloud/aws-cloudwatch.md).
* **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](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html) to understand alarm configuration.
* **SNS HTTPS subscriptions**: Go to [SNS HTTPS Subscriptions](https://docs.aws.amazon.com/sns/latest/dg/sns-http-https-endpoint-as-subscriber.html) for subscription setup details.

***

### Architecture overview <a href="#architecture-overview" id="architecture-overview"></a>

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 <a href="#create-sns-topic" id="create-sns-topic"></a>

#### Navigate to SNS <a href="#navigate-to-sns" id="navigate-to-sns"></a>

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 <a href="#configure-topic" id="configure-topic"></a>

{% tabs %}
{% tab title="Standard 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)
  {% endtab %}

{% tab title="FIFO topic" %}
FIFO topics are not recommended for CloudWatch alarms as they require message group IDs.

Use **Standard** topic instead.
{% endtab %}
{% endtabs %}

#### Save topic <a href="#save-topic" id="save-topic"></a>

Click **Create topic** and note the **Topic ARN**.

***

### Create HTTPS subscription <a href="#create-https-subscription" id="create-https-subscription"></a>

#### Add subscription to topic <a href="#add-subscription-to-topic" id="add-subscription-to-topic"></a>

Start a new subscription from the topic details:

1. In the SNS topic details, click **Create subscription**

#### Configure subscription <a href="#configure-subscription" id="configure-subscription"></a>

{% tabs %}
{% tab title="Basic configuration" %}

* **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
  {% endtab %}

{% tab title="With filter policy" %}

* **Protocol**: HTTPS
* **Endpoint**: Your Harness webhook URL
* **Subscription filter policy**: JSON filter to process only specific alarms

```json
{
  "AlarmName": [
    {"prefix": "Production-"}
  ],
  "NewStateValue": [
    "ALARM"
  ]
}
```

This filters to only production alarms in ALARM state.
{% endtab %}
{% endtabs %}

#### Confirm subscription <a href="#confirm-subscription" id="confirm-subscription"></a>

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 <a href="#create-cloudwatch-alarm" id="create-cloudwatch-alarm"></a>

#### Navigate to CloudWatch <a href="#navigate-to-cloudwatch" id="navigate-to-cloudwatch"></a>

Open the alarm creation page:

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

#### Select metric <a href="#select-metric" id="select-metric"></a>

{% tabs %}
{% tab title="EC2 CPU usage" %}

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`
  {% endtab %}

{% tab title="RDS connections" %}

1. Click **Select metric**
2. Select **RDS**, then select **Per-Database Metrics**
3. Select **DatabaseConnections** for your DB instance
4. Click **Select metric**

**Conditions**:

* **Threshold type**: Static
* **Whenever DatabaseConnections is...**: Greater than `100`
* **Datapoints to alarm**: `3 out of 5`
  {% endtab %}

{% tab title="Lambda errors" %}

1. Click **Select metric**
2. Select **Lambda**, then select **Per-Function Metrics**
3. Select **Errors** for your function
4. Click **Select metric**

**Conditions**:

* **Threshold type**: Static
* **Whenever Errors is...**: Greater than `10`
* **Datapoints to alarm**: `1 out of 1`
  {% endtab %}
  {% endtabs %}

#### Configure actions <a href="#configure-actions" id="configure-actions"></a>

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 <a href="#set-alarm-details" id="set-alarm-details"></a>

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 <a href="#create-alarm" id="create-alarm"></a>

Click **Create alarm**.

***

### Configure field mapping in Harness <a href="#configure-field-mapping-in-harness" id="configure-field-mapping-in-harness"></a>

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

#### CloudWatch alarm SNS payload structure <a href="#cloudwatch-alarm-sns-payload-structure" id="cloudwatch-alarm-sns-payload-structure"></a>

```json
{
  "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 <a href="#basic-field-mapping-example" id="basic-field-mapping-example"></a>

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

```yaml
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 <a href="#advanced-field-mapping-with-cel" id="advanced-field-mapping-with-cel"></a>

Parse the nested JSON message:

```cel
// 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 <a href="#test-the-integration" id="test-the-integration"></a>

#### Test with CloudWatch console <a href="#test-with-cloudwatch-console" id="test-with-cloudwatch-console"></a>

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 <a href="#test-sns-subscription" id="test-sns-subscription"></a>

```bash
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 <a href="#verify-in-harness" id="verify-in-harness"></a>

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 <a href="#available-cloudwatch-alarm-fields" id="available-cloudwatch-alarm-fields"></a>

Fields in the parsed `Message` JSON:

| Field                        | Description                 | Example                                  |
| ---------------------------- | --------------------------- | ---------------------------------------- |
| `AlarmName`                  | Alarm name                  | `Production-EC2-HighCPU`                 |
| `AlarmDescription`           | Alarm description           | `EC2 instance CPU usage above 80%`       |
| `AWSAccountId`               | AWS account ID              | `123456789012`                           |
| `NewStateValue`              | New alarm state             | `ALARM`, `OK`, `INSUFFICIENT_DATA`       |
| `OldStateValue`              | Previous alarm state        | `OK`, `ALARM`                            |
| `NewStateReason`             | State change reason         | `Threshold Crossed: ...`                 |
| `StateChangeTime`            | When state changed          | `2025-07-01T10:10:00.000Z`               |
| `Region`                     | AWS region                  | `US East (N. Virginia)`                  |
| `AlarmArn`                   | Alarm ARN                   | `arn:aws:cloudwatch:...`                 |
| `Trigger.MetricName`         | Metric name                 | `CPUUtilization`                         |
| `Trigger.Namespace`          | Metric namespace            | `AWS/EC2`                                |
| `Trigger.Statistic`          | Statistic type              | `AVERAGE`, `SUM`, `MAXIMUM`              |
| `Trigger.Dimensions`         | Metric dimensions           | `[{name: "InstanceId", value: "i-..."}]` |
| `Trigger.Period`             | Evaluation period (seconds) | `300`                                    |
| `Trigger.EvaluationPeriods`  | Number of periods           | `2`                                      |
| `Trigger.Threshold`          | Alarm threshold             | `80.0`                                   |
| `Trigger.ComparisonOperator` | Comparison operator         | `GreaterThanThreshold`                   |

***

### Advanced configuration <a href="#advanced-configuration" id="advanced-configuration"></a>

#### Filter by namespace <a href="#filter-by-namespace" id="filter-by-namespace"></a>

Only process specific AWS service alarms:

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

#### Route by region <a href="#route-by-region" id="route-by-region"></a>

Tag and route by AWS region:

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

#### Extract EC2 instance details <a href="#extract-ec2-instance-details" id="extract-ec2-instance-details"></a>

For EC2 alarms, extract instance ID:

```cel
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 <a href="#create-composite-alert-messages" id="create-composite-alert-messages"></a>

Combine multiple alarm details:

```cel
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 <a href="#troubleshooting" id="troubleshooting"></a>

<details>

<summary>SNS subscription is not confirming for the Harness AI SRE webhook</summary>

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.

</details>

<details>

<summary>CloudWatch SNS Message field is not parsing in Harness AI SRE</summary>

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.

</details>

<details>

<summary>CloudWatch alarms are not triggering the SNS webhook</summary>

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).

</details>

<details>

<summary>SNS message signature verification is failing</summary>

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.

</details>

***

### Example: complete integration <a href="#example-complete-integration" id="example-complete-integration"></a>

#### AWS SNS topic <a href="#aws-sns-topic" id="aws-sns-topic"></a>

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 <a href="#cloudwatch-alarm" id="cloudwatch-alarm"></a>

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 <a href="#harness-webhook-field-mapping-example" id="harness-webhook-field-mapping-example"></a>

```yaml
# Parse nested Message JSON <a href="#parse-nested-message-json" id="parse-nested-message-json"></a>
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 <a href="#next-steps" id="next-steps"></a>

* [Route alerts](/ai-sre/ai-sre-for-administrators/set-up-alert-management/alert-rules/overview.md): Route and deduplicate CloudWatch alarms.
* [Use CEL in webhooks](/ai-sre/ai-sre-for-administrators/set-up-alert-management/webhooks/use-cel-webhooks.md): Add JSON parsing and filtering.
* [AI agent](/ai-sre/ai-sre-for-incident-responders/use-ai-agents/ai-agent.md): Enable automated alarm investigation.
* [AWS CloudWatch template](/ai-sre/ai-sre-for-administrators/set-up-alert-management/webhooks/templates/cloud/aws-cloudwatch.md): Use the pre-configured template.

***

### Related documentation <a href="#related-documentation" id="related-documentation"></a>

#### AWS official documentation <a href="#aws-official-documentation" id="aws-official-documentation"></a>

* [CloudWatch alarms](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html): Complete guide to CloudWatch alarm configuration and SNS integration.
* [SNS HTTPS subscriptions](https://docs.aws.amazon.com/sns/latest/dg/sns-http-https-endpoint-as-subscriber.html): HTTPS subscription setup and subscription confirmation process.
* [CloudWatch and EventBridge](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch-and-eventbridge.html): CloudWatch alarm message format and payload structure.
* [SNS message formats](https://docs.aws.amazon.com/sns/latest/dg/sns-message-and-json-formats.html): SNS notification wrapper and Message field structure.
