vault
22 — Environment Resources
Environment Resources
What each environment is made of, resource by resource. 17-System-Design
shows how the pieces talk to each other; this page is the inventory — counts,
sizes and which flag turns each one on. Everything here comes from one shared
Terraform module, infra/terraform/modules/platform, driven by three sets of
variable values.
The three roots differ in quantity and topology, never in shape. That is deliberate: a control proven in one environment behaves the same in the next, and the only variables are how many of a thing exist and whether it is reachable from the internet.
Dev
Public-subnet single instance. Web and worker share a host, egress goes straight out through the internet gateway, and there is no NAT to pay for.
Beta
Production topology at one of everything, behind an allowlist. Private subnet, NAT, the free S3 gateway endpoint and the full edge — the difference from Prod is redundancy and reach, not architecture.
Two subnets is a floor, not a choice
AWS refuses an ALB with fewer than two subnets in two Availability Zones, and refuses a DB subnet group that does not span two. Beta therefore keeps two public and two database subnets while running its workload in one AZ.
Prod
Everything above, doubled where redundancy matters, plus the pieces that only exist here: a standby database and a cross-region replica. The Co-Signer is not in this stack; see Custody and access.
Inventory
| Resource | Dev | Beta | Prod |
|---|---|---|---|
| VPC | 1 | 1 | 1 + 1 in the DR region |
| Public subnets | 2 | 2 | 2 |
| Private app subnets | — | 1 | 2 |
| Private data subnets | 2 | 2 | 2 (+2 in DR) |
| Internet gateway | 1 | 1 | 1 |
| NAT gateways | — | 1 | 2, one per AZ |
| Interface VPC endpoints | — | none by default | none by default (interface_endpoint_services) |
| S3 gateway endpoint | — | 1 | 1 |
| Load balancer | ALB | ALB | ALB + access logs to S3 |
| App compute | 1 × t4g.small + Elastic IP |
ASG of 1, t4g.small |
ASG 2–4 across 2 AZ, t4g.small |
| Worker compute | same host | ASG of 1 | ASG of 1 |
| Database | db.t4g.small, single-AZ |
db.t4g.small, single-AZ, 30-day PITR |
db.t4g.small, Multi-AZ, 50 GB |
| Read replica | — | — | cross-region, own CMK |
| Security groups | SG-ALB, SG-APP, SG-DB | same (+ SG-VPCE only with interface endpoints) | + SG-REPLICA (+ SG-VPCE only with interface endpoints) |
| WAF | web ACL, rate limit | + managed rules, admin allowlist, site allowlist | + managed rules, admin allowlist |
| KMS keys | 1 | 1 | 1 + 1 in the DR region |
| Secrets | app env, Fireblocks key | same | same |
| CloudWatch alarms | — | set, incl. CPU credits | 9, incl. CPU credits and replica lag |
| SNS topic | — | 1 | 1 |
| Cost budget | optional | optional | monthly |
| Co-Signer | — | shared host in the custody stack | shared host in the custody stack |
What turns each one on
Nothing above is conditional on an environment name. Every difference is a variable, which is why the same module produces all three:
| Variable | Turns on |
|---|---|
compute_mode |
single for one EC2 instance, asg for Auto Scaling Groups |
app_subnet_cidrs |
Private app subnets. One entry pins the workload to a single AZ |
enable_nat_gateway |
NAT per app subnet |
interface_endpoint_services |
Interface endpoints for the listed services; the free S3 gateway exists whenever private subnets do |
db_multi_az |
The standby database instance |
db_replica_region |
The cross-region replica, its DR VPC and its own CMK |
enable_waf_managed_rules, admin_allowed_cidrs |
Managed rule groups and the admin allowlist |
enable_cloudwatch_alarms, alarm_email_addresses |
Alarms and their SNS delivery |
enable_alb_access_logs |
The log bucket and its retention rule |
site_allowed_cidrs |
The site-wide WAF allowlist with exact health, webhook and mail-image exemptions |
container_memory_limit |
The Docker memory cap on fleet containers |
Several of these are fail-closed against each other: private subnets with neither NAT nor endpoints, alarms with no subscriber, a budget with no recipient, or a replica in the same region as its source all stop the plan rather than producing something that looks configured and does nothing.
Custody and access
Two small stacks in the production account support both environments and belong to neither:
| Stack | Resources | Why it is separate |
|---|---|---|
infra/terraform/custody |
Own VPC, one private subnet, one NAT; a c5.xlarge Nitro Enclave host running the Fireblocks Co-Signer; an S3 bucket and KMS key for its state; a security group with no ingress and egress on 443 only |
One Co-Signer serves the beta and live API users. It only makes outbound calls, so it needs no path into either application VPC |
infra/terraform/access |
Own tiny VPC; a t4g.nano Tailscale routing peer with an Elastic IP; a Secrets Manager entry for its setup key |
The one stable address behind Beta's site allowlist and both admin allowlists. Its route covers the AWS eu-north-1 prefixes only, so tester traffic to anything else goes direct |
Related
- 17-System-Design — how these resources interact, per environment
- 12-Environments — environment profiles, DR and what each one proves
- 21-Trust-Boundaries — the same resources grouped by trust instead