Skip to content

Commit 1783cd8

Browse files
authored
refactor(infra): Terraform edge and hybrid stacks (#2137)
1 parent a082d11 commit 1783cd8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

87 files changed

+1374
-882
lines changed

.gitignore

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,21 +41,6 @@ node_modules/
4141
# https://developers.cloudflare.com/workers/wrangler/
4242
.wrangler/
4343

44-
# Terraform
45-
# https://github.com/github/gitignore/blob/main/Terraform.gitignore
46-
*.tfstate
47-
*.tfstate.*
48-
.terraform/
49-
.terraform.lock.hcl
50-
*.tfvars
51-
*.tfvars.json
52-
override.tf
53-
override.tf.json
54-
*_override.tf
55-
*_override.tf.json
56-
.terraformrc
57-
terraform.rc
58-
5944
# Astro
6045
.astro/
6146

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ This starter kit uses a thoughtfully organized monorepo structure that promotes
9191

9292
## Prerequisites
9393

94-
- [Bun](https://bun.sh/) v1.2+ (replaces Node.js and npm)
94+
- [Bun](https://bun.sh/) v1.3+ (replaces Node.js and npm)
9595
- [VS Code](https://code.visualstudio.com/) with our [recommended extensions](.vscode/extensions.json)
9696
- [React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en) browser extension (recommended)
9797
- [Cloudflare account](https://dash.cloudflare.com/sign-up) for deployment

docs/specs/infra-terraform.md

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
# Infrastructure Terraform Specification
2+
3+
## Overview
4+
5+
Two deployment stacks with clear separation of concerns.
6+
7+
**Non-goals:** Multi-region orchestration, blue-green deployments, auto-scaling policies. These belong in CI/CD or dedicated tooling.
8+
9+
| Stack | Components | Use Case |
10+
| ------------------- | ------------------------------------------- | ----------------------- |
11+
| **edge** (default) | Hyperdrive, DNS (Workers via Wrangler) | Most SaaS apps |
12+
| **hybrid** (opt-in) | Cloud Run, Cloud SQL, GCS + optional CF DNS | GCP services, Vertex AI |
13+
14+
## Directory Structure
15+
16+
```bash
17+
infra/
18+
modules/ # Atomic resources (no credentials)
19+
cloudflare/
20+
hyperdrive/ # Database connection pooling
21+
r2-bucket/ # Object storage
22+
dns/ # Proxied DNS records
23+
gcp/
24+
cloud-run/ # Container deployment
25+
cloud-sql/ # Managed PostgreSQL
26+
gcs/ # Object storage
27+
28+
stacks/ # Architectural compositions
29+
edge/ # Hyperdrive + DNS (Workers via Wrangler)
30+
hybrid/ # GCP + optional CF DNS
31+
32+
envs/ # Terraform roots (providers + backend + state)
33+
dev/edge/
34+
preview/edge/
35+
staging/edge/
36+
prod/edge/
37+
38+
templates/
39+
env-roots/hybrid/ # Copy to enable hybrid
40+
backend-r2.example.hcl # Remote state for edge
41+
backend-gcs.example.hcl # Remote state for hybrid
42+
```
43+
44+
## Module Contract
45+
46+
Modules must NOT define `provider` blocks. Non-HashiCorp providers require `required_providers` to specify the source:
47+
48+
```hcl
49+
# Cloudflare modules declare source only (no version):
50+
terraform {
51+
required_providers {
52+
cloudflare = {
53+
source = "cloudflare/cloudflare"
54+
}
55+
}
56+
}
57+
```
58+
59+
Version constraints live exclusively in env roots. This keeps modules reusable while centralizing version management.
60+
61+
## Provider Versions
62+
63+
Canonical versions (single source of truth):
64+
65+
| Provider | Version |
66+
| ---------- | -------------- |
67+
| terraform | `>= 1.12, < 2` |
68+
| cloudflare | `~> 5.0` |
69+
| google | `~> 7.0` |
70+
71+
## Design Decisions
72+
73+
### Explicit Roots Over Dispatcher
74+
75+
Each `(environment, stack)` pair gets its own Terraform root with isolated state.
76+
77+
```bash
78+
terraform -chdir=infra/envs/prod/edge apply
79+
```
80+
81+
**Why not a dispatcher?** A `variable "stack"` that switches configs:
82+
83+
- Destroys one stack when switching to another
84+
- Requires separate backends anyway
85+
- Creates awkward `module.edge[0].x` references
86+
87+
### No Backend by Default
88+
89+
Terraform uses local state when no backend is configured. Remote backends require pre-existing buckets and credentials.
90+
91+
**Rationale:** Zero-friction onboarding. Add remote backend when ready for team collaboration.
92+
93+
### Providers in Env Roots Only
94+
95+
Only env roots define `provider` blocks with credentials. Modules declare `required_providers` for source resolution only (no versions, no credentials).
96+
97+
**Rationale:** Keeps modules reusable. Version constraints and credentials stay in one place per environment.
98+
99+
### Preview Uses Edge Only
100+
101+
PR previews need fast spin-up and low cost. Cloudflare Workers: no cold starts, instant deploys, minimal cost.
102+
103+
## Secrets
104+
105+
```bash
106+
# Via environment variables (CI/CD)
107+
export TF_VAR_cloudflare_api_token="..."
108+
terraform -chdir=infra/envs/prod/edge apply
109+
110+
# Or local terraform.tfvars (gitignored)
111+
```
112+
113+
Mark sensitive variables:
114+
115+
```hcl
116+
variable "cloudflare_api_token" {
117+
type = string
118+
sensitive = true
119+
}
120+
```
121+
122+
## Switching to Remote Backend
123+
124+
### Edge Stack (R2)
125+
126+
```bash
127+
cp infra/templates/backend-r2.example.hcl infra/envs/prod/edge/backend.hcl
128+
terraform -chdir=infra/envs/prod/edge init -backend-config=backend.hcl -migrate-state
129+
```
130+
131+
### Hybrid Stack (GCS)
132+
133+
```bash
134+
cp infra/templates/backend-gcs.example.hcl infra/envs/prod/hybrid/backend.hcl
135+
terraform -chdir=infra/envs/prod/hybrid init -backend-config=backend.hcl -migrate-state
136+
```
137+
138+
## Multi-Region
139+
140+
Use separate roots: `envs/prod-eu/edge`, `envs/prod-us/edge`. Each manages its own state.
141+
142+
## Naming Conventions
143+
144+
### Resource values
145+
146+
Cloud resources use `{project_slug}-{environment}`; lowercase alphanumeric and hyphens only: `^[a-z0-9-]+$`.
147+
148+
### Resource identifiers
149+
150+
One simple set of rules:
151+
152+
1. Name the thing being created (provider-native noun, singular).
153+
154+
```hcl
155+
resource "cloudflare_hyperdrive_config" "hyperdrive" {}
156+
resource "cloudflare_r2_bucket" "bucket" {}
157+
resource "cloudflare_dns_record" "record" {}
158+
resource "google_cloud_run_v2_service" "service" {}
159+
resource "google_sql_database_instance" "instance" {}
160+
```
161+
162+
2. If you have multiples, suffix with the role.
163+
164+
```hcl
165+
resource "cloudflare_r2_bucket" "uploads" {}
166+
resource "cloudflare_r2_bucket" "backups" {}
167+
```
168+
169+
3. Module names describe architectural role; resource names describe the concrete thing.
170+
171+
```hcl
172+
module "hyperdrive" {
173+
# contains: cloudflare_hyperdrive_config.hyperdrive
174+
}
175+
# → module.hyperdrive.id
176+
```
177+
178+
## Known Limitations
179+
180+
### Hyperdrive Database URL Parsing
181+
182+
The hyperdrive module parses `database_url` via regex to extract individual connection parameters. This works reliably with Neon URLs (which use URL-safe generated credentials) but has limitations:
183+
184+
- Port must be explicitly specified (e.g., `:5432`)
185+
- Credentials must not contain unencoded `@` or `:` characters
186+
- Validation fails fast with a descriptive error message
187+
188+
For non-Neon databases with special characters in credentials, consider modifying the module to accept individual connection parameters instead.

infra/.gitignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.terraform/
2+
*.tfstate
3+
*.tfstate.*
4+
*.tfvars
5+
*.tfvars.json
6+
backend.hcl
7+
override.tf
8+
override.tf.json
9+
*_override.tf
10+
*_override.tf.json
11+
.terraform.tfstate.lock.info
12+
crash.log
13+
crash.*.log

0 commit comments

Comments
 (0)