Skip to content

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

ResourceDescription
AccountDedicated AWS account (mailman)
Regionus-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 OIDC

Remote state in S3 with a lockfile. Terraform version ≥ 1.0, AWS provider ~> 6.12.

Module Inventory

ResourceModuleStatus
VPCterraform-aws-modules/vpc/aws ~> 6.0Deployed
S3 (mail + attachments + logs)terraform-aws-modules/s3-bucket/aws ~> 5.4Deployed
RDS PostgreSQLterraform-aws-modules/rds/aws ~> 6.12Deployed
SQS (inbound + outbound + telemetry + webhooks.fifo)terraform-aws-modules/sqs/aws ~> 5.0Deployed
ECS cluster + servicesterraform-aws-modules/ecs/aws ~> 6.2Deployed
ECR repositoriesterraform-aws-modules/ecr/aws ~> 2.3Deployed
ALBterraform-aws-modules/alb/aws ~> 9.0Deployed
ACM certificateaws_acm_certificate resourceDeployed
SNS (SES notifications)aws_sns_topic resources + terraform-aws-modules/sns/aws ~> 6.0 (alarms)Deployed
CloudWatch alarmsterraform-aws-modules/cloudwatch/aws ~> 5.0Deployed
IAM roles (ECS)inline via ECS moduleDeployed
GitHub OIDC provider + roleterraform-aws-modules/iam/aws ~> 6.0 submodulesDeployed

See terraform/main.tf for the full module wiring. ALB config is in terraform/alb.tf, monitoring in terraform/monitoring.tf, SES/SNS in terraform/ses.tf, ECR/OIDC in terraform/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.tfmodule "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.tfmodule "mail_rds" for the full RDS configuration.

S3 Buckets

BucketLifecycleEncryptionVersioning
Raw email365-day expiryKMSEnabled
Attachments730-day expiryKMSEnabled

Both buckets block all public access.

SQS Queues

QueueTypeVisibility TimeoutMax ReceivesDLQ
inboundStandard60s5inbound-dlq
outboundStandard300s3outbound-dlq
telemetryStandard60s10telemetry-dlq
webhooks.fifoFIFO60s7webhooks-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.tf for 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 Topicsinbound_notification and delivery_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 subnets
  • mailman-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

GroupIngressEgress
ECSPort 8080 from ALB SGAll
RDSPort 5432 from ECS SG
ALBPort 443 from 0.0.0.0/0All

Environments

EnvironmentPurposeResources
devLocal development testingMinimal (single AZ, small instances)
stagingPre-production testingProduction-like (multi-AZ, medium instances)
productionLive trafficFull 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 Service

CI/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.

hcl
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
}