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 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 evaluation flow is:
- CloudWatch Metric
- Alarm Evaluates
- Threshold Breached?
- Yes → ALARM State → SNS Topic → Email/Slack/PagerDuty
- No → OK State
Before creating alarms, you need an SNS topic for notifications.
Setting Up the Foundation
A Terraform configuration for CloudWatch monitoring typically starts with supporting resources.
- SNS topic for alarm actions
- CloudWatch log group with retention
- Log metric filter to extract custom metrics from logs
- Metric alarm resource or module
The terraform-aws-modules/terraform-aws-cloudwatch repository provides modules for these resources.
Basic Metric Alarm with Terraform
A native aws_cloudwatch_metric_alarm resource defines the core alarm behavior.
hcl
resource "aws_cloudwatch_metric_alarm" "instance_cpu" {
for_each = var.monitored_instances
alarm_name = "high-cpu-${each.key}"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = 300
statistic = "Average"
threshold = 80
alarm_description = "High CPU on ${each.key}"
dimensions = {
InstanceId = each.value
}
alarm_actions = [aws_sns_topic.warning_alerts.arn]
}
Key arguments from reference examples:
alarm_namealarm_descriptioncomparison_operatorevaluation_periodsthresholdperiodunitnamespacemetric_namestatisticdimensionsalarm_actions
Example from the metric-alarm module:
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"]
}
Log Group, Metric Filter, and Alarm Integration
Log-based metrics require a log group, a metric filter, and an alarm.
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"
}
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.
The complete log metric filter and alarm example creates Cloudwatch log metric based on pattern "ERROR" and connects it to Cloudwatch alarm which will push to SNS topic.
Required inputs for the example:
- terraform >= 1.0
- aws >= 5.81
Outputs include:
- cloudwatchloggroup_arn
- cloudwatchloggroup_name
- cloudwatchlogmetricfilterid
- cloudwatchmetricalarm_arn
- cloudwatchmetricalarm_id
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.
Scaling Alarms with for_each
This pattern creates identical CPU alarms for every instance in a list.
hcl
variable "monitored_instances" {
type = map(string)
default = {
"web-1" = "i-0abc123def456789a"
"web-2" = "i-0abc123def456789b"
"worker-1" = "i-0abc123def456789c"
}
}
Terraform's for_each handles this nicely and avoids repetition.
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 =
}
}
}
Module Parameters and Options
The terraform-aws-modules/cloudwatch/aws repository provides several submodules.
- log-metric-filter
- log-group
- log-stream
- metric-alarm
- metric-alarms
- query-definition
- composite-alarm
- log-account-policy
- cis-alarms
A representative parameter table for metric-alarm module:
| Parameter | Example Value | Description |
|---|---|---|
| alarm_name | my-application-logs-errors | Name of the alarm |
| alarm_description | Bad errors in my-application-logs | Description |
| comparison_operator | GreaterThanOrEqualToThreshold | Operator |
| evaluation_periods | 1 | Number of periods |
| threshold | 10 | Threshold value |
| period | 60 | Period in seconds |
| unit | Count | Unit of metric |
| namespace | MyApplication | Namespace |
| metric_name | ErrorCount | Metric name |
| statistic | Maximum | Statistic |
| alarm_actions | arn:aws:sns:... | SNS ARNs |
Metric Alarms by Multiple Dimensions
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"]
}
Composite Alarms
Composite alarms combine multiple alarms with boolean logic.
hcl
module "composite_alarm" {
source = "terraform-aws-modules/cloudwatch/aws//modules/composite-alarm"
version = "~> 4.0"
alarm_name = "composite-alarm"
alarm_description = "Example of a composite alarm"
alarm_actions = ["arn:aws:sns:eu-west-1:835367859852:my-sns-topic"]
ok_actions = ["arn:aws:sns:eu-west-1:835367859852:my-sns-topic"]
alarm_rule = join(" AND ", tolist([
"ALARM(metric-alarm-1)",
"ALARM(metric-alarm-2)"
]))
actions_suppressor = {
alarm = "suppressor"
extension_period = 20
wait_period = 10
}
}
Additional Modules
The repository also includes:
hcl
module "query_definition" {
source = "terraform-aws-modules/cloudwatch/aws//modules/query-definition"
version = "~> 4.0"
name = "my-query-definition"
log_group_names = ["my-log-group-name"]
query_string = <<EOF
fields @timestamp, @message
| sort @timestamp desc
| limit 25
EOF
}
hcl
module "log_account_policy" {
source = "terraform-aws-modules/cloudwatch/aws//modules/log-account-policy"
version = "~> 4.0"
log_account_policy_name = "account-data-protection"
log_account_policy_type = "DATA_PROTECTION_POLICY"
create_log_data_protection_policy = true
log_data_protection_policy_name = "redact-addresses"
data_identifiers =
}
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.
Production Best Practices
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.
For a broader look at monitoring strategy beyond just CloudWatch, check out AWS monitoring best practices.
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.
The module approach provides reusable, tested patterns for log metric filters, metric alarms, composite alarms, and query definitions, while native resources give fine-grained control for anomaly detection and multi-dimensional alarms.