Skip to content

AWS ECS Fargate (Beta)

Deploy Broch on AWS with ECS Fargate, an Application Load Balancer, and RDS PostgreSQL. The Terraform module lives in broch-io/broch-deploy/terraform/aws-ecs — clone the repo, fill in terraform.tfvars, and terraform apply.

Beta. This module is less exercised by automation than the AWS appliance. Test it before you rely on it.

  • VPC with 2 public + 2 private subnets across 2 AZs, single NAT gateway
  • Application Load Balancer with ACM cert (hostname + wildcard, DNS-validated via Route 53)
  • ECS Fargate cluster + task definition + service, one task (no autoscaling)
  • RDS PostgreSQL 17 in private subnets, encrypted at rest (db.t4g.micro by default)
  • Secrets Manager entries: master key (generated BROCH_MASTER_KEY), IdP client secret, DB password, connection string
  • IAM execution role scoped tight to just this stack’s secrets
  • Route 53 A-ALIAS records: hostname + wildcard pointing at the ALB
  • CloudWatch Logs group, 30-day retention

All resources are created in your account. Broch has no access to your environment.

This stack runs ALB + Fargate + RDS + a NAT gateway. To estimate spend, use the AWS Pricing Calculator with your chosen sizes; the module README notes the resource tradeoffs.

  • Terraform 1.6+
  • AWS credentials with permissions to create VPC, ECS, RDS, ALB, Route 53, IAM, ACM, and Secrets Manager resources
  • A Route 53 hosted zone for your wildcard hostname’s parent domain
  • Your hostname chosen; the module creates its records in the Route 53 zone
  • Identity provider app registration created
  • License — activated in-app after first sign-in (nothing to set before deploy)
Terminal window
git clone https://github.com/broch-io/broch-deploy.git
cd broch-deploy/terraform/aws-ecs
cp terraform.tfvars.example terraform.tfvars
$EDITOR terraform.tfvars # non-secret values; delete the auth_client_secret line
# Pass the IdP client secret as an environment variable, not in the file.
# Set it before every plan, apply or destroy (upgrades included): an empty
# value overwrites the stored secret. Paste the value, then press Enter.
read -rs TF_VAR_auth_client_secret
export TF_VAR_auth_client_secret
terraform init
terraform plan
terraform apply # 5-10 min on first run; RDS is the long pole

terraform plan refuses settings Broch would refuse at startup: an unknown auth_provider, a missing client ID or secret, a blank wildcard_hostname, or a value your provider needs (Auth0: auth_domain and auth_audience; Okta: auth_domain; Entra ID: auth_tenant_id and auth_instance, such as https://login.microsoftonline.com/; generic OIDC: auth_authority).

After apply:

Terminal window
echo "Broch is at: $(terraform output -raw broch_url)"

DNS records are created automatically by the Terraform — no manual record entry needed. The ACM cert is DNS-validated against your Route 53 zone, also automatic.

The Terraform state holds every secret in plaintext (master key, client secret, database credentials): keep terraform.tfstate somewhere access-controlled, such as an S3 remote backend.

Terminal window
curl "$(terraform output -raw broch_url)/healthz" # liveness — 200 once the process is up
curl "$(terraform output -raw broch_url)/healthz/ready" # readiness — 200 once the license and database check out

/healthz/ready returns 503 until an admin completes setup. Sign in at your Broch URL and finish in the browser: see Self-Managed Deployment → After You Install.

Update the image tag and re-apply, then point the service at the new task definition. apply registers a new task-definition revision but deliberately leaves the service on the old one, so apply alone does not upgrade. The aws commands on this page assume the default name_prefix (broch) and that your CLI region matches aws_region (add --region otherwise):

Terminal window
$EDITOR terraform.tfvars # set broch_image = "ghcr.io/broch-io/broch:<new-version>"
read -rs TF_VAR_auth_client_secret # the current value, unless you're rotating it
export TF_VAR_auth_client_secret
terraform apply
# Naming the family (not a revision) deploys its latest revision.
aws ecs update-service \
--cluster broch-cluster \
--service broch-service \
--task-definition broch-broch

ECS starts the new task, then stops the old one; tunnels reconnect on their own. Database migrations run automatically on the new task’s first start. Any other task-definition change, such as task_cpu, task_memory or an auth_* setting, needs the same update-service step after apply.

For force-refreshing the image at the same tag (e.g. pulling a re-tagged :latest):

Terminal window
aws ecs update-service \
--cluster broch-cluster \
--service broch-service \
--force-new-deployment

RDS handles automated backups with the configured retention period (7 days default — see backup_retention_period in the module’s database.tf). Snapshot manually before risky operations:

Terminal window
aws rds create-db-snapshot \
--db-instance-identifier broch-postgres \
--db-snapshot-identifier broch-pre-upgrade-$(date +%Y%m%d)

Terraform owns the Secrets Manager entries, so rotate through it: a direct edit bypasses Terraform’s state, and the next change to the variable overwrites it. To rotate the IdP client secret, set the new value and apply, then force a new ECS deployment so the task reads it:

Terminal window
read -rs TF_VAR_auth_client_secret # paste the new value, then Enter
export TF_VAR_auth_client_secret
terraform apply
aws ecs update-service \
--cluster broch-cluster \
--service broch-service \
--force-new-deployment

The default terraform.tfvars.example values prioritise a minimal footprint over availability. Before production:

  • Set multi_az = true on the RDS resource for cross-AZ failover
  • Set skip_final_snapshot = false (with a final_snapshot_identifier) on RDS. Deletion protection is already on (rds_deletion_protection)
  • Database TLS isn’t authenticated: the connection string sets no SSL Mode, so the RDS certificate isn’t verified. See the module README
  • Add WAF in front of the ALB if you’re exposed to abuse

See the module README for the full list.

RDS deletion protection is on by default, so turn it off first, with TF_VAR_auth_client_secret still set:

Terminal window
terraform apply -var rds_deletion_protection=false
terraform destroy -var rds_deletion_protection=false

skip_final_snapshot = true in the default config means the Postgres data is deleted permanently. Snapshot first if you want to keep it.