feat: Added remote state storage in S3 bucket
This commit is contained in:
@@ -72,7 +72,9 @@ 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
|
||||
│ ├── 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>
|
||||
@@ -107,10 +109,10 @@ Each of `services/dev`, `services/prod`, `services/vpn` follows the same convent
|
||||
|
||||
- A [Hetzner Cloud](https://console.hetzner.cloud/) project and API token
|
||||
- One or more SSH keys uploaded to that project (Console → Security → SSH Keys), plus the matching private key available locally
|
||||
- [OpenTofu](https://opentofu.org/docs/intro/install/) `>= 1.6.0`
|
||||
- [OpenTofu](https://opentofu.org/docs/intro/install/) `>= 1.7.0` (state encryption, see [Remote state](#remote-state))
|
||||
- [Ansible](https://docs.ansible.com/ansible/latest/installation_guide/index.html) `>= 2.15` and the `hetzner.hcloud` collection (`ansible-galaxy collection install -r ansible/requirements.yml`)
|
||||
- Ownership of the domains in `var.dns_zones` at whatever registrar they're bought through, so you can point their NS records at Hetzner
|
||||
- An S3-compatible bucket (e.g. [Hetzner Object Storage](https://www.hetzner.com/storage/object-storage/)) and an access key/secret pair — not provisioned by OpenTofu, so create this yourself
|
||||
- An S3-compatible bucket (e.g. [Hetzner Object Storage](https://www.hetzner.com/storage/object-storage/)) and an access key/secret pair — not provisioned by OpenTofu, so create this yourself. Used both for service backups (`BACKUP_S3_*`) and, under a separate object key, for OpenTofu's own remote state (see [Remote state](#remote-state))
|
||||
|
||||
## Quickstart
|
||||
|
||||
@@ -120,25 +122,36 @@ 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`, and the `BACKUP_S3_*` credentials, saved to gitignored `.control.env` so you only enter them once), 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`/`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.
|
||||
|
||||
## Provisioning the infrastructure (`infra/`)
|
||||
|
||||
```sh
|
||||
cd infra
|
||||
export HCLOUD_TOKEN=your-hetzner-api-token # never commit this
|
||||
export AWS_ACCESS_KEY_ID=your-s3-access-key-id # for the state backend, below
|
||||
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
|
||||
tofu init -backend-config=backend.hcl
|
||||
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` 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`, `backend.hcl`, and any `*.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.
|
||||
|
||||
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).
|
||||
|
||||
### Managing DNS
|
||||
|
||||
Creating a zone doesn't make Hetzner authoritative by itself — run `tofu output dns_nameservers` and set those as the domain's NS records at its registrar. Propagation time depends on the registrar and the domain's previous NS TTL.
|
||||
|
||||
Reference in New Issue
Block a user