Files
bodyshop/documenso/terraform/main.tf
2026-03-26 14:57:09 -07:00

1007 lines
28 KiB
HCL

terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
}
}
provider "aws" {
region = var.aws_region
}
locals {
name_prefix = lower(replace(var.project_name, "_", "-"))
azs = slice(data.aws_availability_zones.available.names, 0, 2)
public_subnet_cidrs = [for index in range(length(local.azs)) : cidrsubnet(var.vpc_cidr, 8, index)]
db_subnet_cidrs = [for index in range(length(local.azs)) : cidrsubnet(var.vpc_cidr, 8, index + 10)]
ses_domain = coalesce(var.ses_identity_domain, var.hosted_zone_name)
smtp_host = "email-smtp.${var.aws_region}.amazonaws.com"
s3_bucket_name = coalesce(var.upload_bucket_name, "${local.name_prefix}-${data.aws_caller_identity.current.account_id}-${var.aws_region}")
app_secret_name = coalesce(var.app_secret_name, "${local.name_prefix}/${replace(var.domain_name, ".", "-")}/app")
common_tags = merge(var.tags, {
Application = var.project_name
ManagedBy = "Terraform"
})
app_secret_values = merge(
{
NEXTAUTH_SECRET = random_password.nextauth_secret.result
NEXT_PRIVATE_ENCRYPTION_KEY = random_password.encryption_key_primary.result
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY = random_password.encryption_key_secondary.result
NEXT_PRIVATE_DATABASE_URL = "postgresql://${var.db_username}:${random_password.db_password.result}@${aws_db_instance.postgres.address}:5432/${var.db_name}?schema=public"
NEXT_PRIVATE_DIRECT_DATABASE_URL = "postgresql://${var.db_username}:${random_password.db_password.result}@${aws_db_instance.postgres.address}:5432/${var.db_name}?schema=public"
NEXT_PRIVATE_SMTP_USERNAME = var.smtp_username
NEXT_PRIVATE_SMTP_PASSWORD = var.smtp_password
NEXT_PRIVATE_SMTP_FROM_NAME = var.smtp_from_name
NEXT_PRIVATE_SMTP_FROM_ADDRESS = var.smtp_from_address
NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS = var.allowed_signup_domains
NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID = aws_iam_access_key.documenso_upload.id
NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY = aws_iam_access_key.documenso_upload.secret
},
trimspace(var.signing_certificate_base64) != "" ? {
NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS = var.signing_certificate_base64
} : {},
trimspace(var.signing_certificate_passphrase) != "" ? {
NEXT_PRIVATE_SIGNING_PASSPHRASE = var.signing_certificate_passphrase
} : {}
)
app_secret_env = concat(
[
for secret_name in [
"NEXTAUTH_SECRET",
"NEXT_PRIVATE_ENCRYPTION_KEY",
"NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY",
"NEXT_PRIVATE_DATABASE_URL",
"NEXT_PRIVATE_DIRECT_DATABASE_URL",
"NEXT_PRIVATE_SMTP_USERNAME",
"NEXT_PRIVATE_SMTP_PASSWORD",
"NEXT_PRIVATE_SMTP_FROM_NAME",
"NEXT_PRIVATE_SMTP_FROM_ADDRESS",
"NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS",
"NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID",
"NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY"
] : {
name = secret_name
valueFrom = "${aws_secretsmanager_secret.app.arn}:${secret_name}::"
}
],
trimspace(var.signing_certificate_base64) != "" ? [
{
name = "NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS"
valueFrom = "${aws_secretsmanager_secret.app.arn}:NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS::"
}
] : [],
trimspace(var.signing_certificate_passphrase) != "" ? [
{
name = "NEXT_PRIVATE_SIGNING_PASSPHRASE"
valueFrom = "${aws_secretsmanager_secret.app.arn}:NEXT_PRIVATE_SIGNING_PASSPHRASE::"
}
] : []
)
}
data "aws_caller_identity" "current" {}
data "aws_availability_zones" "available" {
state = "available"
}
data "aws_route53_zone" "primary" {
name = var.hosted_zone_name
private_zone = false
}
data "aws_rds_engine_version" "postgres" {
engine = "postgres"
version = var.postgres_major_version
latest = true
}
resource "random_password" "db_password" {
length = 32
special = false
}
resource "random_password" "nextauth_secret" {
length = 64
special = false
}
resource "random_password" "encryption_key_primary" {
length = 64
special = false
}
resource "random_password" "encryption_key_secondary" {
length = 64
special = false
}
resource "random_id" "final_snapshot" {
byte_length = 4
}
resource "aws_vpc" "this" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-vpc"
})
}
resource "aws_internet_gateway" "this" {
vpc_id = aws_vpc.this.id
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-igw"
})
}
resource "aws_subnet" "public" {
count = length(local.azs)
vpc_id = aws_vpc.this.id
cidr_block = local.public_subnet_cidrs[count.index]
availability_zone = local.azs[count.index]
map_public_ip_on_launch = true
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-public-${count.index + 1}"
Tier = "public"
})
}
resource "aws_subnet" "database" {
count = length(local.azs)
vpc_id = aws_vpc.this.id
cidr_block = local.db_subnet_cidrs[count.index]
availability_zone = local.azs[count.index]
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-database-${count.index + 1}"
Tier = "database"
})
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.this.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.this.id
}
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-public-rt"
})
}
resource "aws_route_table_association" "public" {
count = length(aws_subnet.public)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "database_public" {
count = var.db_publicly_accessible ? length(aws_subnet.database) : 0
subnet_id = aws_subnet.database[count.index].id
route_table_id = aws_route_table.public.id
}
resource "aws_security_group" "alb" {
name = "${local.name_prefix}-alb-sg"
description = "Public ingress to the Documenso load balancer"
vpc_id = aws_vpc.this.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-alb-sg"
})
}
resource "aws_security_group" "ecs" {
name = "${local.name_prefix}-ecs-sg"
description = "Restrict Documenso container access to the ALB"
vpc_id = aws_vpc.this.id
ingress {
from_port = var.app_port
to_port = var.app_port
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-ecs-sg"
})
}
resource "aws_security_group" "db" {
name = "${local.name_prefix}-db-sg"
description = "Allow PostgreSQL access only from Documenso tasks"
vpc_id = aws_vpc.this.id
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.ecs.id]
}
dynamic "ingress" {
for_each = var.db_allowed_cidrs
content {
from_port = 5432
to_port = 5432
protocol = "tcp"
cidr_blocks = [ingress.value]
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-db-sg"
})
}
resource "aws_db_subnet_group" "this" {
name = "${local.name_prefix}-db-subnets"
subnet_ids = aws_subnet.database[*].id
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-db-subnets"
})
}
resource "aws_db_parameter_group" "postgres" {
name = "${local.name_prefix}-postgres${var.postgres_major_version}"
family = "postgres${var.postgres_major_version}"
tags = local.common_tags
}
resource "aws_db_instance" "postgres" {
identifier = "${local.name_prefix}-postgres"
engine = "postgres"
engine_version = data.aws_rds_engine_version.postgres.version_actual
instance_class = var.db_instance_class
allocated_storage = var.db_allocated_storage
max_allocated_storage = var.db_max_allocated_storage
storage_type = "gp3"
storage_encrypted = true
db_name = var.db_name
username = var.db_username
password = random_password.db_password.result
port = 5432
backup_retention_period = var.db_backup_retention_days
multi_az = var.db_multi_az
deletion_protection = var.db_deletion_protection
skip_final_snapshot = !var.db_final_snapshot_on_destroy
final_snapshot_identifier = var.db_final_snapshot_on_destroy ? "${local.name_prefix}-final-${random_id.final_snapshot.hex}" : null
auto_minor_version_upgrade = true
publicly_accessible = var.db_publicly_accessible
apply_immediately = false
db_subnet_group_name = aws_db_subnet_group.this.name
vpc_security_group_ids = [aws_security_group.db.id]
parameter_group_name = aws_db_parameter_group.postgres.name
performance_insights_enabled = false
enabled_cloudwatch_logs_exports = ["postgresql", "upgrade"]
depends_on = [aws_route_table_association.database_public]
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-postgres"
})
}
resource "aws_cloudwatch_log_group" "documenso" {
name = "/ecs/${local.name_prefix}"
retention_in_days = 30
tags = local.common_tags
}
resource "aws_secretsmanager_secret" "app" {
name = local.app_secret_name
recovery_window_in_days = 7
tags = local.common_tags
}
resource "aws_secretsmanager_secret_version" "app" {
secret_id = aws_secretsmanager_secret.app.id
secret_string = jsonencode(local.app_secret_values)
}
resource "aws_iam_role" "ecs_task_execution" {
name = "${local.name_prefix}-ecs-execution"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}]
})
tags = local.common_tags
}
resource "aws_iam_role_policy_attachment" "ecs_task_execution" {
role = aws_iam_role.ecs_task_execution.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
resource "aws_iam_role_policy" "ecs_task_execution_secrets" {
name = "${local.name_prefix}-ecs-secrets"
role = aws_iam_role.ecs_task_execution.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"secretsmanager:GetSecretValue"
]
Resource = aws_secretsmanager_secret.app.arn
}
]
})
}
resource "aws_s3_bucket" "uploads" {
bucket = local.s3_bucket_name
lifecycle {
prevent_destroy = false #Remove this to tear down the bucket.
}
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-uploads"
})
}
resource "aws_s3_bucket_versioning" "uploads" {
bucket = aws_s3_bucket.uploads.id
versioning_configuration {
status = var.s3_versioning_enabled ? "Enabled" : "Suspended"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "uploads" {
bucket = aws_s3_bucket.uploads.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
bucket_key_enabled = true
}
}
resource "aws_s3_bucket_public_access_block" "uploads" {
bucket = aws_s3_bucket.uploads.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_ownership_controls" "uploads" {
bucket = aws_s3_bucket.uploads.id
rule {
object_ownership = "BucketOwnerEnforced"
}
}
resource "aws_s3_bucket_cors_configuration" "uploads" {
bucket = aws_s3_bucket.uploads.id
cors_rule {
allowed_headers = ["*"]
allowed_methods = ["GET", "PUT", "POST"]
allowed_origins = ["https://${var.domain_name}"]
expose_headers = ["ETag"]
max_age_seconds = 3000
}
}
resource "aws_s3_bucket_lifecycle_configuration" "uploads" {
bucket = aws_s3_bucket.uploads.id
rule {
id = "abort-incomplete-multipart-uploads"
status = "Enabled"
abort_incomplete_multipart_upload {
days_after_initiation = 7
}
}
}
resource "aws_iam_user" "documenso_upload" {
name = "${local.name_prefix}-upload"
tags = local.common_tags
}
resource "aws_iam_access_key" "documenso_upload" {
user = aws_iam_user.documenso_upload.name
}
resource "aws_iam_user_policy" "documenso_upload" {
name = "${local.name_prefix}-upload-s3"
user = aws_iam_user.documenso_upload.name
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:ListBucket"
]
Resource = aws_s3_bucket.uploads.arn
},
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
]
Resource = "${aws_s3_bucket.uploads.arn}/*"
}
]
})
}
resource "aws_iam_role" "ecs_task" {
name = "${local.name_prefix}-ecs-task"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}]
})
tags = local.common_tags
}
resource "aws_ecs_cluster" "this" {
name = "${local.name_prefix}-cluster"
setting {
name = "containerInsights"
value = "enabled"
}
tags = local.common_tags
}
resource "aws_acm_certificate" "this" {
domain_name = var.domain_name
validation_method = "DNS"
lifecycle {
create_before_destroy = true
}
tags = local.common_tags
}
resource "aws_route53_record" "certificate_validation" {
for_each = {
for dvo in aws_acm_certificate.this.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
zone_id = data.aws_route53_zone.primary.zone_id
name = each.value.name
type = each.value.type
ttl = 60
records = [each.value.record]
}
resource "aws_acm_certificate_validation" "this" {
certificate_arn = aws_acm_certificate.this.arn
validation_record_fqdns = [for record in aws_route53_record.certificate_validation : record.fqdn]
}
resource "aws_lb" "this" {
name = substr("${local.name_prefix}-alb", 0, 32)
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = aws_subnet.public[*].id
idle_timeout = 60
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-alb"
})
}
resource "aws_lb_target_group" "documenso" {
name = substr("${local.name_prefix}-tg", 0, 32)
port = var.app_port
protocol = "HTTP"
target_type = "ip"
vpc_id = aws_vpc.this.id
health_check {
enabled = true
healthy_threshold = 2
unhealthy_threshold = 3
interval = 30
timeout = 5
path = "/"
matcher = "200-399"
}
tags = local.common_tags
}
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.this.arn
port = 80
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.this.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = aws_acm_certificate.this.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.documenso.arn
}
depends_on = [aws_acm_certificate_validation.this]
}
resource "aws_wafv2_web_acl" "this" {
name = "${local.name_prefix}-web-acl"
description = "WAF protection for Documenso"
scope = "REGIONAL"
default_action {
allow {}
}
rule {
name = "AWSManagedRulesCommonRuleSet"
priority = 1
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesCommonRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "CommonRuleSet"
sampled_requests_enabled = true
}
}
rule {
name = "RateLimitPerIp"
priority = 2
action {
block {}
}
statement {
rate_based_statement {
limit = var.waf_rate_limit
aggregate_key_type = "IP"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "RateLimitPerIp"
sampled_requests_enabled = true
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "${replace(local.name_prefix, "-", "")}-web-acl"
sampled_requests_enabled = true
}
tags = local.common_tags
}
resource "aws_wafv2_web_acl_association" "alb" {
resource_arn = aws_lb.this.arn
web_acl_arn = aws_wafv2_web_acl.this.arn
}
resource "aws_route53_record" "app" {
zone_id = data.aws_route53_zone.primary.zone_id
name = var.domain_name
type = "A"
alias {
name = aws_lb.this.dns_name
zone_id = aws_lb.this.zone_id
evaluate_target_health = true
}
}
resource "aws_ses_domain_identity" "this" {
count = var.manage_ses_resources ? 1 : 0
domain = local.ses_domain
}
resource "aws_route53_record" "ses_verification" {
count = var.manage_ses_resources ? 1 : 0
zone_id = data.aws_route53_zone.primary.zone_id
name = "_amazonses.${aws_ses_domain_identity.this[0].domain}"
type = "TXT"
ttl = 600
records = [aws_ses_domain_identity.this[0].verification_token]
allow_overwrite = true
}
resource "aws_ses_domain_dkim" "this" {
count = var.manage_ses_resources ? 1 : 0
domain = aws_ses_domain_identity.this[0].domain
}
resource "aws_route53_record" "ses_dkim" {
count = var.manage_ses_resources ? 3 : 0
zone_id = data.aws_route53_zone.primary.zone_id
name = "${aws_ses_domain_dkim.this[0].dkim_tokens[count.index]}._domainkey.${aws_ses_domain_identity.this[0].domain}"
type = "CNAME"
ttl = 600
records = ["${aws_ses_domain_dkim.this[0].dkim_tokens[count.index]}.dkim.amazonses.com"]
allow_overwrite = true
}
resource "aws_ecs_task_definition" "documenso" {
family = "${local.name_prefix}-task"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = tostring(var.fargate_cpu)
memory = tostring(var.fargate_memory)
execution_role_arn = aws_iam_role.ecs_task_execution.arn
task_role_arn = aws_iam_role.ecs_task.arn
depends_on = [aws_secretsmanager_secret_version.app]
container_definitions = jsonencode([
{
name = "documenso"
image = var.documenso_image
essential = true
portMappings = [
{
containerPort = var.app_port
hostPort = var.app_port
protocol = "tcp"
}
]
environment = [
{ name = "PORT", value = tostring(var.app_port) },
{ name = "NEXT_PUBLIC_WEBAPP_URL", value = "https://${var.domain_name}" },
{ name = "NEXT_PRIVATE_INTERNAL_WEBAPP_URL", value = "http://127.0.0.1:${var.app_port}" },
{ name = "NEXT_PUBLIC_UPLOAD_TRANSPORT", value = "s3" },
{ name = "NEXT_PRIVATE_UPLOAD_BUCKET", value = aws_s3_bucket.uploads.bucket },
{ name = "NEXT_PRIVATE_UPLOAD_REGION", value = var.aws_region },
{ name = "NEXT_PRIVATE_SMTP_TRANSPORT", value = "smtp-auth" },
{ name = "NEXT_PRIVATE_SMTP_HOST", value = local.smtp_host },
{ name = "NEXT_PRIVATE_SMTP_PORT", value = tostring(var.smtp_port) },
{ name = "NEXT_PRIVATE_SMTP_SECURE", value = var.smtp_secure ? "true" : "false" },
{ name = "NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS", value = var.smtp_unsafe_ignore_tls ? "true" : "false" },
{ name = "NEXT_PUBLIC_DISABLE_SIGNUP", value = var.disable_signup ? "true" : "false" },
{ name = "NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT", value = tostring(var.document_size_upload_limit_mb) }
]
secrets = local.app_secret_env
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = aws_cloudwatch_log_group.documenso.name
awslogs-region = var.aws_region
awslogs-stream-prefix = "documenso"
}
}
}
])
tags = local.common_tags
}
resource "aws_ecs_service" "documenso" {
name = "${local.name_prefix}-service"
cluster = aws_ecs_cluster.this.id
task_definition = aws_ecs_task_definition.documenso.arn
desired_count = var.desired_count
launch_type = "FARGATE"
health_check_grace_period_seconds = 60
deployment_maximum_percent = 200
deployment_minimum_healthy_percent = 100
enable_execute_command = true
deployment_circuit_breaker {
enable = true
rollback = true
}
network_configuration {
subnets = aws_subnet.public[*].id
security_groups = [aws_security_group.ecs.id]
assign_public_ip = true
}
load_balancer {
target_group_arn = aws_lb_target_group.documenso.arn
container_name = "documenso"
container_port = var.app_port
}
depends_on = [aws_lb_listener.https]
tags = local.common_tags
}
resource "aws_appautoscaling_target" "ecs" {
max_capacity = var.max_count
min_capacity = var.min_count
resource_id = "service/${aws_ecs_cluster.this.name}/${aws_ecs_service.documenso.name}"
scalable_dimension = "ecs:service:DesiredCount"
service_namespace = "ecs"
}
resource "aws_appautoscaling_policy" "cpu" {
name = "${local.name_prefix}-cpu-scaling"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.ecs.resource_id
scalable_dimension = aws_appautoscaling_target.ecs.scalable_dimension
service_namespace = aws_appautoscaling_target.ecs.service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
target_value = var.cpu_target_utilization
scale_in_cooldown = 120
scale_out_cooldown = 60
}
}
resource "aws_appautoscaling_policy" "memory" {
name = "${local.name_prefix}-memory-scaling"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.ecs.resource_id
scalable_dimension = aws_appautoscaling_target.ecs.scalable_dimension
service_namespace = aws_appautoscaling_target.ecs.service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageMemoryUtilization"
}
target_value = var.memory_target_utilization
scale_in_cooldown = 120
scale_out_cooldown = 60
}
}
resource "aws_cloudwatch_metric_alarm" "alb_5xx" {
alarm_name = "${local.name_prefix}-alb-5xx"
alarm_description = "ALB is returning elevated 5xx responses"
namespace = "AWS/ApplicationELB"
metric_name = "HTTPCode_ELB_5XX_Count"
statistic = "Sum"
period = 300
evaluation_periods = 1
threshold = var.alb_5xx_alarm_threshold
comparison_operator = "GreaterThanOrEqualToThreshold"
treat_missing_data = "notBreaching"
alarm_actions = var.alarm_actions
ok_actions = var.alarm_actions
dimensions = {
LoadBalancer = aws_lb.this.arn_suffix
}
tags = local.common_tags
}
resource "aws_cloudwatch_metric_alarm" "alb_unhealthy_hosts" {
alarm_name = "${local.name_prefix}-alb-unhealthy-hosts"
alarm_description = "ALB target group has unhealthy hosts"
namespace = "AWS/ApplicationELB"
metric_name = "UnHealthyHostCount"
statistic = "Average"
period = 60
evaluation_periods = 2
threshold = 1
comparison_operator = "GreaterThanOrEqualToThreshold"
treat_missing_data = "notBreaching"
alarm_actions = var.alarm_actions
ok_actions = var.alarm_actions
dimensions = {
LoadBalancer = aws_lb.this.arn_suffix
TargetGroup = aws_lb_target_group.documenso.arn_suffix
}
tags = local.common_tags
}
resource "aws_cloudwatch_metric_alarm" "ecs_cpu_high" {
alarm_name = "${local.name_prefix}-ecs-cpu-high"
alarm_description = "Documenso ECS service CPU is consistently high"
namespace = "AWS/ECS"
metric_name = "CPUUtilization"
statistic = "Average"
period = 300
evaluation_periods = 2
threshold = var.ecs_cpu_alarm_threshold
comparison_operator = "GreaterThanOrEqualToThreshold"
treat_missing_data = "notBreaching"
alarm_actions = var.alarm_actions
ok_actions = var.alarm_actions
dimensions = {
ClusterName = aws_ecs_cluster.this.name
ServiceName = aws_ecs_service.documenso.name
}
tags = local.common_tags
}
resource "aws_cloudwatch_metric_alarm" "ecs_memory_high" {
alarm_name = "${local.name_prefix}-ecs-memory-high"
alarm_description = "Documenso ECS service memory is consistently high"
namespace = "AWS/ECS"
metric_name = "MemoryUtilization"
statistic = "Average"
period = 300
evaluation_periods = 2
threshold = var.ecs_memory_alarm_threshold
comparison_operator = "GreaterThanOrEqualToThreshold"
treat_missing_data = "notBreaching"
alarm_actions = var.alarm_actions
ok_actions = var.alarm_actions
dimensions = {
ClusterName = aws_ecs_cluster.this.name
ServiceName = aws_ecs_service.documenso.name
}
tags = local.common_tags
}
resource "aws_cloudwatch_metric_alarm" "rds_cpu_high" {
alarm_name = "${local.name_prefix}-rds-cpu-high"
alarm_description = "RDS CPU utilization is high"
namespace = "AWS/RDS"
metric_name = "CPUUtilization"
statistic = "Average"
period = 300
evaluation_periods = 2
threshold = var.rds_cpu_alarm_threshold
comparison_operator = "GreaterThanOrEqualToThreshold"
treat_missing_data = "notBreaching"
alarm_actions = var.alarm_actions
ok_actions = var.alarm_actions
dimensions = {
DBInstanceIdentifier = aws_db_instance.postgres.id
}
tags = local.common_tags
}
resource "aws_cloudwatch_metric_alarm" "rds_free_storage_low" {
alarm_name = "${local.name_prefix}-rds-free-storage-low"
alarm_description = "RDS free storage is running low"
namespace = "AWS/RDS"
metric_name = "FreeStorageSpace"
statistic = "Average"
period = 300
evaluation_periods = 1
threshold = var.rds_free_storage_alarm_threshold_bytes
comparison_operator = "LessThanOrEqualToThreshold"
treat_missing_data = "notBreaching"
alarm_actions = var.alarm_actions
ok_actions = var.alarm_actions
dimensions = {
DBInstanceIdentifier = aws_db_instance.postgres.id
}
tags = local.common_tags
}