CloudWatch alarms are the backbone of AWS monitoring. They watch your metrics, fire when things go sideways, and trigger notifications or automated responses. Managing them through the AWS console works fine for a handful of alarms, but once you're dealing with dozens of services across multiple environments, Terraform becomes essential. This guide covers everything from basic metric alarms to composite alarms, SNS integrations, and patterns you'll actually use in production.
What CloudWatch Metric Alarms Do
A basic CloudWatch metric alarm watches a single metric over a time period you define. When the metric crosses a threshold for a specified number of evaluation periods, the alarm transitions from OK to ALARM state. You can attach actions to state transitions - most commonly sending an SNS notification that hits a Slack channel or PagerDuty.
The flow is:
- CloudWatch Metric
- Alarm Evaluates
- Threshold Breached?
- Yes → ALARM State → SNS Topic → Email/Slack/PagerDuty
- No → OK State
Focus on metrics that indicate user-facing impact. CPU at 80% might not matter if response times are fine. Use appropriate evaluation periods. Single-period alarms are noisy. Most alarms should require 2-3 consecutive breaches before firing. Set treatmissingdata intentionally. The default is missing, which can cause confusing behavior. Use notBreaching for error count metrics and breaching for health check metrics. Tag your alarms consistently. When you've got 200 alarms, you'll thank yourself for adding environment, team, and service tags.
Terraform Resource Foundation
The aws_cloudwatch_metric_alarm resource provides a CloudWatch Metric Alarm resource.
Example Usage
hcl
resource "aws_cloudwatch_metric_alarm" "foobar" {
alarm_name = "terraform-test-foobar5"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = "2"
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = "120"
statistic = "Average"
threshold = "80"
alarm_description = "This metric monitor ec2 cpu utilization"
insufficient_data_actions = []
}
Example in Conjunction with Scaling Policies
```hcl
resource "awsautoscalingpolicy" "bat" {
name = "foobar3-terraform-test"
scalingadjustment = 4
adjustmenttype = "ChangeInCapacity"
cooldown = 300
autoscalinggroupname = "${awsautoscalinggroup.bar.name}"
}
resource "awscloudwatchmetricalarm" "bat" {
alarmname = "terraform-test-foobar5"
comparisonoperator = "GreaterThanOrEqualToThreshold"
evaluationperiods = "2"
metricname = "CPUUtilization"
namespace = "AWS/EC2"
period = "120"
statistic = "Average"
threshold = "80"
dimensions {
AutoScalingGroupName = "${awsautoscalinggroup.bar.name}"
}
alarmdescription = "This metric monitor ec2 cpu utilization"
alarmactions = ["${awsautoscaling_policy.bat.arn}"]
}
```
Argument Reference
See related part of AWS Docs for details about valid values. The following arguments are supported:
| Argument | Required | Description |
|---|---|---|
| alarm_name | Yes | The descriptive name for the alarm |
| comparison_operator | ||
| evaluation_periods | ||
| metric_name | ||
| namespace | ||
| period | ||
| statistic | ||
| threshold | ||
| alarm_description | ||
| alarm_actions | ||
| insufficientdataactions |
SNS Foundation and Notification Actions
Before creating alarms, you need an SNS topic for notifications.
A typical alarm with SNS action is configured as:
hcl
module "metric_alarm" {
source = "terraform-aws-modules/cloudwatch/aws//modules/metric-alarm"
version = "~> 3.0"
alarm_name = "my-application-logs-errors"
alarm_description = "Bad errors in my-application-logs"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 1
threshold = 10
period = 60
unit = "Count"
namespace = "MyApplication"
metric_name = "ErrorCount"
statistic = "Maximum"
alarm_actions = ["arn:aws:sns:eu-west-1:835367859852:my-sns-queue"]
}
This pattern creates identical CPU alarms for every instance in a list using Terraform's for_each:
```hcl
variable "monitored_instances" {
type = map(string)
default = {
"web-1" = "i-0abc123def456789a"
"web-2" = "i-0abc123def456789b"
"worker-1" = "i-0abc123def456789c"
}
}
resource "awscloudwatchmetricalarm" "instancecpu" {
foreach = var.monitoredinstances
alarmname = "high-cpu-${each.key}"
comparisonoperator = "GreaterThanThreshold"
evaluationperiods = 2
metricname = "CPUUtilization"
namespace = "AWS/EC2"
period = 300
statistic = "Average"
threshold = 80
alarmdescription = "High CPU on ${each.key}"
dimensions = {
InstanceId = each.value
}
alarmactions = [awssnstopic.warning_alerts.arn]
}
```
Reusable Modules for CloudWatch
Terraform module which creates Cloudwatch resources on AWS.
Module examples:
hcl
module "log_metric_filter" {
source = "terraform-aws-modules/cloudwatch/aws//modules/log-metric-filter"
version = "~> 3.0"
log_group_name = "my-application-logs"
name = "error-metric"
pattern = "ERROR"
metric_transformation_namespace = "MyApplication"
metric_transformation_name = "ErrorCount"
}
Read Filter and Pattern Syntax for explanation of pattern.
hcl
module "log_group" {
source = "terraform-aws-modules/cloudwatch/aws//modules/log-group"
version = "~> 3.0"
name = "my-app"
retention_in_days = 120
}
hcl
module "log_stream" {
source = "terraform-aws-modules/cloudwatch/aws//modules/log-stream"
version = "~> 3.0"
name = "stream1"
log_group_name = "my-app"
}
This submodule is useful when you need to create very similar alarms where only dimensions are different, eg, multiple AWS Lambda functions, but the rest of arguments are the same.
hcl
module "metric_alarms" {
source = "terraform-aws-modules/cloudwatch/aws//modules/metric-alarms-by-multiple-dimensions"
version = "~> 3.0"
alarm_name = "lambda-duration-"
alarm_description = "Lambda duration is too high"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 1
threshold = 10
period = 60
unit = "Milliseconds"
namespace = "AWS/Lambda"
metric_name = "Duration"
statistic = "Maximum"
dimensions = {
"lambda1" = {
FunctionName = "index"
},
"lambda2" = {
FunctionName = "signup"
},
}
alarm_actions = ["arn:aws:sns:eu-west-1:835367859852:my-sns-queue"]
}
Check out list of all AWS services that publish CloudWatch metrics for detailed information about each supported service.
hcl
module "cis_alarms" {
source = "terraform-aws-modules/cloudwatch/aws//modules/cis-alarms"
version = "~> 3.0"
log_group_name = "my-cloudtrail-logs"
alarm_actions = ["arn:aws:sns:eu-west-1:835367859852:my-sns-queue"]
}
AWS CloudTrail normally publishes logs into AWS CloudWatch Logs
Log Metric Filter and Alarm Pattern
Configuration in this directory creates Cloudwatch log metric based on pattern "ERROR" and connects it to Cloudwatch alarm which will push to SNS topic.
To run this example you need to execute:
bash
$ terraform init
$ terraform plan
$ terraform apply
Note that this example may create resources which cost money. Run terraform destroy when you don't need these resources.
| Name | Version |
|---|---|
| terraform | >= 1.0 |
| aws | >= 5.81 |
No providers.
| Name | Source | Version |
|---|---|---|
| alarm | ../../modules/metric-alarm | n/a |
| awssnstopic | ../fixtures/awssnstopic | n/a |
| log_group | ../../modules/log-group | n/a |
| logmetricfilter | ../../modules/log-metric-filter | n/a |
No resources.
No inputs.
| Name | Description |
|---|---|
| cloudwatchloggroup_arn | ARN of Cloudwatch log group |
| cloudwatchloggroup_name | Name of Cloudwatch log group |
| cloudwatchlogmetricfilterid | The name of the metric filter |
| cloudwatchmetricalarm_arn | The ARN of the Cloudwatch metric alarm |
| cloudwatchmetricalarm_id | The ID of the Cloudwatch metric alarm |
Anomaly Detection Alarms
Standard threshold-based alarms work for metrics with predictable ranges like CPU percentage, but for metrics like request counts that vary by time of day, anomaly detection is more useful.
This alarm uses CloudWatch anomaly detection to flag unusual API request patterns:
hcl
resource "aws_cloudwatch_metric_alarm" "api_anomaly" {
alarm_name = "api-request-anomaly"
comparison_operator = "GreaterThanUpperThreshold"
evaluation_periods = 2
threshold_metric_id = "ad1"
alarm_description = "API request count is anomalously high"
metric_query {
id = "ad1"
expression = "ANOMALY_DETECTION_BAND(m1, 2)"
label = "Request Count (Expected)"
return_data = true
}
metric_query {
id = "m1"
metric {
metric_name = "RequestCount"
namespace =
Production Patterns
- Use for_each to avoid repetition for similar alarms across resources.
- Create identical CPU alarms for every instance in a list with for_each.
- Use modules for log metric filter, log group, log stream, metric alarm, metric alarms by multiple dimensions, and cis alarms.
- Attach alarm actions to SNS topics for notifications and to autoscaling policies for automated responses.
- Set evaluation_periods to 2 or more to reduce noise.
- Tag alarms consistently for environment, team, and service.
Conclusion
CloudWatch alarms in Terraform give you version-controlled, repeatable monitoring that scales with your infrastructure. Start with the critical metrics for each service type, use for_each to avoid repetition, and lean on composite alarms to reduce alert fatigue. The configurations in this guide should cover the most common scenarios, but don't hesitate to customize thresholds based on your application's actual behavior patterns. Setting up the foundation with an SNS topic for notifications, building metric alarms with appropriate evaluation periods and comparison operators, and reusing modules for log metric filters and multi-dimensional alarms provides a production-ready monitoring baseline that remains maintainable as services grow.
Sources
- https://oneuptime.com/blog/post/2026-02-12-create-cloudwatch-alarms-terraform/view
- https://github.com/terraform-aws-modules/terraform-aws-cloudwatch
- https://www.koding.com/docs/terraform/providers/aws/r/cloudwatchmetricalarm.html/
- https://github.com/terraform-aws-modules/terraform-aws-cloudwatch/blob/master/examples/complete-log-metric-filter-and-alarm/README.md