LAB09_AWS_CloudWatch Alarm on EC2 CPU with SNS Alert

CloudWatch Alarm on EC2 CPU with SNS Alert

☁ AWS ⭐ Beginner CloudWatch Alarms Metric Filters SNS

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.

Your Objectives
  • 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=..."
✓ Expected Output After Apply
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"