mirror of
https://github.com/Twigpine/node.git
synced 2026-11-13 06:51:46 -10:00
* 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]>
106 lines
4.0 KiB
Terraform
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
|
|
}
|