diff --git a/terraform/projects/kubernetes_vms/main.tf b/terraform/projects/kubernetes_vms/main.tf index e4a05d3..b2631e3 100644 --- a/terraform/projects/kubernetes_vms/main.tf +++ b/terraform/projects/kubernetes_vms/main.tf @@ -27,20 +27,79 @@ locals { ]) : split("|", key)[1] ] - vm_layout = flatten([ - for role in ["master", "worker"] : [ - for i in range(role == "master" ? var.master_count : var.worker_count) : { - vm_name = "kub-${var.vm_env}-${substr(role, 0, 1)}${format("%02d", i + 1)}" - seq_index = (role == "master" ? 0 : var.master_count) + i - vm_role = role + node_free = { for node in local.node_stats : node.name => node.free_mib } + + master_ram = var.vm_attr.master.ram + worker_ram = var.vm_attr.worker.ram + + # Masters: one per node, only on nodes with enough free memory + feasible_masters = [ + for node in local.node_order : node + if local.node_free[node] >= local.master_ram + ] + + master_nodes = length(local.feasible_masters) >= var.master_count ? [ + for i in range(var.master_count) : local.feasible_masters[i] + ] : [] + + master_hosts = toset(local.master_nodes) + + # Residual capacity per node after masters are reserved + residual_mib = { + for node in local.node_order : + node => local.node_free[node] - (contains(local.master_hosts, node) ? local.master_ram : 0) + } + + max_residual_mib = length(local.node_order) > 0 ? max([for node in local.node_order : local.residual_mib[node]]...) : 0 + + worker_slots = { + for node in local.node_order : node => floor(local.residual_mib[node] / local.worker_ram) + } + + # Workers: pack nodes with residual capacity, most available first (name as tie-break) + worker_precedence = [ + for key in sort([ + for node in local.node_order : + "${format("%012d", local.max_residual_mib - local.residual_mib[node])}|${node}" + if local.worker_slots[node] >= 1 + ]) : split("|", key)[1] + ] + + worker_slots_list = [for node in local.worker_precedence : local.worker_slots[node]] + + # Water-fill: each node takes workers until it runs out of capacity or demand is met + worker_placed = [ + for i in range(length(local.worker_precedence)) : + min(local.worker_slots_list[i], max(0, var.worker_count - try(sum([for j in range(i) : local.worker_slots_list[j]]), 0))) + ] + + worker_prefix = [ + for i in range(length(local.worker_placed)) : + try(sum([for j in range(i) : local.worker_placed[j]]), 0) + ] + + master_placements = [ + for i in range(length(local.master_nodes)) : { + vm_name = "kub-${var.vm_env}-m${format("%02d", i + 1)}" + vm_role = "master" + vm_node = local.master_nodes[i] + } + ] + + worker_placements = flatten([ + for p in range(length(local.worker_precedence)) : [ + for k in range(local.worker_placed[p]) : { + vm_name = "kub-${var.vm_env}-w${format("%02d", local.worker_prefix[p] + k + 1)}" + vm_role = "worker" + vm_node = local.worker_precedence[p] } ] ]) vm_list = { - for vm in local.vm_layout : + for vm in concat(local.master_placements, local.worker_placements) : vm.vm_name => { - node_name = length(local.node_order) > 0 ? local.node_order[vm.seq_index % length(local.node_order)] : "" + node_name = vm.vm_node vm_cpu = var.vm_attr[vm.vm_role].cpu vm_ram = var.vm_attr[vm.vm_role].ram vm_vlan = var.vm_attr[vm.vm_role].vlan @@ -53,6 +112,27 @@ locals { data "proxmox_virtual_environment_nodes" "pve_nodes" {} +check "proxmox_nodes" { + assert { + condition = length(local.node_order) > 0 + error_message = "No online Proxmox node available for deployment." + } +} + +check "master_capacity" { + assert { + condition = length(local.master_nodes) == var.master_count + error_message = "Cannot place ${var.master_count} master(s): only ${length(local.feasible_masters)} online node(s) have at least ${local.master_ram} MiB free memory." + } +} + +check "worker_capacity" { + assert { + condition = try(sum(local.worker_placed), 0) == var.worker_count + error_message = "Cannot place ${var.worker_count} worker(s) (${local.worker_ram} MiB each): residual free capacity on online nodes fits only ${try(sum(local.worker_placed), 0)} worker(s)." + } +} + output "vm_ip" { value = { for k, v in module.pve_vm : k => v.vm_ip } }