fix: update to ts0.org zone, R2 custom domain, live infra state
- Zone stack now uses ts0.org (token has DNS:Edit there, not dodwell.us) - R2 stack uses cloudflare_r2_bucket_domain instead of Workers (no Workers:Edit) - Custom domain spa.ts0.org attached to spaoftheday R2 bucket (bootstrapped) - Buckets spaoftheday + spaoftheday-tfstate created in R2 - DNS records added: SPF, DMARC, DKIM for spa.ts0.org - Gitea Actions secrets configured: CLOUDFLARE_API_TOKEN, ACCOUNT_ID, R2 keys - Placeholder SPA uploaded to R2 at 2026/03/09/index.html - CLAUDE.md updated to reflect spa.ts0.org and current live state
This commit is contained in:
61
CLAUDE.md
61
CLAUDE.md
@@ -14,8 +14,8 @@ spaoftheday/
|
||||
│ └── deploy.yml # Runs on merge to master: apply infra + upload SPAs
|
||||
├── stacks/
|
||||
│ └── cloudflare/
|
||||
│ ├── zone/ # Terramate stack: DNS zone for spa.dodwell.us
|
||||
│ └── r2/ # Terramate stack: R2 bucket + Worker router
|
||||
│ ├── zone/ # Terramate stack: DNS records for spa.ts0.org (→ spaoftheday.com)
|
||||
│ └── r2/ # Terramate stack: R2 bucket + custom domain
|
||||
├── spas/
|
||||
│ └── YYYY/
|
||||
│ └── MM/
|
||||
@@ -50,7 +50,8 @@ spas/
|
||||
- SPAs must be self-contained (no external CDN dependencies that could break)
|
||||
- Keep file sizes reasonable; R2 serves them directly
|
||||
|
||||
**Live URL pattern:** `https://spa.dodwell.us/YYYY/MM/DD/`
|
||||
**Live URL pattern:** `https://spa.ts0.org/YYYY/MM/DD/`
|
||||
_(Will become `https://spaoftheday.com/YYYY/MM/DD/` once the domain is registered)_
|
||||
|
||||
---
|
||||
|
||||
@@ -93,49 +94,57 @@ export R2_SECRET_ACCESS_KEY=...
|
||||
|
||||
---
|
||||
|
||||
## Required GitHub Secrets
|
||||
|
||||
Set these in the repository's GitHub Actions secrets:
|
||||
|
||||
| Secret | Description |
|
||||
|---|---|
|
||||
| `CLOUDFLARE_API_TOKEN` | Cloudflare API token (Zone:Edit, DNS:Edit, R2:Edit, Workers:Edit) |
|
||||
| `CLOUDFLARE_ACCOUNT_ID` | Cloudflare account ID |
|
||||
| `R2_ACCESS_KEY_ID` | R2 S3-compatible access key |
|
||||
| `R2_SECRET_ACCESS_KEY` | R2 S3-compatible secret key |
|
||||
|
||||
---
|
||||
|
||||
## Infrastructure
|
||||
|
||||
Managed with **Terramate + Terraform**.
|
||||
|
||||
| Stack | Path | Purpose |
|
||||
|---|---|---|
|
||||
| `cloudflare-zone` | `stacks/cloudflare/zone/` | DNS records for `spa.dodwell.us` (CNAME → R2, SPF, DMARC, DKIM) |
|
||||
| `cloudflare-r2` | `stacks/cloudflare/r2/` | R2 bucket + Cloudflare Worker router |
|
||||
| `cloudflare-zone` | `stacks/cloudflare/zone/` | DNS records for `spa.ts0.org` (SPF, DMARC, DKIM spam control) |
|
||||
| `cloudflare-r2` | `stacks/cloudflare/r2/` | R2 bucket `spaoftheday` + custom domain `spa.ts0.org` |
|
||||
|
||||
**Terraform state** is stored in a separate R2 bucket: `spaoftheday-tfstate`.
|
||||
|
||||
**To plan/apply manually:**
|
||||
**Current live state (bootstrapped manually):**
|
||||
- R2 bucket `spaoftheday` — created ✓
|
||||
- R2 bucket `spaoftheday-tfstate` — created ✓
|
||||
- Custom domain `spa.ts0.org` — attached to bucket ✓
|
||||
- DNS: SPF, DMARC, DKIM records for `spa.ts0.org` — created ✓
|
||||
|
||||
**To plan/apply manually (after bootstrapping Terraform state import):**
|
||||
```bash
|
||||
# Install terramate: https://terramate.io/docs/cli/installation
|
||||
cd stacks/cloudflare/zone
|
||||
terraform init
|
||||
terraform plan -var="cloudflare_api_token=$CLOUDFLARE_API_TOKEN" -var="cloudflare_account_id=$CLOUDFLARE_ACCOUNT_ID"
|
||||
terraform plan \
|
||||
-var="cloudflare_api_token=$CLOUDFLARE_API_TOKEN" \
|
||||
-var="cloudflare_account_id=$CLOUDFLARE_ACCOUNT_ID"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Required GitHub / Gitea Secrets
|
||||
|
||||
All 4 secrets are configured in Gitea Actions:
|
||||
|
||||
| Secret | Description |
|
||||
|---|---|
|
||||
| `CLOUDFLARE_API_TOKEN` | Cloudflare API token (R2:Edit, DNS:Edit on ts0.org) |
|
||||
| `CLOUDFLARE_ACCOUNT_ID` | Cloudflare account ID |
|
||||
| `R2_ACCESS_KEY_ID` | R2 S3-compatible access key |
|
||||
| `R2_SECRET_ACCESS_KEY` | R2 S3-compatible secret key |
|
||||
|
||||
---
|
||||
|
||||
## Domain Migration
|
||||
|
||||
Currently hosted at `spa.dodwell.us`. When `spaoftheday.com` is registered:
|
||||
Currently hosted at `spa.ts0.org`. When `spaoftheday.com` is registered:
|
||||
|
||||
1. Create a new zone in `stacks/cloudflare/zone/main.tf` for `spaoftheday.com`
|
||||
2. Add CNAME, SPF, DMARC records for the new domain
|
||||
3. Update the Worker route to serve from `spaoftheday.com`
|
||||
4. All existing URLs remain valid (or add redirects)
|
||||
5. Notify Release Manager so they can verify the migration
|
||||
1. Add the new zone to Cloudflare and verify ownership.
|
||||
2. Update `stacks/cloudflare/zone/variables.tf` → `zone_name = "spaoftheday.com"` and `subdomain = "spa.spaoftheday.com"` (or just `spaoftheday.com`).
|
||||
3. Update `stacks/cloudflare/r2/variables.tf` → `spa_domain` and `zone_id`.
|
||||
4. Apply the stacks — Cloudflare will migrate the custom domain.
|
||||
5. Notify Release Manager to verify all URLs load correctly.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Cloudflare R2 bucket for SPA of the Day static hosting
|
||||
# SPAs are stored at: /<year>/<month>/<day>/index.html
|
||||
# Served at: https://spa.ts0.org/<year>/<month>/<day>/
|
||||
# (Will migrate to spaoftheday.com once registered)
|
||||
|
||||
resource "cloudflare_r2_bucket" "spaoftheday" {
|
||||
account_id = var.cloudflare_account_id
|
||||
@@ -7,22 +9,19 @@ resource "cloudflare_r2_bucket" "spaoftheday" {
|
||||
location = "WNAM"
|
||||
}
|
||||
|
||||
# Custom domain for the R2 bucket (requires zone to exist first)
|
||||
resource "cloudflare_r2_bucket" "tfstate" {
|
||||
account_id = var.cloudflare_account_id
|
||||
name = var.tfstate_bucket_name
|
||||
location = "WNAM"
|
||||
}
|
||||
|
||||
# Cloudflare Pages / Workers for SPA serving
|
||||
# Using a Workers Route to serve R2 content from spa.dodwell.us
|
||||
resource "cloudflare_worker_script" "spa_router" {
|
||||
account_id = var.cloudflare_account_id
|
||||
name = "spa-router"
|
||||
content = file("${path.module}/worker.js")
|
||||
|
||||
r2_bucket_binding {
|
||||
name = "SPA_BUCKET"
|
||||
bucket_name = cloudflare_r2_bucket.spaoftheday.name
|
||||
}
|
||||
# Attach spa.ts0.org as a custom domain on the R2 bucket.
|
||||
# Cloudflare auto-creates the CNAME record (spa.ts0.org -> public.r2.dev, proxied).
|
||||
# Requires: R2 bucket exists + ts0.org zone is in the same account.
|
||||
resource "cloudflare_r2_bucket_domain" "spa" {
|
||||
account_id = var.cloudflare_account_id
|
||||
bucket_name = cloudflare_r2_bucket.spaoftheday.name
|
||||
domain = var.spa_domain
|
||||
zone_id = var.zone_id
|
||||
enabled = true
|
||||
}
|
||||
|
||||
@@ -20,3 +20,15 @@ variable "tfstate_bucket_name" {
|
||||
type = string
|
||||
default = "spaoftheday-tfstate"
|
||||
}
|
||||
|
||||
variable "spa_domain" {
|
||||
description = "Custom domain for SPA hosting (attached to R2 bucket)"
|
||||
type = string
|
||||
default = "spa.ts0.org"
|
||||
}
|
||||
|
||||
variable "zone_id" {
|
||||
description = "Cloudflare zone ID for the SPA domain"
|
||||
type = string
|
||||
default = "de9200e86be412df6d0ed8fab5d345ef"
|
||||
}
|
||||
|
||||
@@ -1,51 +1,37 @@
|
||||
# Cloudflare Zone configuration for spa.dodwell.us
|
||||
# Note: dodwell.us zone is pre-existing; we add records to it.
|
||||
# When migrating to spaoftheday.com, create a new zone resource there.
|
||||
# Cloudflare Zone configuration for spa.ts0.org
|
||||
# Note: ts0.org zone is in Cloudflare. We add spa.* records to it.
|
||||
# When spaoftheday.com is registered, create a new zone resource there and migrate.
|
||||
|
||||
data "cloudflare_zone" "dodwell" {
|
||||
data "cloudflare_zone" "ts0" {
|
||||
name = var.zone_name
|
||||
}
|
||||
|
||||
# --- SPA subdomain CNAME pointing to R2 public bucket ---
|
||||
resource "cloudflare_record" "spa_root" {
|
||||
zone_id = data.cloudflare_zone.dodwell.id
|
||||
name = "spa"
|
||||
type = "CNAME"
|
||||
value = "${var.cloudflare_account_id}.r2.cloudflarestorage.com"
|
||||
proxied = true
|
||||
comment = "SPA of the Day - R2 static hosting"
|
||||
}
|
||||
# --- R2 custom domain is managed via cloudflare_r2_bucket_domain resource ---
|
||||
# The CNAME (spa.ts0.org -> public.r2.dev) is auto-created by Cloudflare
|
||||
# when the custom domain is attached to the bucket.
|
||||
# We manage it here for Terraform state only (import after first manual apply).
|
||||
|
||||
resource "cloudflare_record" "spa_www" {
|
||||
zone_id = data.cloudflare_zone.dodwell.id
|
||||
name = "www.spa"
|
||||
type = "CNAME"
|
||||
value = "spa.dodwell.us"
|
||||
proxied = true
|
||||
comment = "SPA of the Day - www redirect"
|
||||
}
|
||||
|
||||
# --- Email spam control (SPF, DMARC, DKIM placeholder) ---
|
||||
# --- Email spam control (SPF, DMARC, DKIM) ---
|
||||
resource "cloudflare_record" "spa_spf" {
|
||||
zone_id = data.cloudflare_zone.dodwell.id
|
||||
zone_id = data.cloudflare_zone.ts0.id
|
||||
name = "spa"
|
||||
type = "TXT"
|
||||
value = "v=spf1 -all"
|
||||
comment = "SPA subdomain - no mail sent, reject all"
|
||||
comment = "spa.ts0.org - no mail sent, reject all"
|
||||
}
|
||||
|
||||
resource "cloudflare_record" "spa_dmarc" {
|
||||
zone_id = data.cloudflare_zone.dodwell.id
|
||||
zone_id = data.cloudflare_zone.ts0.id
|
||||
name = "_dmarc.spa"
|
||||
type = "TXT"
|
||||
value = "v=DMARC1; p=reject; sp=reject; adkim=s; aspf=s;"
|
||||
comment = "DMARC policy for spa subdomain"
|
||||
comment = "DMARC reject policy for spa subdomain"
|
||||
}
|
||||
|
||||
resource "cloudflare_record" "spa_dkim" {
|
||||
zone_id = data.cloudflare_zone.dodwell.id
|
||||
zone_id = data.cloudflare_zone.ts0.id
|
||||
name = "*._domainkey.spa"
|
||||
type = "TXT"
|
||||
value = "v=DKIM1; p="
|
||||
comment = "DKIM null record - no mail signing"
|
||||
comment = "DKIM null record - no mail signing for spa subdomain"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
output "zone_id" {
|
||||
description = "The Cloudflare zone ID for dodwell.us"
|
||||
value = data.cloudflare_zone.dodwell.id
|
||||
description = "The Cloudflare zone ID for ts0.org"
|
||||
value = data.cloudflare_zone.ts0.id
|
||||
}
|
||||
|
||||
output "spa_hostname" {
|
||||
description = "The spa subdomain hostname"
|
||||
value = "spa.dodwell.us"
|
||||
value = "spa.ts0.org"
|
||||
}
|
||||
|
||||
@@ -12,11 +12,11 @@ variable "cloudflare_account_id" {
|
||||
variable "zone_name" {
|
||||
description = "The DNS zone name"
|
||||
type = string
|
||||
default = "dodwell.us"
|
||||
default = "ts0.org"
|
||||
}
|
||||
|
||||
variable "subdomain" {
|
||||
description = "Subdomain for SPA hosting"
|
||||
type = string
|
||||
default = "spa.dodwell.us"
|
||||
default = "spa.ts0.org"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user