terraform state: backup, dedicated bucket, second copy on OVH, encryption #220

Open
opened 2026-10-16 11:40:30 -04:00 by jfraeys · 4 comments
Owner

Review of how Terraform state is managed (2026-10-16). The basics are sound; the gaps are backup and blast radius.

Current

  • S3 backend, Linode Object Storage, bucket blizzard.jfraeys.com, key infra/terraform.tfstate, workspaces for environments.
  • Native lockfile locking (use_lockfile), verified live 2026-08-26; ./infra unlock for stuck locks.
  • CI uses its own bucket-scoped key (CI_STATE_BACKEND_*).
  • OpenTofu 1.13.1; prevent_destroy on production nodes.

Gaps (most important first)

  1. No backup of the state object. docs/runbooks/node-failure.md: "the repo has no separate backup of the state object, so snapshot it by hand." Bucket versioning: unknown, not checked.
  2. Shared blast radius. The state shares a bucket with the primary restic repo (infra-backup). Linode keys scope to the bucket, not a prefix, so the CI staging key can reach prod state and the backups. Unverified: whether that key is read-write.
  3. State is not encrypted client-side. It holds generated passwords and tokens. OpenTofu has native state encryption (1.7+).
  4. One backend for all workspaces. Prod and staging differ only by workspace name.
  5. State depends on Linode while production moves to OVH (#187): a Linode outage blocks plan/apply.

Plan, in order

  1. Check bucket versioning; turn it on if off.
  2. Dedicated state bucket with its own key; migrate with tofu init -migrate-state; update CI key provisioning (scripts/secrets/provision-ci-state-key.py).
  3. Scheduled tofu state pull snapshot into restic (both repos), with a check that alerts when it is stale.
  4. A second copy of the state in OVH Object Storage, refreshed on a schedule (removes the Linode dependency for recovery without moving the backend).
  5. Decide on OpenTofu state encryption (key management, recovery with the age recovery key).
  6. Last: separate backends for prod and staging.

#187 (OVH migration, DR drill: confirm the state copy is current when the drill runs), docs/runbooks/node-failure.md.

Review of how Terraform state is managed (2026-10-16). The basics are sound; the gaps are backup and blast radius. ## Current - S3 backend, Linode Object Storage, bucket `blizzard.jfraeys.com`, key `infra/terraform.tfstate`, workspaces for environments. - Native lockfile locking (`use_lockfile`), verified live 2026-08-26; `./infra unlock` for stuck locks. - CI uses its own bucket-scoped key (`CI_STATE_BACKEND_*`). - OpenTofu 1.13.1; `prevent_destroy` on production nodes. ## Gaps (most important first) 1. **No backup of the state object.** `docs/runbooks/node-failure.md`: "the repo has no separate backup of the state object, so snapshot it by hand." Bucket versioning: unknown, not checked. 2. **Shared blast radius.** The state shares a bucket with the primary restic repo (`infra-backup`). Linode keys scope to the bucket, not a prefix, so the CI staging key can reach prod state and the backups. Unverified: whether that key is read-write. 3. **State is not encrypted client-side.** It holds generated passwords and tokens. OpenTofu has native state encryption (1.7+). 4. **One backend for all workspaces.** Prod and staging differ only by workspace name. 5. **State depends on Linode** while production moves to OVH (#187): a Linode outage blocks plan/apply. ## Plan, in order 1. Check bucket versioning; turn it on if off. 2. Dedicated state bucket with its own key; migrate with `tofu init -migrate-state`; update CI key provisioning (`scripts/secrets/provision-ci-state-key.py`). 3. Scheduled `tofu state pull` snapshot into restic (both repos), with a check that alerts when it is stale. 4. A second copy of the state in OVH Object Storage, refreshed on a schedule (removes the Linode dependency for recovery without moving the backend). 5. Decide on OpenTofu state encryption (key management, recovery with the age recovery key). 6. Last: separate backends for prod and staging. ## Related #187 (OVH migration, DR drill: confirm the state copy is current when the drill runs), `docs/runbooks/node-failure.md`.
Author
Owner

Design for step 3 (scheduled state snapshot): fetch the state objects (infra/terraform.tfstate and env://infra/terraform.tfstate) with curl --aws-sigv4 into /var/lib/infra-backups/terraform-state/ on the services host; the existing restic run already backs that directory up to both repos, so the primary and OVH/Linode secondary both get a copy with no new dependency. A failed fetch marks the backup DEGRADED. Roles change: staging, promotion, then ./infra deploy --tags backups (not on the new OVH node before cutover). Note for step 2: the backup credential and the state backend credential are currently the same Linode key.

Design for step 3 (scheduled state snapshot): fetch the state objects (infra/terraform.tfstate and env:/<workspace>/infra/terraform.tfstate) with curl --aws-sigv4 into /var/lib/infra-backups/terraform-state/ on the services host; the existing restic run already backs that directory up to both repos, so the primary and OVH/Linode secondary both get a copy with no new dependency. A failed fetch marks the backup DEGRADED. Roles change: staging, promotion, then ./infra deploy --tags backups (not on the new OVH node before cutover). Note for step 2: the backup credential and the state backend credential are currently the same Linode key.
Author
Owner

Update: step 3 is done and verified. The state snapshot runs in every backup (6 workspace files, mode 0600, in both restic repos); the first deploy failed with 403 on Debian 12's curl 7.88 (missing x-amz-content-sha256) and was fixed in #223. Bucket versioning is off (checked read-only). Costs checked: negligible on both providers. Next: dedicated state bucket, OVH copy, encryption decision.

Update: step 3 is done and verified. The state snapshot runs in every backup (6 workspace files, mode 0600, in both restic repos); the first deploy failed with 403 on Debian 12's curl 7.88 (missing x-amz-content-sha256) and was fixed in #223. Bucket versioning is off (checked read-only). Costs checked: negligible on both providers. Next: dedicated state bucket, OVH copy, encryption decision.
Author
Owner

Status 2026-10-16: plan step 3 (scheduled tofu state pull snapshot into restic) is merged (#222, fix #223) and enabled by backups_state_snapshot_enabled: true in inventory/host_vars/services.yml. It reaches production on the next --tags backups deploy and must not run on the new OVH node before cutover. Steps 1, 2, 4, 5, 6 are open; step 4 (second copy on OVH) waits for the OVH primary-repo flip in #187.

Status 2026-10-16: plan step 3 (scheduled `tofu state pull` snapshot into restic) is merged (#222, fix #223) and enabled by `backups_state_snapshot_enabled: true` in `inventory/host_vars/services.yml`. It reaches production on the next `--tags backups` deploy and must not run on the new OVH node before cutover. Steps 1, 2, 4, 5, 6 are open; step 4 (second copy on OVH) waits for the OVH primary-repo flip in #187.
Author
Owner

Status 2026-10-17: backups can now pick the provider per repo (#268), which the planned second state copy on OVH (step 4) builds on. State snapshot into restic (step 3) is deployed as before; steps 1, 2, 5, 6 are still open.

Status 2026-10-17: backups can now pick the provider per repo (#268), which the planned second state copy on OVH (step 4) builds on. State snapshot into restic (step 3) is deployed as before; steps 1, 2, 5, 6 are still open.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jfraeys/infra#220
No description provided.