# main.tf — 3 cloud-init'd Proxmox VE VMs (k3s-dev-1/2/3), each with an imported # OS disk (from the Debian cloud image) and a blank Longhorn data disk (R-ENV-01). # # Node lifecycle stage 1 ("Create") per docs/02. cloud-init (infra/cloud-init/) # does stage 2 ("Baseline"); Ansible (infra/ansible/) does stage 3 ("Cluster-join"). # # Provider: bpg/proxmox. VMs land on the Proxmox node `var.proxmox_node`, disks on # `var.datastore` (ZFS "Storage"), NIC on `var.bridge` (vmbr0), static IPs via the # native cloud-init `initialization` block. The rich first-boot baseline (users, # packages, kernel modules, Longhorn mount, swap-off) is delivered as a cloud-init # user-data SNIPPET (see the snippets note in README / operator action below). locals { # Per-node identity/addressing, KEYED BY NODE NAME (map, not list). # for_each over this map gives every node a stable, name-based state address # (…vm.node["k3s-dev-2"]) instead of a positional one. Positional count indexes # are brittle under -target/-replace: a single-node destroy on a count-indexed # resource can cascade to siblings (F1 finding). Name keys are individually # addressable, so one node can be replaced/destroyed without disturbing the others. nodes = { for i in range(var.node_count) : "${var.node_name_prefix}${i + 1}" => { name = "${var.node_name_prefix}${i + 1}" ip = var.node_ips[i] fqdn = "${var.node_name_prefix}${i + 1}.${var.node_domain}" is_first = i == 0 } } # Ordered (node1..nodeN) list + primary view for outputs that need positional # access — the resource for_each uses the map above; these preserve ordering for # the Ansible inventory (primary = cluster-init server, rest = secondaries). node_list = [for i in range(var.node_count) : local.nodes["${var.node_name_prefix}${i + 1}"]] primary = local.nodes["${var.node_name_prefix}1"] } # --- base OS cloud image (downloaded once to `local`, imported per node) ----- # content_type "iso" stores it on the ISO datastore; the .img file name lets # Proxmox accept the qcow2 (it still reads it as qcow2 on disk import). resource "proxmox_virtual_environment_download_file" "debian_cloud_image" { content_type = "iso" datastore_id = var.image_datastore node_name = var.proxmox_node url = var.image_source file_name = var.image_file_name overwrite = false } # --- per-node cloud-init user-data snippet ---------------------------------- # bpg's native initialization.user_account only sets user/keys/password; it can't # carry packages/write_files/runcmd. To preserve the full k3s/Cilium/Longhorn # baseline we render the existing user-data.yaml.tftpl and ship it as a SNIPPET, # referenced by the VM's initialization.user_data_file_id. # # OPERATOR ACTION: the `snippets` content type must be enabled on var.image_datastore # (Datacenter -> Storage -> local -> Content -> add "Snippets"), else this file # resource fails. See README "Operator prerequisites". resource "proxmox_virtual_environment_file" "user_data" { for_each = local.nodes content_type = "snippets" datastore_id = var.image_datastore node_name = var.proxmox_node source_raw { file_name = "${each.value.name}-user-data.yaml" data = templatefile("${path.module}/../cloud-init/user-data.yaml.tftpl", { hostname = each.value.name fqdn = each.value.fqdn username = var.login_user ssh_authorized_key = var.ssh_authorized_key }) } } # --- the VMs ---------------------------------------------------------------- resource "proxmox_virtual_environment_vm" "node" { for_each = local.nodes name = each.value.name node_name = var.proxmox_node tags = ["k3s", "k3s-dev", "opentofu"] description = "k3s dev node ${each.value.name} (${each.value.fqdn}) — provisioned by OpenTofu." on_boot = true stop_on_destroy = true # `host` CPU type passes through the physical CPU so nested virt is available for # KubeVirt in Phase 5. cpu { cores = var.vcpu type = var.cpu_type } memory { dedicated = var.memory_mib } # qemu-guest-agent is installed by cloud-init; lets tofu/Proxmox read guest state. agent { enabled = true } operating_system { type = "l26" } # OS disk (virtio0 -> /dev/vda, boot): a copy of the imported cloud image, grown # to os_disk_gib. Longhorn data disk (virtio1 -> /dev/vdb): blank, data_disk_gib. # virtio* keeps the guest device names (/dev/vda, /dev/vdb) the cloud-init # fs_setup/mount for /var/lib/longhorn already expects. disk { datastore_id = var.datastore import_from = proxmox_virtual_environment_download_file.debian_cloud_image.id interface = "virtio0" size = var.os_disk_gib discard = "on" } disk { datastore_id = var.datastore interface = "virtio1" size = var.data_disk_gib discard = "on" } network_device { bridge = var.bridge } # Static addressing + DNS come from cloud-init (bpg generates the network-config), # so the libvirt netplan template is gone. User-data (the baseline) is the snippet. initialization { datastore_id = var.datastore ip_config { ipv4 { address = "${each.value.ip}/${var.network_prefix}" gateway = var.network_gateway } } dns { servers = [var.dns_server] } user_data_file_id = proxmox_virtual_environment_file.user_data[each.key].id } # Boot from the OS disk only. boot_order = ["virtio0"] }