Move terraform state to sops-git; add public assets bucket - #6
Open
MasonEgger wants to merge 1 commit into
Open
Conversation
Retire the self-referential DO Spaces state bucket in favor of sops-encrypted state committed to the repo, and repurpose the $5/mo Spaces spend toward a public web-assets bucket. Why: a whole Spaces bucket plus the chicken-and-egg bootstrap dance (backend.tf.disabled, init -migrate-state, a second credential type, prevent_destroy + a destroy-ordering caveat) was a lot of machinery to hold one small state file. DO Spaces bills a flat $5/mo per account, so once we wanted a public assets bucket anyway, keeping tfstate in Spaces saved nothing. Moving state to git-sops removes the bootstrap dance entirely, and the $5 now buys asset hosting instead of one file. State: - terraform/state.sops.json holds the state, whole-file age-encrypted. - The `_tf` wrapper in bootstrap/justfile decrypts before each terraform run and re-encrypts in place only when the plaintext changed, so reads don't churn git; it also re-encrypts on a non-zero exit so a partial apply's state is never lost. All terraform recipes route through it. - No remote backend, no locking (fine for a solo/coordinated operator). - .sops.yaml rule + gitleaks allowlist added; gitignore keeps the plaintext working file out and the encrypted file in. Assets bucket: - digitalocean_spaces_bucket.assets (pytexas-assets), public read via a bucket policy granting anonymous s3:GetObject (no bucket listing), writes gated by the Spaces key, plus a CORS config for cross-site embedding. Verified: key-gated PUT, anonymous GET. Also: - backups: DO no longer honors the bare `backups = true` toggle, so add a backup_policy block (weekly) to stop a perpetual plan diff. - ansible day-2: `_ansible` auto-detects the SSH user (pytexas on a hardened droplet, root on a fresh one), since the bootstrap role disables root login after the first apply; inventory defaults to pytexas. - secrets: rotate the DO API token, drop the backend-only AWS_* mirror creds (keep TF_VAR_do_token + SPACES_*). - docs: rewrite CLAUDE.md, README, terraform/README, secrets/README to describe the sops-state workflow and assets bucket instead of the self-referential bucket and bootstrap dance. Validated end to end with a full destroy + fresh redeploy (nothing in prod): terraform stands up droplet, firewall, DNS, project, and assets bucket with no bootstrap dance. The droplet's ansible provisioning was not run from here (a local ansible-core/community.general mismatch in this environment, not a repo defect); the droplet is up and reachable, so `just apply` from a working controller finishes it.
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.
What this does
Retires the self-referential DO Spaces state bucket in favor of sops-encrypted state committed to the repo, and adds a public DO Spaces web-assets bucket in its place.
Why
DO Spaces bills a flat $5/mo per account (not per bucket). A whole bucket plus the bootstrap dance (
backend.tf.disabled,init -migrate-state, a second credential type,prevent_destroy+ a destroy-ordering caveat) was a lot of machinery for one small state file. Once we wanted a public assets bucket anyway, keeping tfstate in Spaces saved nothing. So: state moves to git-sops (kills the whole bootstrap dance), and the $5 now buys asset hosting.State: sops-encrypted in git
terraform/state.sops.json, whole-file age-encrypted._tfwrapper inbootstrap/justfiledecrypts before each run and re-encrypts only if the state changed (no git churn on reads, no new nonce per plan) and even on non-zero terraform exit (partial-apply state survives). All terraform recipes route through it..sops.yamlrule + gitleaks allowlist added; gitignore keeps the plaintext working file out, the encrypted file in.terraform/state.sops.jsonafter any apply that changes state (the wrapper prints a reminder).Public assets bucket
digitalocean_spaces_bucket.assets(pytexas-assets): public read via a bucket policy (anonymouss3:GetObject, no listing), writes gated by the Spaces key, CORS for cross-site embedding.https://pytexas-assets.sfo3.digitaloceanspaces.com/<key>.Also in here
backup_policyblock; DO no longer honors the barebackups = truetoggle (was a perpetual plan diff). Confirmed backups now enabled._ansibleauto-detects the SSH user (pytexason a hardened droplet,rooton a fresh one), since the bootstrap role disables root login after the first apply; inventory default flipped topytexas.AWS_*mirror creds (keptTF_VAR_do_token+SPACES_*).Validation
Full destroy + fresh redeploy (nothing in prod): terraform stands up droplet, firewall, DNS, project, and the assets bucket with no bootstrap dance.
just planis clean.Not done here
The droplet's ansible provisioning wasn't run from the work environment (a local
ansible-core/community.generalcallback mismatch there, not a repo defect). The droplet is up and reachable, sojust applyfrom a working controller finishes provisioning.Reviewer notes
terraform initonce, thenjust apply. No backend setup.state rm+importaround it.