Initial terraform for Documenso.
This commit is contained in:
976
documenso/terraform/main.tf
Normal file
976
documenso/terraform/main.tf
Normal file
@@ -0,0 +1,976 @@
|
||||
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}")
|
||||
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_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]
|
||||
}
|
||||
|
||||
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 = false
|
||||
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"]
|
||||
|
||||
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.name_prefix}/app"
|
||||
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 = true #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" {
|
||||
domain = local.ses_domain
|
||||
}
|
||||
|
||||
resource "aws_route53_record" "ses_verification" {
|
||||
zone_id = data.aws_route53_zone.primary.zone_id
|
||||
name = "_amazonses.${aws_ses_domain_identity.this.domain}"
|
||||
type = "TXT"
|
||||
ttl = 600
|
||||
records = [aws_ses_domain_identity.this.verification_token]
|
||||
}
|
||||
|
||||
resource "aws_ses_domain_dkim" "this" {
|
||||
domain = aws_ses_domain_identity.this.domain
|
||||
}
|
||||
|
||||
resource "aws_route53_record" "ses_dkim" {
|
||||
count = 3
|
||||
|
||||
zone_id = data.aws_route53_zone.primary.zone_id
|
||||
name = "${aws_ses_domain_dkim.this.dkim_tokens[count.index]}._domainkey.${aws_ses_domain_identity.this.domain}"
|
||||
type = "CNAME"
|
||||
ttl = 600
|
||||
records = ["${aws_ses_domain_dkim.this.dkim_tokens[count.index]}.dkim.amazonses.com"]
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user