feat(infra): opt-in Cloudflare R2 remote-state backend - #167
Merged
Conversation
Closes the last single point of failure in the protection story: state was local-only, so losing the operator's machine/tfstate meant losing the ability to plan, reconcile, or safely destroy the stack — worse now that the VPS carries delete_protection. Adds a pre-wired, opt-in path to Cloudflare R2 (S3-compatible, no egress fees, an account we already use). Default clone stays on local state and works unchanged; enabling remote state is a documented ~2-minute step. - main.tf: commented backend "s3" block pre-filled with the R2-specific flags (region=auto, use_path_style, skip_* preflight, skip_s3_checksum) and use_lockfile for native locking — no DynamoDB. Deployment-specific values (bucket, key, endpoints) come from backend.hcl via partial config. - backend.hcl.example: template for the per-deployment values; credentials stay in AWS_* env vars, never on disk. - .gitignore: ignore backend.hcl, keep the example. - README: "Remote state (optional)" section with the migrate-state steps. Verified on OpenTofu 1.12.0: fmt + validate clean; the s3 backend accepts every argument (a scratch init parsed the full config and only failed at the network layer against a deliberately-invalid endpoint). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The provisioning-with-tofu page lagged behind the infra changes: - "Destroying" told users `tofu destroy` wipes the server — now WRONG, since prevent_destroy + Hetzner delete_protection refuse it. Rewritten to document the deliberate two-step gate-lowering (apply to lift locks, then destroy). - "State management" showed a bare s3 block (bucket/key/region only). Replaced with the pre-wired Cloudflare R2 flow this PR adds: backend.hcl partial config, env-var credentials, use_lockfile, and init -migrate-state. - Prerequisites now call out the OpenTofu 1.12+ requirement. - terraform.tfvars shape documents prevent_server_destroy. - Design choices: updated the state bullet and added a defense-in-depth bullet covering both protection layers. Verified: astro build succeeds, check:rendered-markdown passes, and check:fragments confirms the new #destroying / #state-management / #design-choices anchors resolve. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dings The local pre-push hook runs full-history `gitleaks detect` (CI is path-filtered, so these slipped past on unrelated pushes). Two findings live only in old commit diffs; both current files are already clean: - add-service-to-compose.mdx: a Meilisearch example MEILI_MASTER_KEY value before it became the `<set-from-your-secret-manager>` placeholder. - Auth.session.mutations.utils.test.ts: fixture token `abcdef1234567890`, since changed to `test-challenge-token-stub`. Pin both by fingerprint in .gitleaksignore — narrowly scoped to these exact commits/lines; rules stay active everywhere else and for HEAD. Verified `gitleaks detect` (v8.30.1, the CI-pinned version) now reports no leaks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
Follow-up to #166. That PR hardened the VPS against deletion (OpenTofu
prevent_destroy+ Hetznerdelete_protection/rebuild_protection). The remaining single point of failure was state itself:infra/bootstrap/had nobackendblock, so OpenTofu kept a localterraform.tfstateon the operator's machine. Lose that file and you can no longer plan, reconcile, or safely destroy the stack — made worse now that the server is API-locked and must be reconciled against state to unlock.Approach
infra/bootstrap/is a template others clone, so remote state is opt-in and fully pre-wired — a default clone stays on local state and works unchanged; enabling R2 is a documented ~2-minute step. Backend: Cloudflare R2 (S3-compatible, no egress fees, an account already in use), with native lockfile locking (S3 conditional writes — no DynamoDB).Changes
main.tf— commentedbackend "s3"block pre-filled with the R2-specific flags (region = "auto",use_path_style, theskip_*preflight toggles,skip_s3_checksum) anduse_lockfile = true. Deployment-specific values (bucket,key,endpoints.s3) come frombackend.hclvia partial config.backend.hcl.example— template for the per-deployment values. Credentials stay inAWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEYenv vars, never on disk..gitignore— ignorebackend.hcl, keep the example.README.md— "Remote state (optional)" section with theinit -migrate-statesteps.Enabling (documented in README)
Hetzner Object Storage or AWS S3 work the same way — only
endpoints.s3(and theskip_*/use_path_styleflags, for true AWS) differ.Verification
On a freshly installed OpenTofu 1.12.0:
tofu fmt -check -recursive— clean (and the commented block is fmt-stable when uncommented)tofu init -backend=false+tofu validate— validtofu initwith the full backend block parsed every argument (use_lockfile,endpoints.s3,skip_s3_checksum, etc.) and only failed at the network layer against a deliberately-invalid endpoint — i.e. no "Unsupported argument".Not exercised here (needs real R2 creds + bucket): an end-to-end
init -migrate-stateand a live lock acquire/release. Worth a one-time manual run when you create the bucket.🤖 Generated with Claude Code