From fa1be640f911aee8860c6fac561f5c8e79391fa9 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Sat, 12 Sep 2026 11:05:24 +0000 Subject: [PATCH] refactor: allow input variable --- terraform/projects/kubernetes_vms/main.tf | 146 +++++++++++------- .../projects/kubernetes_vms/variables.tf | 119 ++++++++++---- 2 files changed, 184 insertions(+), 81 deletions(-) diff --git a/terraform/projects/kubernetes_vms/main.tf b/terraform/projects/kubernetes_vms/main.tf index 50ed4cf..32d4d67 100644 --- a/terraform/projects/kubernetes_vms/main.tf +++ b/terraform/projects/kubernetes_vms/main.tf @@ -29,80 +29,115 @@ locals { 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 + # Per-role simple overrides merged over vm_attr defaults (null keeps the default) + role_overrides = { + for role, cfg in var.vm_attr : role => lookup({ + "master" = { ram = var.master_ram != null ? var.master_ram * 1024 : null, cpu = var.master_cpu, vlan = var.master_vlan } + "worker" = { ram = var.worker_ram != null ? var.worker_ram * 1024 : null, cpu = var.worker_cpu, vlan = var.worker_vlan } + }, role, {}) + } - # Masters: one per node, only on nodes with enough free memory - feasible_masters = [ + effective_vm_attr = { + for role, cfg in var.vm_attr : role => merge(cfg, { + for attr, value in local.role_overrides[role] : attr => value if value != null + }) + } + + # Control plane (max_per_node = 1): one VM per node, only on nodes with enough free memory + cp_role = [for role, cfg in local.effective_vm_attr : role if cfg.max_per_node == 1][0] + cp_attr = local.effective_vm_attr[local.cp_role] + + cp_feasible = [ for node in local.node_order : node - if local.node_free[node] >= local.master_ram + if local.node_free[node] >= local.cp_attr.ram ] - master_nodes = length(local.feasible_masters) >= var.master_count ? [ - for i in range(var.master_count) : local.feasible_masters[i] + cp_nodes = length(local.cp_feasible) >= local.cp_attr.count ? [ + for i in range(local.cp_attr.count) : local.cp_feasible[i] ] : [] - master_hosts = toset(local.master_nodes) + cp_hosts = toset(local.cp_nodes) - # Residual capacity per node after masters are reserved + # Residual capacity per node after control-plane VMs are reserved residual_mib = { for node in local.node_order : - node => local.node_free[node] - (contains(local.master_hosts, node) ? local.master_ram : 0) + node => local.node_free[node] - (contains(local.cp_hosts, node) ? local.cp_attr.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) + # Remaining roles (max_per_node != 1): pack nodes with residual capacity, most available first + pack_roles = [for role, cfg in local.effective_vm_attr : role if cfg.max_per_node != 1] + + pack_slots = { + for role in local.pack_roles : role => { + for node in local.node_order : + node => min( + floor(local.residual_mib[node] / local.effective_vm_attr[role].ram), + local.effective_vm_attr[role].max_per_node == 0 + ? floor(local.residual_mib[node] / local.effective_vm_attr[role].ram) + : local.effective_vm_attr[role].max_per_node + ) + } } - # 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] - ] + pack_precedence = { + for role in local.pack_roles : role => [ + for key in sort([ + for node in local.node_order : + "${format("%012d", local.max_residual_mib - local.residual_mib[node])}|${node}" + if local.pack_slots[role][node] >= 1 + ]) : split("|", key)[1] + ] + } - worker_slots_list = [for node in local.worker_precedence : local.worker_slots[node]] + pack_slots_list = { + for role in local.pack_roles : role => + [for node in local.pack_precedence[role] : local.pack_slots[role][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))) - ] + # Water-fill: each node takes VMs until it runs out of capacity or demand is met + pack_placed = { + for role in local.pack_roles : role => [ + for i in range(length(local.pack_precedence[role])) : + min(local.pack_slots_list[role][i], max(0, local.effective_vm_attr[role].count - try(sum([for j in range(i) : local.pack_slots_list[role][j]]), 0))) + ] + } - worker_prefix = [ - for i in range(length(local.worker_placed)) : - try(sum([for j in range(i) : local.worker_placed[j]]), 0) - ] + pack_prefix = { + for role in local.pack_roles : role => [ + for i in range(length(local.pack_placed[role])) : + try(sum([for j in range(i) : local.pack_placed[role][j]]), 0) + ] + } - master_placements = [ - for i in range(length(local.master_nodes)) : { - vm_name = "kub-${var.vm_env}-m${format("%01d", i + 1)}" - vm_role = "master" - vm_node = local.master_nodes[i] + cp_placements = [ + for i in range(length(local.cp_nodes)) : { + vm_name = "kub-${var.vm_env}-${substr(local.cp_role, 0, 1)}${format("%01d", i + 1)}" + vm_role = local.cp_role + vm_node = local.cp_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("%01d", local.worker_prefix[p] + k + 1)}" - vm_role = "worker" - vm_node = local.worker_precedence[p] - } + pack_placements = flatten([ + for role in local.pack_roles : [ + for p in range(length(local.pack_precedence[role])) : [ + for k in range(local.pack_placed[role][p]) : { + vm_name = "kub-${var.vm_env}-${substr(role, 0, 1)}${format("%01d", local.pack_prefix[role][p] + k + 1)}" + vm_role = role + vm_node = local.pack_precedence[role][p] + } + ] ] ]) vm_list = { - for vm in concat(local.master_placements, local.worker_placements) : + for vm in concat(local.cp_placements, local.pack_placements) : vm.vm_name => { 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 + vm_cpu = local.effective_vm_attr[vm.vm_role].cpu + vm_ram = local.effective_vm_attr[vm.vm_role].ram + vm_vlan = local.effective_vm_attr[vm.vm_role].vlan vm_role = vm.vm_role } } @@ -119,17 +154,24 @@ check "proxmox_nodes" { } } -check "master_capacity" { +check "control_plane_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." + condition = length(local.cp_nodes) == local.cp_attr.count + error_message = "Cannot place ${local.cp_attr.count} ${local.cp_role} VM(s): only ${length(local.cp_feasible)} online node(s) have at least ${local.cp_attr.ram} MiB free memory." } } -check "worker_capacity" { +check "pack_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)." + condition = length(local.pack_roles) == 0 ? true : alltrue([ + for role in local.pack_roles : + try(sum(local.pack_placed[role]), 0) == local.effective_vm_attr[role].count + ]) + error_message = "Cannot place all pack VMs: ${join(", ", [ + for role in local.pack_roles : + "${role} needs ${local.effective_vm_attr[role].count} VM(s) (${local.effective_vm_attr[role].ram} MiB each), residual free capacity fits only ${try(sum(local.pack_placed[role]), 0)} VM(s)." + if try(sum(local.pack_placed[role]), 0) != local.effective_vm_attr[role].count + ])}" } } diff --git a/terraform/projects/kubernetes_vms/variables.tf b/terraform/projects/kubernetes_vms/variables.tf index 1525310..f3ce432 100644 --- a/terraform/projects/kubernetes_vms/variables.tf +++ b/terraform/projects/kubernetes_vms/variables.tf @@ -21,38 +21,99 @@ variable "proxmox_ssh_password" { sensitive = true } -variable "master_count" { - description = "Number of Kubernetes control-plane (master) VMs to deploy" - type = number - default = 3 - - validation { - condition = var.master_count >= 1 - error_message = "master_count must be at least 1." - } -} - -variable "worker_count" { - description = "Number of Kubernetes worker VMs to deploy" - type = number - default = 3 - - validation { - condition = var.worker_count >= 1 - error_message = "worker_count must be at least 1." - } -} - variable "vm_attr" { - description = "VM attributes" + description = "VM attributes per role. max_per_node = 0 means unlimited VMs per node." type = map(object({ - ram = number - cpu = number - vlan = number + ram = number + cpu = number + vlan = number + count = number + max_per_node = number })) default = { - "master" = { ram = 2048, cpu = 2, vlan = 66 } - "worker" = { ram = 4096, cpu = 2, vlan = 66 } + "master" = { ram = 2048, cpu = 2, vlan = 66, count = 3, max_per_node = 1 } + "worker" = { ram = 4096, cpu = 2, vlan = 66, count = 3, max_per_node = 0 } + } + + validation { + condition = alltrue([for v in values(var.vm_attr) : v.count >= 1]) + error_message = "Each role must deploy at least 1 VM." + } + + validation { + condition = alltrue([for v in values(var.vm_attr) : v.max_per_node >= 0]) + error_message = "max_per_node must be 0 (unlimited) or a positive number." + } + + validation { + condition = length([for v in values(var.vm_attr) : v if v.max_per_node == 1]) == 1 + error_message = "Exactly one role must have max_per_node = 1 (the control-plane role)." + } +} + +variable "master_cpu" { + description = "Master role CPU override. null keeps the vm_attr default." + type = number + default = null + + validation { + condition = var.master_cpu == null || var.master_cpu >= 1 + error_message = "master_cpu must be null or at least 1." + } +} + +variable "master_ram" { + description = "Master role RAM override in GiB (converted to MiB internally). null keeps the vm_attr default." + type = number + default = null + + validation { + condition = var.master_ram == null || var.master_ram >= 1 + error_message = "master_ram must be null or at least 1 GiB." + } +} + +variable "master_vlan" { + description = "Master role VLAN override. null keeps the vm_attr default." + type = number + default = null + + validation { + condition = var.master_vlan == null || var.master_vlan >= 1 + error_message = "master_vlan must be null or at least 1." + } +} + +variable "worker_cpu" { + description = "Worker role CPU override. null keeps the vm_attr default." + type = number + default = null + + validation { + condition = var.worker_cpu == null || var.worker_cpu >= 1 + error_message = "worker_cpu must be null or at least 1." + } +} + +variable "worker_ram" { + description = "Worker role RAM override in GiB (converted to MiB internally). null keeps the vm_attr default." + type = number + default = null + + validation { + condition = var.worker_ram == null || var.worker_ram >= 1 + error_message = "worker_ram must be null or at least 1 GiB." + } +} + +variable "worker_vlan" { + description = "Worker role VLAN override. null keeps the vm_attr default." + type = number + default = null + + validation { + condition = var.worker_vlan == null || var.worker_vlan >= 1 + error_message = "worker_vlan must be null or at least 1." } } @@ -60,4 +121,4 @@ variable "vm_env" { description = "VM environment" type = string default = "prod" -} +} \ No newline at end of file