Files
node/infra/aws/monitoring.tf
Kevin CodexandOpenClaude 7a8b6c4974 Aws ecr image (#55)
* feat(infra): optional private ECR repo for the node image

create_ecr_repo = true provisions an ECR repository in-account and
switches the instance to pull from it: the instance role gets
AmazonEC2ContainerRegistryReadOnly and user-data configures
amazon-ecr-credential-helper, so first-boot pulls and SSM upgrades
authenticate via the role (no expiring login tokens). README documents
the local build-and-push flow.

Motivation: ghcr.io/gitlawb/node is not anonymously pullable (403),
which left first boot unable to start the stack.

Co-Authored-By: OpenClaude <[email protected]>

* feat(infra): HTTPS via Caddy + production tier (RDS, alarms, S3 state)

- domain_name: Caddy sidecar terminates TLS on 80/443 with auto-renewed
  Let's Encrypt certs (persisted on the data volume); public_url
  defaults to https://<domain>; SG opens 80/443 — matches the Fly nodes
- use_rds: managed Postgres (db.t4g.micro, encrypted, 7-day PITR,
  deletion protection) replaces the in-container postgres; DB security
  group only admits the node instance
- alert_email: SNS email alerts wired to CloudWatch alarms — external
  HTTPS /health via Route53 health check, EC2 status checks, CPU,
  RDS free storage
- user-data grows the filesystem when the EBS volume is enlarged
- terraform state moved to S3 (versioned, KMS-encrypted, public-blocked
  bucket) with lockfile locking (requires terraform >= 1.10)

Co-Authored-By: OpenClaude <[email protected]>

---------

Co-authored-by: OpenClaude <[email protected]>
2026-06-18 22:05:50 +08:00

106 lines
4.0 KiB
Terraform

# Alerting (enabled when alert_email is set): SNS email + CloudWatch alarms
# on external API health (Route 53 health check), EC2 status, CPU, and RDS
# free storage. The email subscription must be confirmed once via the link
# SNS sends.
locals {
alarms_enabled = var.alert_email != ""
}
resource "aws_sns_topic" "alerts" {
count = local.alarms_enabled ? 1 : 0
name = "${var.name_prefix}-alerts"
tags = local.common_tags
}
resource "aws_sns_topic_subscription" "email" {
count = local.alarms_enabled ? 1 : 0
topic_arn = aws_sns_topic.alerts[0].arn
protocol = "email"
endpoint = var.alert_email
}
# External HTTPS /health probe — catches everything the EC2-level checks
# can't (container down, caddy broken, cert expired, DNS wrong).
resource "aws_route53_health_check" "api" {
count = local.alarms_enabled && var.domain_name != "" ? 1 : 0
fqdn = var.domain_name
port = 443
type = "HTTPS"
resource_path = "/health"
request_interval = 30
failure_threshold = 3
tags = merge(local.common_tags, { Name = "${var.name_prefix}-api" })
}
# Route 53 health check metrics are only published in us-east-1; this module
# already runs there by default.
resource "aws_cloudwatch_metric_alarm" "api_health" {
count = local.alarms_enabled && var.domain_name != "" ? 1 : 0
alarm_name = "${var.name_prefix}-api-health"
alarm_description = "https://${var.domain_name}/health is failing"
namespace = "AWS/Route53"
metric_name = "HealthCheckStatus"
dimensions = { HealthCheckId = aws_route53_health_check.api[0].id }
statistic = "Minimum"
period = 60
evaluation_periods = 2
threshold = 1
comparison_operator = "LessThanThreshold"
treat_missing_data = "breaching"
alarm_actions = [aws_sns_topic.alerts[0].arn]
ok_actions = [aws_sns_topic.alerts[0].arn]
tags = local.common_tags
}
resource "aws_cloudwatch_metric_alarm" "status_check" {
count = local.alarms_enabled ? 1 : 0
alarm_name = "${var.name_prefix}-status-check"
alarm_description = "EC2 status checks failing on the node instance"
namespace = "AWS/EC2"
metric_name = "StatusCheckFailed"
dimensions = { InstanceId = aws_instance.node.id }
statistic = "Maximum"
period = 60
evaluation_periods = 2
threshold = 1
comparison_operator = "GreaterThanOrEqualToThreshold"
alarm_actions = [aws_sns_topic.alerts[0].arn]
ok_actions = [aws_sns_topic.alerts[0].arn]
tags = local.common_tags
}
resource "aws_cloudwatch_metric_alarm" "cpu_high" {
count = local.alarms_enabled ? 1 : 0
alarm_name = "${var.name_prefix}-cpu-high"
alarm_description = "Node instance CPU above 80% for 15 minutes"
namespace = "AWS/EC2"
metric_name = "CPUUtilization"
dimensions = { InstanceId = aws_instance.node.id }
statistic = "Average"
period = 300
evaluation_periods = 3
threshold = 80
comparison_operator = "GreaterThanThreshold"
alarm_actions = [aws_sns_topic.alerts[0].arn]
ok_actions = [aws_sns_topic.alerts[0].arn]
tags = local.common_tags
}
resource "aws_cloudwatch_metric_alarm" "rds_storage_low" {
count = local.alarms_enabled && var.use_rds ? 1 : 0
alarm_name = "${var.name_prefix}-rds-storage-low"
alarm_description = "RDS free storage below 2 GB"
namespace = "AWS/RDS"
metric_name = "FreeStorageSpace"
dimensions = { DBInstanceIdentifier = aws_db_instance.node[0].identifier }
statistic = "Minimum"
period = 300
evaluation_periods = 2
threshold = 2147483648
comparison_operator = "LessThanThreshold"
alarm_actions = [aws_sns_topic.alerts[0].arn]
ok_actions = [aws_sns_topic.alerts[0].arn]
tags = local.common_tags
}