# main.tf — dedicated NAT network + 3 cloud-init'd VMs, each with a thin OS disk # (backed by the cloud image) and a blank thin Longhorn data disk (Phase 1 T-1.1). # # 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"). locals { # 1 GiB in bytes; libvirt volume sizes are bytes. gib = 1073741824 # Per-node identity/addressing, indexed 0..node_count-1. nodes = [ for i in range(var.node_count) : { 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 } ] } # --- dedicated NAT network (10.73.0.0/24, gw .1) --------------------------- # DHCP is OFF: nodes get static IPs from cloud-init network-config, so the # reserved VIP range (.240-.250) can never be leased to something else. resource "libvirt_network" "k3s_dev" { name = var.network_name mode = "nat" addresses = [var.network_cidr] autostart = true dhcp { enabled = false } dns { enabled = true local_only = true } } # --- base OS volume (downloaded/copied once, shared as a backing file) ------ resource "libvirt_volume" "base" { name = "${var.cluster_name}-base.qcow2" pool = var.pool_name source = var.image_source format = "qcow2" } # --- per-node OS disk: thin qcow2 backed by the base image ------------------ resource "libvirt_volume" "os" { count = var.node_count name = "${local.nodes[count.index].name}-os.qcow2" pool = var.pool_name base_volume_id = libvirt_volume.base.id size = var.os_disk_gib * local.gib format = "qcow2" } # --- per-node data disk: blank thin qcow2 for Longhorn ---------------------- resource "libvirt_volume" "data" { count = var.node_count name = "${local.nodes[count.index].name}-data.qcow2" pool = var.pool_name size = var.data_disk_gib * local.gib format = "qcow2" } # --- cloud-init seed (user-data + meta-data + static network-config) -------- # Templates live in the sibling infra/cloud-init/ subtree (T-1.2). resource "libvirt_cloudinit_disk" "seed" { count = var.node_count name = "${local.nodes[count.index].name}-seed.iso" pool = var.pool_name user_data = templatefile("${path.module}/../cloud-init/user-data.yaml.tftpl", { hostname = local.nodes[count.index].name fqdn = local.nodes[count.index].fqdn username = var.login_user ssh_authorized_key = var.ssh_authorized_key }) meta_data = templatefile("${path.module}/../cloud-init/meta-data.yaml.tftpl", { hostname = local.nodes[count.index].name fqdn = local.nodes[count.index].fqdn }) network_config = templatefile("${path.module}/../cloud-init/network-config.yaml.tftpl", { ip = local.nodes[count.index].ip prefix = var.network_prefix gateway = var.network_gateway }) } # --- the VMs ---------------------------------------------------------------- resource "libvirt_domain" "node" { count = var.node_count name = local.nodes[count.index].name memory = var.memory_mib vcpu = var.vcpu # host-passthrough so nested virt is available for KubeVirt in Phase 5. cpu { mode = var.cpu_mode } # qemu-guest-agent is installed by cloud-init; lets tofu read the lease/agent. qemu_agent = true cloudinit = libvirt_cloudinit_disk.seed[count.index].id # IP + hostname come from cloud-init (DHCP is off on this network), so we do NOT # set addresses/hostname here — the provider couples those to DHCP reservations. network_interface { network_id = libvirt_network.k3s_dev.id wait_for_lease = false } # OS disk first (vda, boot), Longhorn data disk second (vdb). disk { volume_id = libvirt_volume.os[count.index].id } disk { volume_id = libvirt_volume.data[count.index].id } console { type = "pty" target_port = "0" target_type = "serial" } console { type = "pty" target_port = "1" target_type = "virtio" } graphics { type = "spice" listen_type = "address" autoport = true } }