Infrastructure
All AWS resources are managed via Terraform using terraform-aws-modules. The infrastructure lives in the terraform/ directory. No manual console configuration.
AWS Account
| Resource | Description |
|---|---|
| Account | Dedicated AWS account (mailman) |
| Region | us-east-1 (required for SES receipt rules) |
VPC Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ VPC │
│ │
│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │
│ │ Public Subnets │ │ Public Subnets │ │
│ │ (AZ-a) │ │ (AZ-b) │ │
│ │ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │ │
│ │ │ NAT Gateway │ │ │ │ NAT Gateway │ │ │
│ │ └─────────────────────┘ │ │ └─────────────────────┘ │ │
│ └─────────────────────────────┘ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │
│ │ Private Subnets │ │ Private Subnets │ │
│ │ (AZ-a) │ │ (AZ-b) │ │
│ │ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │ │
│ │ │ ECS: API Service │ │ │ │ ECS: API Service │ │ │
│ │ └─────────────────────┘ │ │ └─────────────────────┘ │ │
│ │ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │ │
│ │ │ ECS: Worker Service│ │ │ │ ECS: Worker Service│ │ │
│ │ └─────────────────────┘ │ │ └─────────────────────┘ │ │
│ └─────────────────────────────┘ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │
│ │ Database Subnets │ │ Database Subnets │ │
│ │ (AZ-a) │ │ (AZ-b) │ │
│ │ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │ │
│ │ │ RDS (Primary) │ │ │ │ RDS (Standby) │ │ │
│ │ └─────────────────────┘ │ │ └─────────────────────┘ │ │
│ └─────────────────────────────┘ └─────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘Terraform Structure
terraform/
├── bootstrap/
│ └── create-state-bucket.sh # One-time S3 state bucket bootstrap
├── main.tf # Root module, wires everything together
├── variables.tf # Input variables
├── outputs.tf # Output values
└── ecr.tf # ECR repositories + GitHub OIDCRemote state in S3 with a lockfile. Terraform version ≥ 1.0, AWS provider ~> 6.12.
Module Inventory
| Resource | Module | Status |
|---|---|---|
| VPC | terraform-aws-modules/vpc/aws ~> 6.0 | Deployed |
| S3 (mail + attachments + logs) | terraform-aws-modules/s3-bucket/aws ~> 5.4 | Deployed |
| RDS PostgreSQL | terraform-aws-modules/rds/aws ~> 6.12 | Deployed |
| SQS (inbound + outbound + telemetry + webhooks.fifo) | terraform-aws-modules/sqs/aws ~> 5.0 | Deployed |
| ECS cluster + services | terraform-aws-modules/ecs/aws ~> 6.2 | Deployed |
| ECR repositories | terraform-aws-modules/ecr/aws ~> 2.3 | Deployed |
| ALB | terraform-aws-modules/alb/aws ~> 9.0 | Deployed |
| ACM certificate | aws_acm_certificate resource | Deployed |
| SNS (SES notifications) | aws_sns_topic resources + terraform-aws-modules/sns/aws ~> 6.0 (alarms) | Deployed |
| CloudWatch alarms | terraform-aws-modules/cloudwatch/aws ~> 5.0 | Deployed |
| IAM roles (ECS) | inline via ECS module | Deployed |
| GitHub OIDC provider + role | terraform-aws-modules/iam/aws ~> 6.0 submodules | Deployed |
See
terraform/main.tffor the full module wiring. ALB config is interraform/alb.tf, monitoring interraform/monitoring.tf, SES/SNS interraform/ses.tf, ECR/OIDC interraform/ecr.tf.
Key Resource Configuration
VPC
Two AZs (us-east-1a, us-east-1b) with three subnet tiers: public, private (ECS services), and database (RDS). Single NAT gateway to reduce cost — production can switch to one-per-AZ for HA.
See
terraform/main.tf—module "network"for the full VPC configuration.
RDS (PostgreSQL)
PostgreSQL 17, db.t3.micro for prototype, encrypted at rest, multi-AZ enabled. RDS lives in the database subnet group with no public accessibility. Enhanced monitoring at 60-second intervals.
See
terraform/main.tf—module "mail_rds"for the full RDS configuration.
S3 Buckets
| Bucket | Lifecycle | Encryption | Versioning |
|---|---|---|---|
| Raw email | 365-day expiry | KMS | Enabled |
| Attachments | 730-day expiry | KMS | Enabled |
Both buckets block all public access.
SQS Queues
| Queue | Type | Visibility Timeout | Max Receives | DLQ |
|---|---|---|---|---|
inbound | Standard | 60s | 5 | inbound-dlq |
outbound | Standard | 300s | 3 | outbound-dlq |
telemetry | Standard | 60s | 10 | telemetry-dlq |
webhooks.fifo | FIFO | 60s | 7 | webhooks-dlq.fifo |
The outbound and webhooks.fifo queues use long polling (20s); inbound and telemetry use short polling (0s). All queues have 14-day message retention.
See
terraform/main.tffor per-queue configuration (visibility timeout, max receives, polling).
SES
- Domain Identity — per registered domain, with DKIM signing enabled
- Receipt Rule Set — routes inbound email to S3 + SNS
- Configuration Set — event publishing for delivery/bounce/complaint tracking
- SNS Topics —
inbound_notificationanddelivery_events, subscribed to respective SQS queues
ECS
Uses terraform-aws-modules/ecs/aws for the Fargate cluster with two services:
mailman-api— 256 CPU / 512 MiB, port 8080, private subnetsmailman-worker— 256 CPU / 512 MiB, no exposed ports, private subnets
NOTE
ALB is deployed with HTTPS via ACM certificate (HTTP→HTTPS redirect). API auto-scaling is configured (CPU/memory target tracking). Worker auto-scaling uses step scaling based on SQS queue depth via CloudWatch alarms.
Security Groups
| Group | Ingress | Egress |
|---|---|---|
| ECS | Port 8080 from ALB SG | All |
| RDS | Port 5432 from ECS SG | — |
| ALB | Port 443 from 0.0.0.0/0 | All |
Environments
| Environment | Purpose | Resources |
|---|---|---|
dev | Local development testing | Minimal (single AZ, small instances) |
staging | Pre-production testing | Production-like (multi-AZ, medium instances) |
production | Live traffic | Full HA (multi-AZ, auto-scaling, monitoring) |
Monitoring
- CloudWatch Log Groups — structured JSON logging from both services
- CloudWatch Alarms — deployed: DLQ depth (inbound, outbound, telemetry), queue age (inbound, outbound), ECS task count (API, worker), ALB 5xx error rate, worker auto-scaling (queue depth high/low). All alarms publish to an SNS topic for notification routing. See
terraform/monitoring.tf.
Deployment
Services are deployed as Docker images pushed to ECR via GitHub Actions with OIDC authentication. ECS services pull the latest image on deployment.
GitHub Push → GitHub Actions → Build Docker → Push to ECR → Update ECS ServiceCI/CD
GitHub Actions workflow (.github/workflows/docker.yml) builds and pushes both Docker images to ECR on every push to main and on manual dispatch. Authentication uses GitHub OIDC — no long-lived credentials stored in GitHub secrets. Images are tagged with sha-<git-sha>, main, and latest.
Terraform Provider (TB-553, TB-556)
A Terraform provider for Mailman lives at terraform-provider/ in the repo root. Written in Go using terraform-plugin-framework, scaffolded from openapi.yaml.
Resources: mailman_domain, mailman_inbox, mailman_webhookData sources: mailman_domain, mailman_inbox
Authentication via provider block or MAILMAN_API_URL / MAILMAN_API_TOKEN environment variables.
resource "mailman_domain" "support" {
name = "mail.acme.com"
}
resource "mailman_inbox" "support" {
domain_id = mailman_domain.support.id
local_part = "support"
name = "Support"
}
resource "mailman_webhook" "support_hook" {
url = "https://app.acme.com/webhooks/mailman"
events = ["message.received"]
inboxes = [mailman_inbox.support.id]
secret = var.webhook_secret
}