diff --git a/.env.example b/.env.example index f96bb86..3c7e4b3 100644 --- a/.env.example +++ b/.env.example @@ -7,3 +7,9 @@ RAILWAY_API_TOKEN= # Cloudflare API token for the aerlinn.net DNS records: Zone.DNS Edit and # Zone.Zone Read, on that zone only. CLOUDFLARE_API_TOKEN= + +# Keys for the tfstate bucket on Railway, where every project's Terraform state +# lives: `railway bucket credentials` in the tfstate project, or the bucket's +# Credentials tab in the dashboard. +TFSTATE_ACCESS_KEY_ID= +TFSTATE_SECRET_ACCESS_KEY= diff --git a/compose.yaml b/compose.yaml index a5d693c..4b25168 100644 --- a/compose.yaml +++ b/compose.yaml @@ -41,6 +41,13 @@ services: terraform: <<: *tool image: hashicorp/terraform:1.16.3 + environment: + - RAILWAY_TOKEN=${RAILWAY_API_TOKEN:-} + - CLOUDFLARE_API_TOKEN + # The S3 backend reads AWS_* names; the bucket is Railway's. + - AWS_ACCESS_KEY_ID=${TFSTATE_ACCESS_KEY_ID:-} + - AWS_SECRET_ACCESS_KEY=${TFSTATE_SECRET_ACCESS_KEY:-} + - HOME=/tmp railway: <<: *tool diff --git a/infra/README.md b/infra/README.md index af72cac..893784d 100644 --- a/infra/README.md +++ b/infra/README.md @@ -32,8 +32,9 @@ Commit `.terraform.lock.hcl`; it pins the provider builds. ## First-time setup 1. **Tokens.** A Railway workspace token (account settings -> Tokens, with the - workspace selected; a project token cannot make projects) and the - Cloudflare token. `cp .env.example .env` and fill both in. + workspace selected; a project token cannot make projects), the Cloudflare + token, and the state bucket's keys (below). `cp .env.example .env` and fill + them in. 2. **Project and domains.** ``` @@ -63,12 +64,30 @@ config-as-code (healthchecks, start commands); Railway's defaults do. ## State -Local: `infra/railway/terraform.tfstate`, gitignored. Back it up. If it is -lost, nothing on Railway goes away; `terraform import` each resource back -(IDs from the dashboard URLs; the Cloudflare records as `/`). +In a bucket on Railway: project `tfstate`, bucket `tfstate` (S3 name +`tfstate-9p20m8qvmqf8gqk-z`, endpoint `https://t3.storageapi.dev`, US East). +Every project keeps its state there under its own key, `/terraform.tfstate`, +with Terraform's lock file next to it. The bucket's keys are `TFSTATE_*` in +`.env`; compose passes them to Terraform as `AWS_*`, the names the S3 backend +reads. + +The bucket is made outside Terraform, once: the provider has no bucket +resource, and a bucket cannot hold the state that creates it. It was made +through the API, since the CLI refuses a workspace token for `init`/`link`: +`projectCreate`, then `bucketCreate`, then `environmentPatchCommit` with +`{buckets: {: {region: "iad", isCreated: true}}}`. + +A new project's stack copies the `backend "s3"` block from +`railway/main.tf` with a new `key`. + +If the state is ever lost, nothing on Railway goes away; `terraform import` +each resource back (IDs from the dashboard URLs; the Cloudflare records as +`/`). ## Cost +The state bucket is $0.015 per GB-month: nothing, at a few KB. + Hobby plan: $5 a month, which includes $5 of usage. The service idles at a few tens of MB and next to no CPU in each environment ($10 per GB-month of memory, $20 per vCPU-month), so both fit in the included usage. Egress is diff --git a/infra/railway/main.tf b/infra/railway/main.tf index 5629028..f7bc31b 100644 --- a/infra/railway/main.tf +++ b/infra/railway/main.tf @@ -4,7 +4,7 @@ # # Terraform makes the project, the service and the domains, and puts their # records on Cloudflare. It never deploys: deploy.sh uploads the site with -# Railway's CLI. State is local (terraform.tfstate here, gitignored). +# Railway's CLI. State is in the tfstate bucket on Railway (infra/README.md). terraform { required_version = ">= 1.10" @@ -12,6 +12,21 @@ terraform { railway = { source = "terraform-community-providers/railway", version = "~> 0.6.2" } cloudflare = { source = "cloudflare/cloudflare", version = "~> 5.25" } } + + # The tfstate project's bucket on Railway, which holds every project's state + # under its own key. S3-compatible, not AWS: hence the skips. Its keys come + # from TFSTATE_* in .env (compose.yaml passes them as AWS_*). + backend "s3" { + bucket = "tfstate-9p20m8qvmqf8gqk-z" + key = "bible/terraform.tfstate" + region = "auto" + endpoints = { s3 = "https://t3.storageapi.dev" } + use_lockfile = true + skip_credentials_validation = true + skip_region_validation = true + skip_requesting_account_id = true + skip_metadata_api_check = true + } } # Reads RAILWAY_TOKEN (compose.yaml sets it from RAILWAY_API_TOKEN in .env):