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
|
- S3 for document uploads and signed PDFs
|
||||||
- Application Load Balancer with ACM-managed TLS
|
- Application Load Balancer with ACM-managed TLS
|
||||||
- Route53 DNS for `esignature.imex.online`
|
- 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
|
- 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
|
- AWS WAF with a basic managed rule set and rate limiting
|
||||||
- CloudWatch alarms for ALB, ECS, and RDS health indicators
|
- 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.
|
1. Your DNS for `imex.online` is hosted in Route53.
|
||||||
2. You want Multi-AZ RDS enabled from the start for database availability.
|
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.
|
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.
|
4. You will provide SES SMTP credentials. Terraform 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.
|
5. SES identity and DKIM might already be managed outside this stack. By default, this Terraform does not attempt to create them.
|
||||||
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.
|
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 want Terraform destroy protection enabled for both the database and the uploads bucket.
|
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
|
## Deploy
|
||||||
|
|
||||||
1. Copy `terraform.tfvars.example` to `terraform.tfvars` and fill in the SMTP values.
|
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`.
|
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.
|
3. Optionally set `upload_bucket_name` if you want a specific S3 bucket name.
|
||||||
4. Run `terraform init`.
|
4. Set `manage_ses_resources = true` only if you want this stack to own SES identity verification and DKIM records.
|
||||||
5. Run `terraform plan`.
|
5. Run `terraform init`.
|
||||||
6. Run `terraform apply`.
|
6. Run `terraform plan`.
|
||||||
|
7. Run `terraform apply`.
|
||||||
|
|
||||||
## Recommended first production adjustments
|
## Recommended first production adjustments
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ locals {
|
|||||||
ses_domain = coalesce(var.ses_identity_domain, var.hosted_zone_name)
|
ses_domain = coalesce(var.ses_identity_domain, var.hosted_zone_name)
|
||||||
smtp_host = "email-smtp.${var.aws_region}.amazonaws.com"
|
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}")
|
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, {
|
common_tags = merge(var.tags, {
|
||||||
Application = var.project_name
|
Application = var.project_name
|
||||||
ManagedBy = "Terraform"
|
ManagedBy = "Terraform"
|
||||||
@@ -192,6 +193,13 @@ resource "aws_route_table_association" "public" {
|
|||||||
route_table_id = aws_route_table.public.id
|
route_table_id = aws_route_table.public.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resource "aws_route_table_association" "database_public" {
|
||||||
|
count = var.db_publicly_accessible ? length(aws_subnet.database) : 0
|
||||||
|
|
||||||
|
subnet_id = aws_subnet.database[count.index].id
|
||||||
|
route_table_id = aws_route_table.public.id
|
||||||
|
}
|
||||||
|
|
||||||
resource "aws_security_group" "alb" {
|
resource "aws_security_group" "alb" {
|
||||||
name = "${local.name_prefix}-alb-sg"
|
name = "${local.name_prefix}-alb-sg"
|
||||||
description = "Public ingress to the Documenso load balancer"
|
description = "Public ingress to the Documenso load balancer"
|
||||||
@@ -259,6 +267,17 @@ resource "aws_security_group" "db" {
|
|||||||
security_groups = [aws_security_group.ecs.id]
|
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 {
|
egress {
|
||||||
from_port = 0
|
from_port = 0
|
||||||
to_port = 0
|
to_port = 0
|
||||||
@@ -306,7 +325,7 @@ resource "aws_db_instance" "postgres" {
|
|||||||
skip_final_snapshot = !var.db_final_snapshot_on_destroy
|
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
|
final_snapshot_identifier = var.db_final_snapshot_on_destroy ? "${local.name_prefix}-final-${random_id.final_snapshot.hex}" : null
|
||||||
auto_minor_version_upgrade = true
|
auto_minor_version_upgrade = true
|
||||||
publicly_accessible = false
|
publicly_accessible = var.db_publicly_accessible
|
||||||
apply_immediately = false
|
apply_immediately = false
|
||||||
db_subnet_group_name = aws_db_subnet_group.this.name
|
db_subnet_group_name = aws_db_subnet_group.this.name
|
||||||
vpc_security_group_ids = [aws_security_group.db.id]
|
vpc_security_group_ids = [aws_security_group.db.id]
|
||||||
@@ -314,6 +333,8 @@ resource "aws_db_instance" "postgres" {
|
|||||||
performance_insights_enabled = false
|
performance_insights_enabled = false
|
||||||
enabled_cloudwatch_logs_exports = ["postgresql", "upgrade"]
|
enabled_cloudwatch_logs_exports = ["postgresql", "upgrade"]
|
||||||
|
|
||||||
|
depends_on = [aws_route_table_association.database_public]
|
||||||
|
|
||||||
tags = merge(local.common_tags, {
|
tags = merge(local.common_tags, {
|
||||||
Name = "${local.name_prefix}-postgres"
|
Name = "${local.name_prefix}-postgres"
|
||||||
})
|
})
|
||||||
@@ -327,7 +348,7 @@ resource "aws_cloudwatch_log_group" "documenso" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_secretsmanager_secret" "app" {
|
resource "aws_secretsmanager_secret" "app" {
|
||||||
name = "${local.name_prefix}/app"
|
name = local.app_secret_name
|
||||||
recovery_window_in_days = 7
|
recovery_window_in_days = 7
|
||||||
|
|
||||||
tags = local.common_tags
|
tags = local.common_tags
|
||||||
@@ -383,7 +404,7 @@ resource "aws_s3_bucket" "uploads" {
|
|||||||
bucket = local.s3_bucket_name
|
bucket = local.s3_bucket_name
|
||||||
|
|
||||||
lifecycle {
|
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, {
|
tags = merge(local.common_tags, {
|
||||||
@@ -693,29 +714,37 @@ resource "aws_route53_record" "app" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_ses_domain_identity" "this" {
|
resource "aws_ses_domain_identity" "this" {
|
||||||
|
count = var.manage_ses_resources ? 1 : 0
|
||||||
|
|
||||||
domain = local.ses_domain
|
domain = local.ses_domain
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_route53_record" "ses_verification" {
|
resource "aws_route53_record" "ses_verification" {
|
||||||
zone_id = data.aws_route53_zone.primary.zone_id
|
count = var.manage_ses_resources ? 1 : 0
|
||||||
name = "_amazonses.${aws_ses_domain_identity.this.domain}"
|
|
||||||
type = "TXT"
|
zone_id = data.aws_route53_zone.primary.zone_id
|
||||||
ttl = 600
|
name = "_amazonses.${aws_ses_domain_identity.this[0].domain}"
|
||||||
records = [aws_ses_domain_identity.this.verification_token]
|
type = "TXT"
|
||||||
|
ttl = 600
|
||||||
|
records = [aws_ses_domain_identity.this[0].verification_token]
|
||||||
|
allow_overwrite = true
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_ses_domain_dkim" "this" {
|
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" {
|
resource "aws_route53_record" "ses_dkim" {
|
||||||
count = 3
|
count = var.manage_ses_resources ? 3 : 0
|
||||||
|
|
||||||
zone_id = data.aws_route53_zone.primary.zone_id
|
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}"
|
name = "${aws_ses_domain_dkim.this[0].dkim_tokens[count.index]}._domainkey.${aws_ses_domain_identity.this[0].domain}"
|
||||||
type = "CNAME"
|
type = "CNAME"
|
||||||
ttl = 600
|
ttl = 600
|
||||||
records = ["${aws_ses_domain_dkim.this.dkim_tokens[count.index]}.dkim.amazonses.com"]
|
records = ["${aws_ses_domain_dkim.this[0].dkim_tokens[count.index]}.dkim.amazonses.com"]
|
||||||
|
allow_overwrite = true
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "aws_ecs_task_definition" "documenso" {
|
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
|
execution_role_arn = aws_iam_role.ecs_task_execution.arn
|
||||||
task_role_arn = aws_iam_role.ecs_task.arn
|
task_role_arn = aws_iam_role.ecs_task.arn
|
||||||
|
|
||||||
|
depends_on = [aws_secretsmanager_secret_version.app]
|
||||||
|
|
||||||
container_definitions = jsonencode([
|
container_definitions = jsonencode([
|
||||||
{
|
{
|
||||||
name = "documenso"
|
name = "documenso"
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ output "secrets_manager_secret_name" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
output "ses_identity_domain" {
|
output "ses_identity_domain" {
|
||||||
description = "SES domain verified for outbound mail."
|
description = "SES domain used for outbound mail."
|
||||||
value = aws_ses_domain_identity.this.domain
|
value = local.ses_domain
|
||||||
}
|
}
|
||||||
|
|
||||||
output "upload_bucket_name" {
|
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" {
|
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
|
type = string
|
||||||
default = null
|
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" {
|
variable "documenso_image" {
|
||||||
description = "Container image for Documenso. Default keeps you on the latest published image."
|
description = "Container image for Documenso. Default keeps you on the latest published image."
|
||||||
type = string
|
type = string
|
||||||
@@ -166,6 +172,18 @@ variable "db_final_snapshot_on_destroy" {
|
|||||||
default = true
|
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" {
|
variable "disable_signup" {
|
||||||
description = "Disable public signup in Documenso."
|
description = "Disable public signup in Documenso."
|
||||||
type = bool
|
type = bool
|
||||||
@@ -211,7 +229,7 @@ variable "smtp_password" {
|
|||||||
variable "smtp_from_name" {
|
variable "smtp_from_name" {
|
||||||
description = "Display name used in outbound email."
|
description = "Display name used in outbound email."
|
||||||
type = string
|
type = string
|
||||||
default = "IMEX eSignature"
|
default = "ImEX E-Signature"
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "smtp_from_address" {
|
variable "smtp_from_address" {
|
||||||
@@ -233,6 +251,12 @@ variable "signing_certificate_passphrase" {
|
|||||||
sensitive = true
|
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" {
|
variable "tags" {
|
||||||
description = "Additional tags applied to all supported resources."
|
description = "Additional tags applied to all supported resources."
|
||||||
type = map(string)
|
type = map(string)
|
||||||
|
|||||||
Reference in New Issue
Block a user