Fix: Fixed remote state storage in S3 Bucket

This commit is contained in:
2026-07-27 21:36:23 +01:00
parent a528867ef5
commit 7a61260a07
6 changed files with 65 additions and 127 deletions
+5 -8
View File
@@ -72,9 +72,8 @@ architecture-beta
├── infra/ # OpenTofu — provisions the 3 servers, network, firewalls, DNS
│ ├── main.tf # root module: wires network + dev/prod/vpn/dns modules together
│ ├── variables.tf # shared inputs (sizes, locations, IP ranges, SSH key names)
│ ├── versions.tf # provider/backend requirements + state encryption config
│ ├── versions.tf # provider/backend requirements + S3 backend config + state encryption - see Remote state
│ ├── terraform.tfvars.example
│ ├── backend.hcl.example # S3 backend config (bucket/key/endpoint) - see Remote state
│ └── modules/
│ ├── network/ # shared private network + subnet (dev + prod)
│ ├── dev/ prod/ vpn/ # one server + firewall each, labeled role=<name>
@@ -122,7 +121,7 @@ Each of `services/dev`, `services/prod`, `services/vpn` follows the same convent
./control.sh
```
It offers **Check prerequisites**, a **Configuration** section (prompts for `HCLOUD_TOKEN`, `DEPLOY_USER`, `ANSIBLE_SSH_PRIVATE_KEY_FILE`, the `BACKUP_S3_*` credentials, and the state encryption passphrase, saved to gitignored `.control.env` so you only enter them once; also copies `terraform.tfvars.example`/`backend.hcl.example` into place), individual OpenTofu/Ansible actions, and a **Guided full setup** that runs everything in the firewall-imposed order — `vpn` first, pause for you to connect, then `dev`/`prod`. It's just a wrapper around the `tofu`/`ansible-playbook` commands below, so you can always drop to running them by hand.
It offers **Check prerequisites**, a **Configuration** section (prompts for `HCLOUD_TOKEN`, `DEPLOY_USER`, `ANSIBLE_SSH_PRIVATE_KEY_FILE`, the `BACKUP_S3_*` credentials, and the state encryption passphrase, saved to gitignored `.control.env` so you only enter them once; also copies `terraform.tfvars.example` into place), individual OpenTofu/Ansible actions, and a **Guided full setup** that runs everything in the firewall-imposed order — `vpn` first, pause for you to connect, then `dev`/`prod`. It's just a wrapper around the `tofu`/`ansible-playbook` commands below, so you can always drop to running them by hand.
## Provisioning the infrastructure (`infra/`)
@@ -134,21 +133,19 @@ export AWS_SECRET_ACCESS_KEY=your-s3-secret-key
export TF_VAR_state_encryption_passphrase=a-long-random-passphrase
cp terraform.tfvars.example terraform.tfvars
$EDITOR terraform.tfvars # set ssh_key_names at minimum
cp backend.hcl.example backend.hcl
$EDITOR backend.hcl # set bucket/key/region/endpoints.s3
tofu init -backend-config=backend.hcl
tofu init
tofu plan
tofu apply
```
This creates the private network + subnet, one server + scoped firewall per host (labeled `role = dev/prod/vpn`), and one DNS zone per domain in `var.dns_zones` plus every record in [Service inventory](#service-inventory). `terraform.tfvars`, `backend.hcl`, and any `*.tfvars` file are gitignored — never commit real values there.
This creates the private network + subnet, one server + scoped firewall per host (labeled `role = dev/prod/vpn`), and one DNS zone per domain in `var.dns_zones` plus every record in [Service inventory](#service-inventory). `terraform.tfvars` and any other `*.tfvars` file are gitignored — never commit real values there.
Useful outputs: `tofu output dev_ipv4`, `tofu output prod_ipv4`, `tofu output vpn_ipv4`, `tofu output dns_nameservers`.
### Remote state
State is stored remotely in the S3-compatible bucket from [Prerequisites](#prerequisites) (`infra/versions.tf`'s `backend "s3" {}`), under its own object key so it never collides with the `BACKUP_S3_*` backup archives living in the same bucket. `infra/backend.hcl` (copied from `backend.hcl.example`, gitignored) holds the non-secret bucket/key/region/endpoint; credentials come from the standard `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` environment variables (`control.sh` exports these from `BACKUP_S3_ACCESS_KEY_ID`/`BACKUP_S3_SECRET_ACCESS_KEY` for you)backend blocks can't reference variables, which is why the split exists.
State is stored remotely in the S3-compatible bucket from [Prerequisites](#prerequisites), under its own object key so it never collides with the `BACKUP_S3_*` backup archives living in the same bucket. The bucket/key/region/endpoint are hardcoded directly into `infra/versions.tf`'s `backend "s3" {}` block — backend blocks can't reference variables, but none of that is secret, so it just lives in the file. Credentials come from the standard `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` environment variables (`control.sh` exports these from `BACKUP_S3_ACCESS_KEY_ID`/`BACKUP_S3_SECRET_ACCESS_KEY` for you — same bucket, same credentials).
The state itself is encrypted client-side (OpenTofu's built-in [state encryption](https://opentofu.org/docs/language/state/encryption/), PBKDF2 + AES-GCM) before it's ever written to the bucket, using `TF_VAR_state_encryption_passphrase`. This is deliberate rather than relying on the bucket's own server-side encryption: Hetzner Object Storage only supports SSE-C (a per-request customer-supplied key), not the SSE-S3 header the s3 backend's `encrypt` option sends, so that option wouldn't actually do anything here. `state { enforced = true }` in `infra/versions.tf` means every `tofu` command fails loudly if the passphrase is missing, rather than silently falling back to plaintext. **Losing the passphrase means losing the state** — back it up somewhere durable (a password manager, not the repo).