diff --git a/main.tf b/main.tf index 9bd0677..93fdaf5 100644 --- a/main.tf +++ b/main.tf @@ -1,17 +1,30 @@ resource "local_sensitive_file" "ssh_private_key" { - filename = "${path.module}/.ssh_key" + filename = pathexpand("~/.ssh/infra_ansible_key") content_base64 = data.external.infisical.result["ssh_private_key_b64"] file_permission = "0600" } locals { + # Flatten all declared hosts from tfvars. Derived from var.nodes (not module + # outputs) so the values are known at plan time and for_each can use them + # before any VMs are created. + all_declared_hosts = flatten([ + for hv_name, hv in var.nodes : concat( + [for name, lxc in try(hv.lxc, {}) : { name = name, ip = split("/", lxc.ip)[0] }], + [for name, vm in try(hv.machines, {}) : { name = name, ip = split("/", vm.ip)[0] }] + ) + ]) + + technitium_enabled = try(data.external.infisical.result["technitium_api_token"], "") != "" + # Shared inputs passed to every proxmox_node module call. node_common = { - search_domain = var.search_domain - dns_nameservers = var.dns_nameservers - ssh_key = data.external.infisical.result["ssh_public_key"] - ansible_user = var.ansible_user - lxc_password = data.external.infisical.result["lxc_password"] + search_domain = var.search_domain + dns_nameservers = var.dns_nameservers + ssh_key = data.external.infisical.result["ssh_public_key"] + ansible_user = var.ansible_user + lxc_password = data.external.infisical.result["lxc_password"] + ssh_private_key_path = local_sensitive_file.ssh_private_key.filename } } @@ -30,8 +43,9 @@ module "proxmox" { search_domain = local.node_common.search_domain dns_nameservers = local.node_common.dns_nameservers ssh_key = local.node_common.ssh_key - ansible_user = local.node_common.ansible_user - lxc_password = local.node_common.lxc_password + ansible_user = local.node_common.ansible_user + lxc_password = local.node_common.lxc_password + ssh_private_key_path = local.node_common.ssh_private_key_path providers = { proxmox = proxmox.proxmox @@ -68,6 +82,16 @@ locals { } } +module "technitium" { + source = "./opentofu/modules/technitium" + + enabled = local.technitium_enabled + zone = var.search_domain + hosts = local.all_declared_hosts + zones = var.dns_zones + records = var.dns_records +} + resource "local_file" "ansible_inventory" { filename = "${path.module}/${var.ansible_inventory_path}" content = templatefile("${path.module}/opentofu/inventory.tpl", { diff --git a/providers.tf b/providers.tf index 09b3523..9840336 100644 --- a/providers.tf +++ b/providers.tf @@ -8,9 +8,21 @@ terraform { source = "hashicorp/local" version = "~> 2.2" } + technitium = { + source = "kenske/technitium" + version = "~> 0.2" + } } } +provider "technitium" { + host = var.technitium_host + # Fall back to a placeholder so the provider passes its credential check on + # the first run (before the token exists in Infisical). No technitium + # resources are created while the token is absent, so the dummy is never used. + token = coalesce(try(data.external.infisical.result["technitium_api_token"], ""), "token-not-yet-provisioned") +} + data "external" "infisical" { program = ["bash", "-c", "infisical export --template=./secrets.tmpl"] } diff --git a/terraform.tfvars b/terraform.tfvars index f8458fb..b73b4b4 100644 --- a/terraform.tfvars +++ b/terraform.tfvars @@ -15,9 +15,32 @@ network_subnets = { lan = "192.168.0.0/24" } -ansible_user = "ansible" -search_domain = "lan.oswaldtheotter.net" -dns_nameservers = "1.1.1.1" +ansible_user = "ansible" +search_domain = "oswaldtheotter.lan" +dns_nameservers = "1.1.1.1" +technitium_host = "http://192.168.0.2:5380" + +# ── DNS (Technitium) ──────────────────────────────────────────────────────────── +# Every declared node automatically gets an A record in the search_domain zone +# (lan.oswaldtheotter.net). Define extra zones and manual records below. + +# Additional zones beyond the auto-managed search_domain zone. +dns_zones = { + # "internal.oswaldtheotter.net" = { type = "Primary" } + # "10.168.192.in-addr.arpa" = { type = "Primary" } +} + +# Manual records. In the search_domain zone, a record whose name matches an +# auto-added host replaces that host's record instead of conflicting with it. +dns_records = [ + # { zone = "lan.oswaldtheotter.net", name = "router", type = "A", value = "192.168.0.1" }, + # { zone = "lan.oswaldtheotter.net", name = "nas", type = "CNAME", value = "dev.lan.oswaldtheotter.net" }, + # { zone = "lan.oswaldtheotter.net", name = "@", type = "MX", value = "mail.lan.oswaldtheotter.net", priority = 10 }, + { zone = "oswaldtheotter.lan", name = "omada", type = "A", value = "192.168.0.110" }, + { zone = "oswaldtheotter.lan", name = "portanas", type = "A", value = "192.168.0.4" }, + { zone = "oswaldtheotter.lan", name = "proxmox", type = "A", value = "192.168.0.240" }, + { zone = "oswaldtheotter.lan", name = "nas", type = "CNAME", value = "portanas.lan.oswaldtheotter.net" }, +] # ── Node definitions ─────────────────────────────────────────────────────────── @@ -77,7 +100,7 @@ nodes = { disk_size = "8G" nic_name = "eth0" bridge = "vmbr0" - ip = "192.168.0.3/24" + ip = "192.168.0.2/24" gw = "192.168.0.1" } } diff --git a/vars.tf b/vars.tf index 5c24a8f..1de7430 100644 --- a/vars.tf +++ b/vars.tf @@ -40,6 +40,34 @@ variable "dns_nameservers" { type = string } +variable "technitium_host" { + description = "URL of the Technitium DNS Server API (e.g. http://192.168.0.3:5380). Required once Technitium is running." + type = string + default = "" +} + +variable "dns_zones" { + description = "Additional Technitium DNS zones to create, beyond the auto-managed search_domain zone. Keyed by zone name." + type = map(object({ + type = optional(string, "Primary") + forwarder = optional(string) + })) + default = {} +} + +variable "dns_records" { + description = "Manually managed DNS records. Each targets a zone by name (the search_domain zone or one from dns_zones). A record in the search_domain zone whose name matches an auto-added host replaces that host record instead of conflicting with it." + type = list(object({ + zone = string + name = string # subdomain label, or "@" for the zone apex + type = string # A, AAAA, CNAME, NS, TXT, MX + value = string # IP / target / text / mail exchanger + ttl = optional(number) + priority = optional(number) # required for MX records + })) + default = [] +} + variable "ansible_inventory_path" { description = "Path where the generated Ansible inventory file is written. Defaults to ../ansible/inventories/generated.yml relative to this module, which points to the parent repo when used as a git submodule." type = string