Replace the gitignored plaintext infra/tofu/.r2.env and backend.hcl with a single committed, SOPS/age-encrypted dotenv (secrets.sops.env) plus a thin wrapper (tf.sh), so tofu runs with no manual secret entry and secrets live encrypted in git like the repo's other secrets. - .sops.yaml: creation rule encrypting infra/tofu/secrets.sops.env (all values) to the existing age recipient. - secrets.sops.env: encrypted, PLACEHOLDER values only. Holds R2 S3 keys, the state-encryption passphrase (TF_STATE_PASSPHRASE scalar), and the Proxmox API token/endpoint. Operator fills real values via `sops`. - tf.sh: sops exec-env decrypts the dotenv, exports PROXMOX_VE_*/AWS_*, builds the multi-line TF_ENCRYPTION HCL from TF_STATE_PASSPHRASE, and runs tofu (injecting -backend-config=backend.r2.hcl on init). Proxmox token stays env-only, never a tofu variable/state. - backend.r2.hcl: committed non-secret R2 bucket/key/endpoint (account id is not a credential), passed to `tofu init`. - Retire .r2.env/backend.hcl(.example): drop the committed templates; keep the gitignore lines as a safety net for leftover local plaintext. - Update backend.tf/README/REMOTE-STATE for the new day-2 flow and DR story (repo + age key is sufficient). Verified: placeholder file round-trips sops -e/-d with the repo age key; tf.sh exports the expected VAR NAMES and builds valid TF_ENCRYPTION HCL (stub tofu, no real plan/secrets). Co-Authored-By: Claude Opus 4.8 <[email protected]>
40 lines
2.3 KiB
Terraform
40 lines
2.3 KiB
Terraform
# Remote state — Cloudflare R2 (S3-compatible), stored OFF the k3s cluster.
|
|
#
|
|
# WHY OFF-CLUSTER (the load-bearing decision): tofu state must live at or below the layer it
|
|
# manages. Its dependency chain is Proxmox host -> k3s VMs -> k3s cluster -> Gitea/apps, so state
|
|
# in Gitea (top of the chain) would be UNREADABLE in exactly the disaster it exists for — cluster
|
|
# gone, `tofu apply` to rebuild. R2 sits entirely outside that chain: a full cluster rebuild can
|
|
# still read state. It also survives total-site loss (off-site) and gives multi-machine access.
|
|
#
|
|
# SECRETS: driven by infra/tofu/tf.sh, which decrypts the COMMITTED, SOPS/age-encrypted
|
|
# secrets.sops.env and exports AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY (R2 S3 keys) plus builds
|
|
# the TF_ENCRYPTION blob from the single TF_STATE_PASSPHRASE. The non-secret bucket/key/endpoint
|
|
# live in the committed backend.r2.hcl (an R2 account id is not a credential). State bytes are
|
|
# encrypted at rest via TF_ENCRYPTION (see REMOTE-STATE.md) — the passphrase lives ONLY inside the
|
|
# encrypted secrets.sops.env (and your password manager), never in git plaintext or in the state.
|
|
#
|
|
# INIT (run via the wrapper, inside the dev container — non-secret bits from backend.r2.hcl,
|
|
# secret bits decrypted from secrets.sops.env):
|
|
# ./infra/devcontainer/dev.sh run ./infra/tofu/tf.sh init
|
|
#
|
|
# NOTE: this `terraform {}` block is MERGED with the one in versions.tf (required_version +
|
|
# required_providers). OpenTofu allows multiple terraform blocks across files; `backend` appears once.
|
|
terraform {
|
|
backend "s3" {
|
|
region = "auto" # R2 ignores region, but the S3 backend requires a value.
|
|
|
|
# R2 is S3-compatible but not AWS: disable the AWS-only preflight/validation paths.
|
|
skip_credentials_validation = true
|
|
skip_region_validation = true
|
|
skip_requesting_account_id = true
|
|
skip_metadata_api_check = true
|
|
skip_s3_checksum = true # R2 rejects AWS trailing (streaming) checksums.
|
|
|
|
# bucket, key, endpoints{ s3 } -> committed backend.r2.hcl, passed by tf.sh via -backend-config.
|
|
#
|
|
# use_lockfile = true -> ENABLE after bumping OpenTofu to >= 1.10. Native S3 conditional-write
|
|
# locking (no DynamoDB); works on R2 via If-None-Match. 1.9.0 has no lockfile support, so state
|
|
# locking is OFF for now (single-operator workflow — see REMOTE-STATE.md follow-up).
|
|
}
|
|
}
|