CloudWatch Alarm on EC2 CPU with SNS Alert
Scenario
Your EC2-based web server has been running in production and sometime last week it hit 100% CPU for several minutes, causing request timeouts. Nobody noticed until users complained. You need to set up a CloudWatch Alarm that fires an SNS notification when CPU utilisation exceeds 80% for two consecutive 5-minute periods.
- Create an aws_cloudwatch_metric_alarm on the CPUUtilization metric for a specific EC2 instance.
- Set the threshold to 80%, with 2 evaluation periods of 5 minutes each.
- Configure both alarm and OK actions pointing to an SNS topic ARN.
- Add a CloudWatch Log Metric Filter that counts log lines containing ERROR and alarms when more than 5 errors occur in 5 minutes.
- Output the alarm ARNs.
Additional Context
CloudWatch alarms use dimensions to filter metrics. For EC2, the required dimension is InstanceId. Without it, the alarm watches all EC2 instances in the region. Always pin alarms to specific resources using dimensions.
The treat_missing_data argument controls what the alarm does when no data points arrive: missing (stays in current state), breaching (treats absence as threshold breach), notBreaching, or ignore. For a web server, missing is usually appropriate.
versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}variables.tf
variable "aws_region" { type = string; default = "us-east-1" }
variable "instance_id" { type = string; description = "EC2 instance ID to monitor." }
variable "sns_topic_arn" { type = string; description = "SNS topic ARN to notify on alarm." }
variable "log_group_name" { type = string; default = "/app/web-server" }
variable "environment" { type = string; default = "prod" }main.tf
# main.tf
provider "aws" {
region = var.aws_region
}
# CPU Utilisation alarm for the EC2 instance.
resource "aws_cloudwatch_metric_alarm" "high_cpu" {
alarm_name = "high-cpu-${var.instance_id}"
alarm_description = "EC2 instance CPU above 80% for 10 minutes"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 2 # must breach for 2 consecutive periods
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = 300 # 5 minutes in seconds
statistic = "Average"
threshold = 80
# Dimension pins the alarm to one specific instance.
dimensions = {
InstanceId = var.instance_id
}
# Actions to run when the alarm fires (goes into ALARM state).
alarm_actions = [var.sns_topic_arn]
# Actions to run when the alarm recovers (goes back to OK state).
ok_actions = [var.sns_topic_arn]
# Behaviour when no data arrives (e.g., instance stopped).
treat_missing_data = "missing"
tags = { Environment = var.environment, ManagedBy = "terraform" }
}
# CloudWatch Log Group — where your application writes structured logs.
resource "aws_cloudwatch_log_group" "app" {
name = var.log_group_name
retention_in_days = 14 # keep 14 days of logs
tags = { Environment = var.environment, ManagedBy = "terraform" }
}
# Metric Filter — scans logs for ERROR lines and emits a custom metric.
resource "aws_cloudwatch_log_metric_filter" "error_count" {
name = "error-count"
log_group_name = aws_cloudwatch_log_group.app.name
pattern = "ERROR" # matches any log line containing "ERROR"
metric_transformation {
name = "ApplicationErrorCount"
namespace = "Custom/AppMetrics"
value = "1" # each matched line increments the counter by 1
}
}
# Alarm on the custom Application Error metric.
resource "aws_cloudwatch_metric_alarm" "error_rate" {
alarm_name = "high-error-rate-${var.environment}"
alarm_description = "More than 5 application errors in 5 minutes"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "ApplicationErrorCount"
namespace = "Custom/AppMetrics"
period = 300
statistic = "Sum"
threshold = 5
alarm_actions = [var.sns_topic_arn]
treat_missing_data = "notBreaching" # no errors = alarm is OK
tags = { Environment = var.environment, ManagedBy = "terraform" }
}outputs.tf
output "cpu_alarm_arn" { value = aws_cloudwatch_metric_alarm.high_cpu.arn }
output "error_alarm_arn" { value = aws_cloudwatch_metric_alarm.error_rate.arn }
output "log_group_name" { value = aws_cloudwatch_log_group.app.name }Workflow Commands
terraform init
terraform apply \
-var="instance_id=i-0abc1234567890def" \
-var="sns_topic_arn=arn:aws:sns:us-east-1:123456789012:deployment-events-dev"
# View alarm state
aws cloudwatch describe-alarms \
--alarm-names "high-cpu-i-0abc1234567890def"
# Push a test log entry triggering the metric filter
aws logs put-log-events \
--log-group-name /app/web-server \
--log-stream-name test-stream \
--log-events timestamp=$(date +%s%3N),message="ERROR: database connection failed"
terraform destroy -var="instance_id=..." -var="sns_topic_arn=..."Apply complete! Resources: 4 added, 0 changed, 0 destroyed. Outputs: cpu_alarm_arn = "arn:aws:cloudwatch:us-east-1:123456789012:alarm:high-cpu-i-0abc..." error_alarm_arn = "arn:aws:cloudwatch:us-east-1:123456789012:alarm:high-error-rate-prod" log_group_name = "/app/web-server"

