feat: Added DNS control to tofu
This commit is contained in:
@@ -81,7 +81,8 @@ architecture-beta
|
||||
│ │ └── templates/
|
||||
│ │ └── runners-docker-compose.yml.tftpl # shape of each generated runner service
|
||||
│ ├── prod/ # prod server + firewall + volume
|
||||
│ └── vpn/ # vpn server + firewall (no private network, no volume)
|
||||
│ ├── vpn/ # vpn server + firewall (no private network, no volume)
|
||||
│ └── dns/ # Hetzner DNS zones + A records for every domain in var.dns_zones
|
||||
├── services/ # Docker Compose stacks, grouped by which server they run on
|
||||
│ ├── dev/ # Gitea + CI runner + Traefik (git.luke-else.co.uk, cicd.luke-else.co.uk)
|
||||
│ ├── prod/ # Websites, Bitwarden, RustDesk, status page + Traefik
|
||||
@@ -100,7 +101,7 @@ 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) - all are installed on every server - plus the private key matching one of them available locally (OpenTofu uses it once per server to run the post-install bootstrap — see below)
|
||||
- [OpenTofu](https://opentofu.org/docs/intro/install/) `>= 1.6.0`
|
||||
- DNS records for the domains in [Service inventory](#service-inventory) pointed at the relevant server's public IP
|
||||
- Ownership of the domains in `var.dns_zones` at whatever registrar they're bought through, so you can point their NS records at Hetzner (see [Managing DNS](#managing-dns) — the zones and records themselves are created for you)
|
||||
|
||||
Docker + the Compose plugin no longer need installing by hand — the bootstrap script below handles that.
|
||||
|
||||
@@ -117,12 +118,13 @@ tofu plan
|
||||
tofu apply
|
||||
```
|
||||
|
||||
This creates, via `module.network` / `module.dev` / `module.prod` / `module.vpn` in `infra/main.tf`:
|
||||
This creates, via `module.network` / `module.dev` / `module.prod` / `module.vpn` / `module.dns` in `infra/main.tf`:
|
||||
- `hcloud_network` + subnet, shared by `dev` and `prod` (`modules/network`)
|
||||
- one `hcloud_server` + `hcloud_firewall` per host, scoped to the ports each host actually uses (`modules/dev`, `modules/prod`, `modules/vpn`)
|
||||
- one `hcloud_volume` each for `dev` and `prod` (`modules/dev`, `modules/prod`; `vpn` has none)
|
||||
- one Hetzner DNS zone per domain in `var.dns_zones`, plus every A record in [Service inventory](#service-inventory) (`modules/dns` — see [Managing DNS](#managing-dns))
|
||||
|
||||
Useful outputs: `tofu output dev_ipv4`, `tofu output prod_ipv4`, `tofu output vpn_ipv4`.
|
||||
Useful outputs: `tofu output dev_ipv4`, `tofu output prod_ipv4`, `tofu output vpn_ipv4`, `tofu output dns_nameservers`.
|
||||
|
||||
`terraform.tfvars` and any `*.tfvars` file are gitignored — never commit real values there. Defaults for server sizes, locations, and IP ranges live in `infra/variables.tf` and are passed down into the modules from `infra/main.tf`; override them per-environment via `terraform.tfvars`.
|
||||
|
||||
@@ -155,6 +157,16 @@ Registration tokens are handled automatically, not baked into the generated file
|
||||
|
||||
If step 3 is run before you're connected to the VPN, the `dev`/`prod` provisioners will fail and Terraform will mark those servers tainted — re-running `tofu apply` once connected will recreate and re-bootstrap them.
|
||||
|
||||
### Managing DNS
|
||||
|
||||
`module.dns` (in `infra/modules/dns`) creates one Hetzner DNS zone per domain in `var.dns_zones` (default: `luke-else.co.uk`, `divine-couture.co.uk`, `snexo.co.uk`) and an `hcloud_zone_rrset` A record for every hostname currently referenced by a Traefik `Host()` rule anywhere under `services/` — kept in sync with `local.dns_records` in `infra/main.tf`, pointed at whichever of `dev`/`prod`/`vpn` actually serves it. Zones have `prevent_destroy = true`, matching the volumes — losing one deletes every record in it.
|
||||
|
||||
Creating the zone doesn't make Hetzner authoritative for the domain by itself: you still need to point that domain's NS records at Hetzner's nameservers at whichever registrar it's registered through. Run `tofu output dns_nameservers` after applying to get the exact nameservers per domain, and set those as the domain's NS records at the registrar. Propagation can take a while depending on the registrar and the domain's previous NS TTL.
|
||||
|
||||
Adding a new subdomain: add an entry to `local.dns_records` in `infra/main.tf` (zone, name, and the target `module.<host>.ipv4`) and re-run `tofu apply` — don't hand-create records in the Hetzner console, they'll drift from state. Note `name = "@"` is Hetzner's convention for a zone's apex record (e.g. bare `snexo.co.uk`), not an empty string.
|
||||
|
||||
Only A records for IPv4 are managed here — none of the modules currently track servers' IPv6 addresses, so AAAA records aren't generated even though the firewalls already allow IPv6 traffic.
|
||||
|
||||
## Deploying the services (`services/`)
|
||||
|
||||
Once a server exists, copy the relevant `services/<host>/` directory to it (e.g. `scp -r services/prod user@<prod_ipv4>:~/services`) and run the matching script from inside that directory:
|
||||
@@ -239,3 +251,4 @@ Then reopen the repo in VS Code with the Dev Containers extension.
|
||||
- `vpn` is intentionally excluded from the private network so that a compromised VPN endpoint cannot reach `dev` or `prod` directly over it — SSH access still works because the VPN's egress traffic is NATed through its own public IP.
|
||||
- Firewalls are allowlists scoped per host in `infra/modules/<host>/main.tf` — only ports actually used by that host's compose stacks (plus the cross-host private network range) are open.
|
||||
- Every host's [post-install bootstrap](#post-install-bootstrap) disables SSH password authentication, restricts root login to key-only, and creates a separate sudo user (`var.deploy_user`) for day-to-day access.
|
||||
- DNS zones (`modules/dns`) carry both Hetzner's own `delete_protection` and Terraform's `prevent_destroy` — losing a zone takes every record in it with it, including for `divine-couture.co.uk` and `snexo.co.uk`, not just `luke-else.co.uk`.
|
||||
|
||||
Reference in New Issue
Block a user