Manthan AWS — Terraform (D-002)
Infrastructure-as-code for the Manthan testnet/production deployment.
What this stack creates
- VPC with 3 public + 3 private subnets across 3 AZs (
terraform-aws-modules/vpc/aws). - ECS cluster (Fargate launch type, FARGATE + FARGATE_SPOT capacity providers).
- ECS service × N (one per entry in
var.node_names, default alpha/beta/gamma). - ALB with HTTPS listener (TLS 1.3), 900s idle timeout, redirects HTTP→HTTPS.
- EFS (one file system) with per-node access points mounted at
/datainside each task. - KMS symmetric CMK with rotation; encrypts seeds, S3 objects, EFS, log groups, ECR.
- Secrets Manager entry per node holding a 32-byte hex operator seed (random on first apply;
ignore_changesafterwards so manual rotation isn't reverted). - ACM cert (DNS-validated) covering every per-node FQDN.
- Route 53 A-alias records:
<node>.<domain>and<node>-mwp.<domain>→ ALB. - S3 bucket (versioned, KMS-encrypted, fully private) for Charters, LOTS, privacy-policies, signed bootstrap-seed-list.
- ECR repo for the testnet-node image with KMS encryption + lifecycle policy (last 30 images).
- CloudWatch log group per node +
Container Insightson the cluster. - IAM least-privilege execution + task roles per node — task role can read only its own seed, mount only its own EFS access point, list/get only its own S3 prefix.
- CloudWatch alarms on per-node target health and ALB 5xx; D-004 layers PagerDuty/Slack on top.
First-apply workflow
# 0. Pre-req on your host: AWS creds with admin or comparable IAM
aws sts get-caller-identity
# 1. One-shot bootstrap stack — creates the S3+DynamoDB used for remote state
cd infra/terraform/bootstrap
terraform init
terraform apply -var aws_region=eu-west-2
# Note the backend_config_snippet output. Paste it into ../backend.tf.
# 2. Main stack (after un-commenting backend.tf with the bootstrap output)
cd ..
terraform init -reconfigure
cp envs/testnet.tfvars envs/testnet.local.tfvars
$EDITOR envs/testnet.local.tfvars # set domain_name + route53_zone_id
# 3. ECR-only first to get a repo URL we can push the D-001 image to
terraform plan -var-file=envs/testnet.local.tfvars -out=plan.bin
terraform apply -target=module.storage.aws_ecr_repository.node \
-var-file=envs/testnet.local.tfvars
ECR_URL=$(terraform output -raw ecr_repository_url)
# 4. Push the D-001 image (run on the host where the image was built)
aws ecr get-login-password --region eu-west-2 \
| docker login --username AWS --password-stdin "${ECR_URL%%/*}"
docker tag manthan-testnet-node:0.2.0 "$ECR_URL:0.2.0"
docker push "$ECR_URL:0.2.0"
# 5. Full apply
terraform apply -var-file=envs/testnet.local.tfvarsLayout
infra/terraform/
├── README.md
├── versions.tf # provider version pins
├── backend.tf # remote state (uncomment after bootstrap)
├── providers.tf # primary + dr-region providers, default tags
├── variables.tf # input vars (region, domain, node_names, sizing, …)
├── locals.tf # AZ slicing, hostname maps
├── main.tf # ECS cluster + module wiring
├── outputs.tf # ALB DNS, ECR URL, secret ARNs, KMS ARN, …
├── bootstrap/ # one-shot tfstate bucket + lock table
│ └── main.tf
├── envs/
│ ├── testnet.tfvars
│ └── production.tfvars
└── modules/
├── network/ # VPC, ALB, ACM, security groups
├── storage/ # KMS, EFS+access points, S3, ECR, Secrets Manager
├── node/ # per-node ECS service, target groups, listener rules
└── observability/ # baseline CloudWatch alarms (D-004 extends this)Per-node hostnames
Each node is reachable on two hostnames behind the same ALB:
| Hostname | Port | Purpose |
|---|---|---|
<node>.<domain> | 443 | HTTPS HTTP API (/healthz, /charter, /metrics, /sth, /entries, /canary) |
<node>-mwp.<domain> | 443 | WSS MWP gossip endpoint |
The container itself listens on plaintext 0.0.0.0:9120 (HTTP) and 0.0.0.0:9220 (WS). ALB terminates TLS at the edge and forwards over the VPC SG-private path.
D-004 — populating webhook secrets
After the first apply, three Secrets Manager entries exist with placeholder
values (so the alertmanager_definition has something to render). Populate
them with the real PagerDuty integration key, MS Teams webhook URL, and
Slack webhook URL:
PD_ARN=$(terraform output -json alertmanager_secret_arns | jq -r .pagerduty)
TEAMS_ARN=$(terraform output -json alertmanager_secret_arns | jq -r .teams)
SLACK_ARN=$(terraform output -json alertmanager_secret_arns | jq -r .slack)
aws secretsmanager update-secret --secret-id "$PD_ARN" --secret-string "<your-pagerduty-events-api-v2-integration-key>"
aws secretsmanager update-secret --secret-id "$TEAMS_ARN" --secret-string "<your-teams-webhook-url>"
aws secretsmanager update-secret --secret-id "$SLACK_ARN" --secret-string "<your-slack-webhook-url>"
# Re-render the alertmanager definition with the new values:
terraform apply -var-file=envs/testnet.local.tfvars \
-replace=module.observability.aws_prometheus_alert_manager_definition.this[0]For Teams the URL can point at either:
- A Power Automate Workflows webhook (if the Workflow is configured to
accept Alertmanager's native JSON shape), or
- A
prometheus-msteamsadapter URL (the community standard translator).
Both are just opaque webhook URLs to Alertmanager; pick whichever format
matches your tested setup.
Routing baked into the alertmanager definition:
| Severity | PagerDuty | Teams | Slack |
|---|---|---|---|
| SEV-1 | ✓ | — | — |
| SEV-2 | ✓ | ✓ | ✓ |
| SEV-3 | — | ✓ | ✓ |
Toggle individual receivers on/off via enable_pagerduty, enable_teams,enable_slack in tfvars.
Notes & gotchas
- Route 53 hosted zone is NOT created here. Provide the zone ID via
route53_zone_id. Creating a zone requires nameserver delegation work that doesn't belong in code. - Charter/LOTS/privacy-policy upload is manual today. Drop them into
s3://<config_bucket_name>/nodes/<node>/charter.mdetc. The ECS task currently expects them at/etc/manthan/...— wiring an S3-fetcher init container is a follow-up (D-003 will own the production content). - ALB idle timeout is 900s so MWP sessions can stay open through quiet periods (handoff §4 D-002 gotcha 1).
- EFS access points: each task is restricted to
/nodes/<name>/via its access point'sroot_directory, and the IAM task policy enforces the access-point-arn condition (handoff §4 D-002 gotcha 2 + 3). - First apply hits a chicken-and-egg with ECR: the ECS service references
<repo>:0.2.0which won't exist until you push. Use the-targettwo-step in the workflow above, or setdesired_count = 0initially and bump it after the push. - Validation:
terraform validateis run as part of CI;terraform planrequires AWS credentials (intentionally not run from CI without a bound account).