Initial terraform for Documenso.
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -152,3 +152,5 @@ docker_data
|
|||||||
/.github/copilot-instructions.md
|
/.github/copilot-instructions.md
|
||||||
/GEMINI.md
|
/GEMINI.md
|
||||||
/_reference/select-component-test-plan.md
|
/_reference/select-component-test-plan.md
|
||||||
|
|
||||||
|
.terraform
|
||||||
45
documenso/terraform/.terraform.lock.hcl
generated
Normal file
45
documenso/terraform/.terraform.lock.hcl
generated
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# This file is maintained automatically by "terraform init".
|
||||||
|
# Manual edits may be lost in future updates.
|
||||||
|
|
||||||
|
provider "registry.terraform.io/hashicorp/aws" {
|
||||||
|
version = "6.38.0"
|
||||||
|
constraints = "~> 6.0"
|
||||||
|
hashes = [
|
||||||
|
"h1:RDoKIzXmt7H1mNFcNIyRT+nA/gTJyO3+iW9QGN5I2eQ=",
|
||||||
|
"zh:143f118ae71059a7a7026c6b950da23fef04a06e2362ffa688bef75e43e869ed",
|
||||||
|
"zh:29ee220a017306effd877e1280f8b2934dc957e16e0e72ca0222e5514d0db522",
|
||||||
|
"zh:3a31baabf7aea7aa7669f5a3d76f3445e0e6cce5e9aea0279992765c0df12aee",
|
||||||
|
"zh:4c1908e62040dbc9901d4426ffb253f53e5dae9e3e1a9125311291ee265c8d8c",
|
||||||
|
"zh:550f4789f5f5b00e16118d4c17770be3ef4535d6b6928af1cf91ebd30f2c263b",
|
||||||
|
"zh:6537b7b70bf2c127771b0b84e4b726c834d10666b6104f017edae50c67ebae37",
|
||||||
|
"zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
|
||||||
|
"zh:af2f9cea0c8bdf5b2a2391f2d179a946c117196f7c829b919673cae3b71d2943",
|
||||||
|
"zh:c53ffa685381aa4e73158fd9f529239f95938dea330e7aca0b32e7b2a1210432",
|
||||||
|
"zh:d0995e1d64a7ec8bbc79fc3fbec3749f989e07f211a318705c37cd6a7c7d19e4",
|
||||||
|
"zh:d2348ffcffc1282983d7a5838dd5d61f372152fe6c0d10868cd6473352318750",
|
||||||
|
"zh:e449312efb73e4747165e689302a68a1df8ba5755e7f59097069acf82c94f011",
|
||||||
|
"zh:ec3a538d264ef79380e56fdf107ffb6c0446814f07fc5890c36855fe1e03196b",
|
||||||
|
"zh:f441e69699b22e32c96a8cdd3bbe694ed302c0dcfe867cd9bd683a16df362714",
|
||||||
|
"zh:f6f8eaa605ff902234d7e9bdab4fda977185fce14f8576f7b622c914c7d98008",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "registry.terraform.io/hashicorp/random" {
|
||||||
|
version = "3.8.1"
|
||||||
|
constraints = "~> 3.6"
|
||||||
|
hashes = [
|
||||||
|
"h1:u8AKlWVDTH5r9YLSeswoVEjiY72Rt4/ch7U+61ZDkiQ=",
|
||||||
|
"zh:08dd03b918c7b55713026037c5400c48af5b9f468f483463321bd18e17b907b4",
|
||||||
|
"zh:0eee654a5542dc1d41920bbf2419032d6f0d5625b03bd81339e5b33394a3e0ae",
|
||||||
|
"zh:229665ddf060aa0ed315597908483eee5b818a17d09b6417a0f52fd9405c4f57",
|
||||||
|
"zh:2469d2e48f28076254a2a3fc327f184914566d9e40c5780b8d96ebf7205f8bc0",
|
||||||
|
"zh:37d7eb334d9561f335e748280f5535a384a88675af9a9eac439d4cfd663bcb66",
|
||||||
|
"zh:741101426a2f2c52dee37122f0f4a2f2d6af6d852cb1db634480a86398fa3511",
|
||||||
|
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||||
|
"zh:a902473f08ef8df62cfe6116bd6c157070a93f66622384300de235a533e9d4a9",
|
||||||
|
"zh:b85c511a23e57a2147355932b3b6dce2a11e856b941165793a0c3d7578d94d05",
|
||||||
|
"zh:c5172226d18eaac95b1daac80172287b69d4ce32750c82ad77fa0768be4ea4b8",
|
||||||
|
"zh:dab4434dba34aad569b0bc243c2d3f3ff86dd7740def373f2a49816bd2ff819b",
|
||||||
|
"zh:f49fd62aa8c5525a5c17abd51e27ca5e213881d58882fd42fec4a545b53c9699",
|
||||||
|
]
|
||||||
|
}
|
||||||
57
documenso/terraform/README.md
Normal file
57
documenso/terraform/README.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# Documenso on AWS
|
||||||
|
|
||||||
|
This Terraform stack deploys Documenso to AWS in `ca-central-1` using:
|
||||||
|
|
||||||
|
- ECS Fargate for the application tier
|
||||||
|
- RDS PostgreSQL for the database tier
|
||||||
|
- S3 for document uploads and signed PDFs
|
||||||
|
- Application Load Balancer with ACM-managed TLS
|
||||||
|
- Route53 DNS for `esignature.imex.online`
|
||||||
|
- SES domain identity and DKIM records for outbound email
|
||||||
|
- Secrets Manager for generated application secrets, SMTP credentials, and the optional Documenso signing certificate
|
||||||
|
- AWS WAF with a basic managed rule set and rate limiting
|
||||||
|
- CloudWatch alarms for ALB, ECS, and RDS health indicators
|
||||||
|
|
||||||
|
## Why this shape
|
||||||
|
|
||||||
|
This is the most practical fit for your Docker Compose workload if you want a balance of cost efficiency, managed operations, and scaling:
|
||||||
|
|
||||||
|
- Fargate gives you horizontal scaling without managing EC2 hosts.
|
||||||
|
- RDS PostgreSQL is simpler and cheaper than Aurora for a single Documenso workload.
|
||||||
|
- S3-backed uploads are better for production scale and keep document growth out of PostgreSQL.
|
||||||
|
- The database stays private; the ALB is public.
|
||||||
|
- The ECS tasks run in public subnets to avoid a NAT gateway charge. Inbound access is still restricted to the ALB security group.
|
||||||
|
- HTTPS is terminated by the ALB using ACM. The Documenso self-signed `.p12` certificate is separate and is used for document signing, not browser TLS.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `main.tf`: core infrastructure
|
||||||
|
- `variables.tf`: configurable inputs
|
||||||
|
- `outputs.tf`: useful deployment outputs
|
||||||
|
- `terraform.tfvars.example`: example input values
|
||||||
|
|
||||||
|
## Assumptions built into this stack
|
||||||
|
|
||||||
|
1. Your DNS for `imex.online` is hosted in Route53.
|
||||||
|
2. You want Multi-AZ RDS enabled from the start for database availability.
|
||||||
|
3. You are comfortable starting with `documenso/documenso:latest`. For repeatable deployments, pin a version or digest after your first rollout.
|
||||||
|
4. You will provide SES SMTP credentials. Terraform verifies the SES domain, but it does not derive SMTP passwords for you.
|
||||||
|
5. You will provide a base64-encoded PKCS#12 signing certificate and passphrase if you want document signing enabled immediately. This stack injects those values through Secrets Manager instead of mounting a host file.
|
||||||
|
6. You are comfortable with Terraform creating a dedicated IAM user and access key for Documenso S3 uploads because Documenso documents explicit S3 credentials for the upload backend.
|
||||||
|
7. You want Terraform destroy protection enabled for both the database and the uploads bucket.
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
1. Copy `terraform.tfvars.example` to `terraform.tfvars` and fill in the SMTP values.
|
||||||
|
2. If you want Documenso signing enabled, add `signing_certificate_base64` and `signing_certificate_passphrase`.
|
||||||
|
3. Optionally set `upload_bucket_name` if you want a specific S3 bucket name.
|
||||||
|
4. Run `terraform init`.
|
||||||
|
5. Run `terraform plan`.
|
||||||
|
6. Run `terraform apply`.
|
||||||
|
|
||||||
|
## Recommended first production adjustments
|
||||||
|
|
||||||
|
1. Pin the Documenso image to a tested version or digest.
|
||||||
|
2. Wire `alarm_actions` to an SNS topic, PagerDuty bridge, or your on-call system so alarms notify someone.
|
||||||
|
3. Expand the WAF rule set if you need more aggressive filtering later.
|
||||||
|
4. Add CloudWatch alarms on ECS 5xx errors, ALB target health, and RDS CPU/storage.
|
||||||
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
|
||||||
|
}
|
||||||
44
documenso/terraform/outputs.tf
Normal file
44
documenso/terraform/outputs.tf
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
output "application_url" {
|
||||||
|
description = "Public URL for the Documenso deployment."
|
||||||
|
value = "https://${var.domain_name}"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "load_balancer_dns_name" {
|
||||||
|
description = "DNS name assigned to the application load balancer."
|
||||||
|
value = aws_lb.this.dns_name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "database_endpoint" {
|
||||||
|
description = "RDS PostgreSQL endpoint for the application."
|
||||||
|
value = aws_db_instance.postgres.address
|
||||||
|
}
|
||||||
|
|
||||||
|
output "postgres_engine_version" {
|
||||||
|
description = "Resolved PostgreSQL engine version deployed to RDS."
|
||||||
|
value = aws_db_instance.postgres.engine_version
|
||||||
|
}
|
||||||
|
|
||||||
|
output "ecs_cluster_name" {
|
||||||
|
description = "ECS cluster name running the Documenso service."
|
||||||
|
value = aws_ecs_cluster.this.name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "secrets_manager_secret_name" {
|
||||||
|
description = "Secrets Manager secret that stores generated and supplied application secrets."
|
||||||
|
value = aws_secretsmanager_secret.app.name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "ses_identity_domain" {
|
||||||
|
description = "SES domain verified for outbound mail."
|
||||||
|
value = aws_ses_domain_identity.this.domain
|
||||||
|
}
|
||||||
|
|
||||||
|
output "upload_bucket_name" {
|
||||||
|
description = "S3 bucket used for Documenso uploads."
|
||||||
|
value = aws_s3_bucket.uploads.bucket
|
||||||
|
}
|
||||||
|
|
||||||
|
output "waf_web_acl_arn" {
|
||||||
|
description = "ARN of the WAF web ACL attached to the ALB."
|
||||||
|
value = aws_wafv2_web_acl.this.arn
|
||||||
|
}
|
||||||
282
documenso/terraform/variables.tf
Normal file
282
documenso/terraform/variables.tf
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
variable "aws_region" {
|
||||||
|
description = "AWS region for the deployment."
|
||||||
|
type = string
|
||||||
|
default = "ca-central-1"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "project_name" {
|
||||||
|
description = "Logical name used to prefix created resources."
|
||||||
|
type = string
|
||||||
|
default = "documenso"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "domain_name" {
|
||||||
|
description = "Fully qualified domain name for the application."
|
||||||
|
type = string
|
||||||
|
default = "esignature.imex.online"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "hosted_zone_name" {
|
||||||
|
description = "Public Route53 hosted zone that contains the application hostname."
|
||||||
|
type = string
|
||||||
|
default = "imex.online"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ses_identity_domain" {
|
||||||
|
description = "Domain to verify in SES. Defaults to the hosted zone when null."
|
||||||
|
type = string
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "documenso_image" {
|
||||||
|
description = "Container image for Documenso. Default keeps you on the latest published image."
|
||||||
|
type = string
|
||||||
|
default = "documenso/documenso:latest"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "app_port" {
|
||||||
|
description = "Container port exposed by Documenso."
|
||||||
|
type = number
|
||||||
|
default = 3000
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "upload_bucket_name" {
|
||||||
|
description = "Optional S3 bucket name for Documenso uploads. If null, Terraform generates a globally unique name based on account and region."
|
||||||
|
type = string
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "s3_versioning_enabled" {
|
||||||
|
description = "Enable S3 object versioning for uploaded documents."
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "document_size_upload_limit_mb" {
|
||||||
|
description = "Upload size limit shown in the Documenso UI, in MB."
|
||||||
|
type = number
|
||||||
|
default = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vpc_cidr" {
|
||||||
|
description = "CIDR block used for the VPC."
|
||||||
|
type = string
|
||||||
|
default = "10.42.0.0/16"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "fargate_cpu" {
|
||||||
|
description = "Fargate CPU units for the task."
|
||||||
|
type = number
|
||||||
|
default = 512
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "fargate_memory" {
|
||||||
|
description = "Fargate memory in MiB for the task."
|
||||||
|
type = number
|
||||||
|
default = 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "desired_count" {
|
||||||
|
description = "Initial number of running Documenso tasks."
|
||||||
|
type = number
|
||||||
|
default = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "min_count" {
|
||||||
|
description = "Minimum number of tasks for autoscaling."
|
||||||
|
type = number
|
||||||
|
default = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "max_count" {
|
||||||
|
description = "Maximum number of tasks for autoscaling."
|
||||||
|
type = number
|
||||||
|
default = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "cpu_target_utilization" {
|
||||||
|
description = "Target average CPU utilization for ECS autoscaling."
|
||||||
|
type = number
|
||||||
|
default = 65
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "memory_target_utilization" {
|
||||||
|
description = "Target average memory utilization for ECS autoscaling."
|
||||||
|
type = number
|
||||||
|
default = 75
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "postgres_major_version" {
|
||||||
|
description = "Preferred PostgreSQL major version. Terraform resolves the latest matching minor release supported by AWS."
|
||||||
|
type = string
|
||||||
|
default = "17"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_name" {
|
||||||
|
description = "Initial PostgreSQL database name."
|
||||||
|
type = string
|
||||||
|
default = "documenso"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_username" {
|
||||||
|
description = "Master PostgreSQL username for the application."
|
||||||
|
type = string
|
||||||
|
default = "documenso"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_instance_class" {
|
||||||
|
description = "RDS instance class. Graviton classes are usually the best cost/performance option for Postgres."
|
||||||
|
type = string
|
||||||
|
default = "db.t4g.small"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_allocated_storage" {
|
||||||
|
description = "Initial allocated storage in GiB."
|
||||||
|
type = number
|
||||||
|
default = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_max_allocated_storage" {
|
||||||
|
description = "Maximum autoscaled storage in GiB."
|
||||||
|
type = number
|
||||||
|
default = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_backup_retention_days" {
|
||||||
|
description = "How many days of automated backups to retain."
|
||||||
|
type = number
|
||||||
|
default = 7
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_multi_az" {
|
||||||
|
description = "Enable Multi-AZ for higher database availability at higher cost."
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_deletion_protection" {
|
||||||
|
description = "Protect the database from accidental deletion."
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_final_snapshot_on_destroy" {
|
||||||
|
description = "Create a final snapshot if the database is destroyed."
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "disable_signup" {
|
||||||
|
description = "Disable public signup in Documenso."
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "allowed_signup_domains" {
|
||||||
|
description = "Optional comma-separated list of allowed email domains when signup is enabled."
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "smtp_port" {
|
||||||
|
description = "SES SMTP endpoint port."
|
||||||
|
type = number
|
||||||
|
default = 587
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "smtp_secure" {
|
||||||
|
description = "Whether to use SMTPS. Keep false for SES on port 587 with STARTTLS."
|
||||||
|
type = bool
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "smtp_unsafe_ignore_tls" {
|
||||||
|
description = "Whether the application should ignore TLS issues when sending mail."
|
||||||
|
type = bool
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "smtp_username" {
|
||||||
|
description = "SES SMTP username."
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "smtp_password" {
|
||||||
|
description = "SES SMTP password."
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "smtp_from_name" {
|
||||||
|
description = "Display name used in outbound email."
|
||||||
|
type = string
|
||||||
|
default = "IMEX eSignature"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "smtp_from_address" {
|
||||||
|
description = "Verified sender email address for SES."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "signing_certificate_base64" {
|
||||||
|
description = "Base64-encoded PKCS#12 signing certificate contents for Documenso. Leave empty to omit certificate injection."
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "signing_certificate_passphrase" {
|
||||||
|
description = "Passphrase for the Documenso signing certificate. Leave empty to omit it."
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "tags" {
|
||||||
|
description = "Additional tags applied to all supported resources."
|
||||||
|
type = map(string)
|
||||||
|
default = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "waf_rate_limit" {
|
||||||
|
description = "Maximum requests per 5-minute window from a single IP before WAF blocks it."
|
||||||
|
type = number
|
||||||
|
default = 2000
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "alarm_actions" {
|
||||||
|
description = "Optional list of SNS topic ARNs or other alarm actions to invoke when CloudWatch alarms fire."
|
||||||
|
type = list(string)
|
||||||
|
default = []
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "alb_5xx_alarm_threshold" {
|
||||||
|
description = "Threshold for ALB 5xx count over a 5-minute period."
|
||||||
|
type = number
|
||||||
|
default = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ecs_cpu_alarm_threshold" {
|
||||||
|
description = "Threshold for average ECS CPU utilization alarm."
|
||||||
|
type = number
|
||||||
|
default = 85
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ecs_memory_alarm_threshold" {
|
||||||
|
description = "Threshold for average ECS memory utilization alarm."
|
||||||
|
type = number
|
||||||
|
default = 85
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "rds_cpu_alarm_threshold" {
|
||||||
|
description = "Threshold for average RDS CPU utilization alarm."
|
||||||
|
type = number
|
||||||
|
default = 80
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "rds_free_storage_alarm_threshold_bytes" {
|
||||||
|
description = "Alarm threshold for low RDS free storage, in bytes."
|
||||||
|
type = number
|
||||||
|
default = 5368709120
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user