Deployed version of Documenso.
This commit is contained in:
@@ -7,7 +7,7 @@ This Terraform stack deploys Documenso to AWS in `ca-central-1` using:
|
||||
- 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
|
||||
- Optional SES domain identity and DKIM management 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
|
||||
@@ -35,19 +35,21 @@ This is the most practical fit for your Docker Compose workload if you want a ba
|
||||
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.
|
||||
4. You will provide SES SMTP credentials. Terraform does not derive SMTP passwords for you.
|
||||
5. SES identity and DKIM might already be managed outside this stack. By default, this Terraform does not attempt to create them.
|
||||
6. 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.
|
||||
7. 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.
|
||||
8. 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`.
|
||||
4. Set `manage_ses_resources = true` only if you want this stack to own SES identity verification and DKIM records.
|
||||
5. Run `terraform init`.
|
||||
6. Run `terraform plan`.
|
||||
7. Run `terraform apply`.
|
||||
|
||||
## Recommended first production adjustments
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ locals {
|
||||
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"
|
||||
@@ -192,6 +193,13 @@ resource "aws_route_table_association" "public" {
|
||||
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"
|
||||
@@ -259,6 +267,17 @@ resource "aws_security_group" "db" {
|
||||
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
|
||||
@@ -306,7 +325,7 @@ resource "aws_db_instance" "postgres" {
|
||||
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
|
||||
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]
|
||||
@@ -314,6 +333,8 @@ resource "aws_db_instance" "postgres" {
|
||||
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"
|
||||
})
|
||||
@@ -327,7 +348,7 @@ resource "aws_cloudwatch_log_group" "documenso" {
|
||||
}
|
||||
|
||||
resource "aws_secretsmanager_secret" "app" {
|
||||
name = "${local.name_prefix}/app"
|
||||
name = local.app_secret_name
|
||||
recovery_window_in_days = 7
|
||||
|
||||
tags = local.common_tags
|
||||
@@ -383,7 +404,7 @@ resource "aws_s3_bucket" "uploads" {
|
||||
bucket = local.s3_bucket_name
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true #Remove this to tear down the bucket.
|
||||
prevent_destroy = false #Remove this to tear down the bucket.
|
||||
}
|
||||
|
||||
tags = merge(local.common_tags, {
|
||||
@@ -693,29 +714,37 @@ resource "aws_route53_record" "app" {
|
||||
}
|
||||
|
||||
resource "aws_ses_domain_identity" "this" {
|
||||
count = var.manage_ses_resources ? 1 : 0
|
||||
|
||||
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]
|
||||
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" {
|
||||
domain = aws_ses_domain_identity.this.domain
|
||||
count = var.manage_ses_resources ? 1 : 0
|
||||
|
||||
domain = aws_ses_domain_identity.this[0].domain
|
||||
}
|
||||
|
||||
resource "aws_route53_record" "ses_dkim" {
|
||||
count = 3
|
||||
count = var.manage_ses_resources ? 3 : 0
|
||||
|
||||
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"]
|
||||
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" {
|
||||
@@ -727,6 +756,8 @@ resource "aws_ecs_task_definition" "documenso" {
|
||||
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"
|
||||
|
||||
@@ -29,8 +29,8 @@ output "secrets_manager_secret_name" {
|
||||
}
|
||||
|
||||
output "ses_identity_domain" {
|
||||
description = "SES domain verified for outbound mail."
|
||||
value = aws_ses_domain_identity.this.domain
|
||||
description = "SES domain used for outbound mail."
|
||||
value = local.ses_domain
|
||||
}
|
||||
|
||||
output "upload_bucket_name" {
|
||||
|
||||
3878
documenso/terraform/terraform.tfstate
Normal file
3878
documenso/terraform/terraform.tfstate
Normal file
File diff suppressed because it is too large
Load Diff
3056
documenso/terraform/terraform.tfstate.backup
Normal file
3056
documenso/terraform/terraform.tfstate.backup
Normal file
File diff suppressed because it is too large
Load Diff
23
documenso/terraform/terraform.tfvars.example
Normal file
23
documenso/terraform/terraform.tfvars.example
Normal file
@@ -0,0 +1,23 @@
|
||||
aws_region = "ca-central-1"
|
||||
domain_name = "esignature.imex.online"
|
||||
hosted_zone_name = "imex.online"
|
||||
documenso_image = "documenso/documenso:latest"
|
||||
smtp_username = "AKIA2MRSPON3O6PRVUPE"
|
||||
smtp_password = "pw"
|
||||
smtp_from_address = "no-reply@imex.online"
|
||||
manage_ses_resources = false
|
||||
ses_identity_domain = "imex.online"
|
||||
app_secret_name = "documenso/esignature-imex-online/app"
|
||||
# signing_certificate_base64 = "MII...base64-encoded-p12..."
|
||||
# signing_certificate_passphrase = "replace-with-your-p12-passphrase"
|
||||
# upload_bucket_name = "esignature-imex-online-documenso"
|
||||
|
||||
# Optional tuning
|
||||
# desired_count = 2
|
||||
# max_count = 6
|
||||
db_instance_class = "db.t4g.micro"
|
||||
db_publicly_accessible = true
|
||||
db_allowed_cidrs = ["64.46.30.40/32"]
|
||||
disable_signup = false
|
||||
# allowed_signup_domains = "imex.online"
|
||||
# alarm_actions = ["arn:aws:sns:ca-central-1:123456789012:ops-alerts"]
|
||||
@@ -23,11 +23,17 @@ variable "hosted_zone_name" {
|
||||
}
|
||||
|
||||
variable "ses_identity_domain" {
|
||||
description = "Domain to verify in SES. Defaults to the hosted zone when null."
|
||||
description = "Domain used for SES. Defaults to the hosted zone when null. If manage_ses_resources is false, this is informational and used only for outputs/documentation."
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "manage_ses_resources" {
|
||||
description = "Whether this Terraform stack should create and manage the SES domain identity, verification record, and DKIM records. Disable this when SES is already configured elsewhere."
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "documenso_image" {
|
||||
description = "Container image for Documenso. Default keeps you on the latest published image."
|
||||
type = string
|
||||
@@ -166,6 +172,18 @@ variable "db_final_snapshot_on_destroy" {
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "db_publicly_accessible" {
|
||||
description = "Whether the RDS instance should have a public endpoint. Requires database subnets with a route to the internet gateway."
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "db_allowed_cidrs" {
|
||||
description = "IPv4 CIDR blocks allowed to connect directly to PostgreSQL. Leave empty to disable direct public access."
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "disable_signup" {
|
||||
description = "Disable public signup in Documenso."
|
||||
type = bool
|
||||
@@ -211,7 +229,7 @@ variable "smtp_password" {
|
||||
variable "smtp_from_name" {
|
||||
description = "Display name used in outbound email."
|
||||
type = string
|
||||
default = "IMEX eSignature"
|
||||
default = "ImEX E-Signature"
|
||||
}
|
||||
|
||||
variable "smtp_from_address" {
|
||||
@@ -233,6 +251,12 @@ variable "signing_certificate_passphrase" {
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "app_secret_name" {
|
||||
description = "Secrets Manager secret name used for Documenso application secrets. Set this if a previous secret with the default name is pending deletion."
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
description = "Additional tags applied to all supported resources."
|
||||
type = map(string)
|
||||
|
||||
Reference in New Issue
Block a user