Deployment Runbook
This runbook deploys Rhumbatron infrastructure and Worker bundles with Terraform.
Principles
Section titled “Principles”- Terraform-only control plane: Terraform alone manages all remote Cloudflare resources, Workers, bindings, and configurations (ADR 0004, ADR 0017). Never deploy with
wrangler deployor with Cloudflare dashboard writes. - Keychain credentials:
scripts/tf.tsloads credentials from the macOS Keychain into environment variables. Never commit secrets to Git or to Terraform state files. - No
-auto-approve: Always make and read the saved plan file before you apply.scripts/tf.tsrefuses the flag.
bun run tf <bootstrap|global|dev|prod> <terraform args...> runs Terraform in one root with that root’s Keychain credentials. The tf:* scripts wrap it.
flowchart LR
b[bun run cf:bundle] --> p[bun run tf:plan:env]
p --> r{Plan as expected?}
r -->|no| fix[Fix source] --> b
r -->|yes| a[bun run tf:apply:env]
a --> s[bun run smoke:env]
s --> d[bun run tf:drift<br/>or tf:drift:prod]
d --> c{Zero changes?}
c -->|no| drift[Stop: find the drift<br/>AGENTS.md 11.7]
c -->|yes| done[Done]
1. Bundle Workers (cf:bundle)
Section titled “1. Bundle Workers (cf:bundle)”Bundle the Worker code locally before you plan or deploy:
bun run cf:bundle- The script compiles all deployables through Turbo:
apps/web,workers/api,workers/coordinator,workers/agents,workers/event-router,workers/integration,workers/indexer, andworkers/sandbox. - It writes
dist/bundles/manifest.jsonwith the bundle file lists and SHA-256 hashes. - Bundles do not depend on the environment. Terraform bindings supply the environment configuration.
2. Deploy Environments
Section titled “2. Deploy Environments”Development (dev)
Section titled “Development (dev)”bun run cf:bundlebun run tf:plan:dev # writes dev.tfplanbun run tf:apply:dev # applies dev.tfplanbun run tf:drift # verifies clean plan for global and devProduction (prod)
Section titled “Production (prod)”bun run cf:bundlebun run tf:plan:prod # writes prod.tfplanbun run tf:apply:prod # applies prod.tfplanbun run tf:drift:prod # verifies clean plan for prodGlobal Singletons (global)
Section titled “Global Singletons (global)”The envs/global root manages account-wide and zone-wide singletons. These are the DNS zone lookup, the zone TLS settings (Strict SSL), and the account budget alerts at 10, 25, and 100 USD (ADR 0019). The prod web Worker does the www to apex redirect. No zone rule does it.
bun run tf global plan -out=global.tfplanbun run tf global apply global.tfplanbun run tf:drift checks global and dev together.
3. Configuration Switches
Section titled “3. Configuration Switches”Set these switches in infrastructure/terraform/envs/dev/main.tf and infrastructure/terraform/envs/prod/main.tf.
bootstrap Switch
Section titled “bootstrap Switch”- Location:
locals { bootstrap = false }inenvs/dev/main.tfandenvs/prod/main.tf. - Purpose: The switch breaks the circular dependency between
coordinator_workerandevent_router_worker(ADR 0005, ADR 0017). In prod, it also selects the first Durable Object migrations. Dev already runs v1, so its migrations stay atv1 -> v1. - First apply on a new environment:
- Set
bootstrap = true. - Plan and apply (
bun run tf:plan:<env>&&bun run tf:apply:<env>).- This apply leaves out the coordinator’s
EVENT_ROUTERservice binding. - Prod: This apply uploads the initial Durable Object migration (
new_tag = "v1"with SQLite classes).
- This apply leaves out the coordinator’s
- Set
bootstrap = false. - Plan and apply again.
- This apply adds the
EVENT_ROUTERlow-latency nudge binding to the coordinator. - Prod: This apply sets the migration steady state to
old_tag = "v1", new_tag = "v1".
- This apply adds the
- Set
attach_apex Switch
Section titled “attach_apex Switch”- Location:
locals { attach_apex = true }inenvs/prod/main.tf. The cutover is complete. The value istrue, and the apex serves the prod web Worker. - Purpose: The switch attaches the apex domains (
rhumbatron.comandwww.rhumbatron.com) to the production web Worker (ADR 0017). - Cutover sequence:
- Keep
attach_apex = falseduring the initial deployment. If a DNS record exists for the hostname, Cloudflare rejects the Worker custom domain. - Delete the legacy DNS records at
rhumbatron.com. - Set
attach_apex = true. - Run the plan, and then apply it. The apply attaches the apex custom domain.
- Keep
4. D1 Migrations Deploy Order
Section titled “4. D1 Migrations Deploy Order”A deployment can include new D1 migrations (migrations/d1/*.sql). The Worker components api_worker and integration_worker depend on terraform_data.d1_migrations, because new Worker code can query new tables or columns.
If the deployment includes new D1 migrations, apply them first. This prevents runtime errors during rollout:
# For dev:bun run tf dev apply -target=terraform_data.d1_migrations
# For prod:bun run tf prod apply -target=terraform_data.d1_migrationsAfter the migrations succeed, run the normal full plan and apply:
bun run tf:plan:dev && bun run tf:apply:dev# or for prod:bun run tf:plan:prod && bun run tf:apply:prod5. Transient HTTP Errors
Section titled “5. Transient HTTP Errors”During a Terraform run, a Cloudflare API call can fail with a transient network or gateway error:
Error: failed to make http request: ...- Cause: A transient HTTP connection reset, a Cloudflare edge timeout, or a short gateway failure.
- Resolution: Do not change resources by hand. Terraform keeps the state of completed resources, and the resources are declarative. Run the plan and apply command again:
bun run tf:plan:dev && bun run tf:apply:dev