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") waf_bypass_ipv4_cidrs = distinct(concat( [var.vpc_cidr], var.waf_bypass_ipv4_cidrs )) 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 NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY = var.documenso_license_key }, 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", "NEXT_PRIVATE_DOCUMENSO_LICENSE_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_ip_set" "trusted_ipv4" { name = "${local.name_prefix}-trusted-ipv4" description = "IPv4 CIDRs that bypass the Documenso WAF rules" scope = "REGIONAL" ip_address_version = "IPV4" addresses = local.waf_bypass_ipv4_cidrs tags = local.common_tags } resource "aws_wafv2_web_acl" "this" { name = "${local.name_prefix}-web-acl" description = "WAF protection for Documenso" scope = "REGIONAL" default_action { allow {} } rule { name = "AllowTrustedIpv4" priority = 0 action { allow {} } statement { ip_set_reference_statement { arn = aws_wafv2_ip_set.trusted_ipv4.arn } } visibility_config { cloudwatch_metrics_enabled = true metric_name = "AllowTrustedIpv4" sampled_requests_enabled = true } } rule { name = "AWSManagedRulesCommonRuleSet" priority = 1 override_action { none {} } statement { managed_rule_group_statement { name = "AWSManagedRulesCommonRuleSet" vendor_name = "AWS" rule_action_override { name = "SizeRestrictions_BODY" action_to_use { count {} } } } } 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 }