1007 lines
28 KiB
HCL
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
|
|
} |