feat: render Talos bootstrap configuration

This commit is contained in:
eding 2026-07-26 21:06:00 +02:00
parent cc0ceb700d
commit 9cfcb87db0
7 changed files with 61 additions and 35 deletions

View file

@ -11,7 +11,8 @@ crash.log
crash.*.log crash.*.log
# Ignore local Terraform configuration files that can contain sensitive data. # Ignore local Terraform configuration files that can contain sensitive data.
#terraform.tfvars terraform.tfvars
.maidn/
# Ignore override files as they are typically used for local testing. # Ignore override files as they are typically used for local testing.
# These should not be checked in to source control. # These should not be checked in to source control.
@ -58,4 +59,4 @@ terraform.rc
.DS_Store .DS_Store
Thumbs.db Thumbs.db
/.secrets /.secrets
/test /test

View file

@ -11,7 +11,7 @@ locals {
all_networks_flat = flatten([ all_networks_flat = flatten([
for node in var.nodes : [ for node in var.nodes : [
for network in node.networks : { for network in node.networks : {
proxmox_node = lookup(node, "proxmox_node", var.proxmox_node) proxmox_node = coalesce(node.proxmox_node, var.proxmox_node)
vlan_id = network.vlan_id vlan_id = network.vlan_id
} }
] ]
@ -57,10 +57,10 @@ locals {
cores = node.cores cores = node.cores
memory = node.memory memory = node.memory
role = node.role role = node.role
proxmox_node = lookup(node, "proxmox_node", var.proxmox_node) proxmox_node = coalesce(node.proxmox_node, var.proxmox_node)
disk_size = node.disk_size disk_size = node.disk_size
disk_storage = var.disk_storage disk_storage = var.disk_storage
tags = lookup(node, "tags", [node.role]) tags = coalesce(node.tags, [node.role])
iso_file = local.node_iso_files[node.name] iso_file = local.node_iso_files[node.name]
additional_disk_size = lookup(node, "additional_disk_size", null) additional_disk_size = lookup(node, "additional_disk_size", null)
additional_disk_storage = lookup(node, "additional_disk_size", null) != null ? var.additional_disk_storage : null additional_disk_storage = lookup(node, "additional_disk_size", null) != null ? var.additional_disk_storage : null
@ -90,4 +90,4 @@ locals {
description = "Talos Worker Node - Managed by Terraform" description = "Talos Worker Node - Managed by Terraform"
} }
} }
} }

View file

@ -21,7 +21,7 @@ resource "null_resource" "stage_talos_image" {
triggers = { triggers = {
node_name = each.value.name node_name = each.value.name
proxmox_node = lookup(each.value, "proxmox_node", var.proxmox_node) proxmox_node = coalesce(each.value.proxmox_node, var.proxmox_node)
image_url = local.talos_iso_download_url image_url = local.talos_iso_download_url
storage = var.talos_image_storage storage = var.talos_image_storage
talos_version = var.talos_version talos_version = var.talos_version
@ -31,19 +31,26 @@ resource "null_resource" "stage_talos_image" {
provisioner "local-exec" { provisioner "local-exec" {
interpreter = ["python", "-c"] interpreter = ["python", "-c"]
command = "import subprocess; subprocess.run(['python', r'${path.module}/scripts/stage_talos_image.py', '--api-url', '${var.proxmox_api_url}', '--api-token', '${var.proxmox_api_token}', '--node', '${self.triggers.proxmox_node}', '--storage', '${var.talos_image_storage}', '--image-url', '${local.talos_iso_download_url}', '--filename', '${self.triggers.filename}', '--force'], check=True)" environment = {
MAIDN_PROXMOX_API_TOKEN = var.proxmox_api_token
}
command = "import subprocess; subprocess.run(['python', r'${path.module}/scripts/stage_talos_image.py', '--api-url', '${var.proxmox_api_url}', '--node', '${self.triggers.proxmox_node}', '--storage', '${var.talos_image_storage}', '--image-url', '${local.talos_iso_download_url}', '--filename', '${self.triggers.filename}'], check=True)"
} }
} }
# Create all required network bridges # Create all required network bridges
resource "proxmox_virtual_environment_network_linux_bridge" "cluster_bridge" { resource "proxmox_virtual_environment_network_linux_bridge" "cluster_bridge" {
for_each = local.unique_bridges for_each = var.manage_network_bridges ? local.unique_bridges : {}
node_name = each.value.proxmox_node node_name = each.value.proxmox_node
name = each.value.bridge_name name = each.value.bridge_name
comment = "Auto-created bridge for Talos VLAN ${each.value.vlan_id} on ${each.value.proxmox_node}" comment = "Auto-created bridge for Talos VLAN ${each.value.vlan_id} on ${each.value.proxmox_node}"
ports = var.create_vlan_interface ? ["${each.value.phys_iface}.${each.value.vlan_id}"] : [each.value.phys_iface] ports = var.create_vlan_interface ? ["${each.value.phys_iface}.${each.value.vlan_id}"] : [each.value.phys_iface]
vlan_aware = false vlan_aware = false
lifecycle {
prevent_destroy = true
}
} }
# Create the Virtual Machines # Create the Virtual Machines

View file

@ -1,5 +1,6 @@
import argparse import argparse
import json import json
import os
import ssl import ssl
import subprocess import subprocess
import sys import sys
@ -82,13 +83,15 @@ def wait_for_storage_content(base_url, token, node, storage, filename):
def main(): def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("--api-url", required=True) parser.add_argument("--api-url", required=True)
parser.add_argument("--api-token", required=True) parser.add_argument("--api-token", default=os.environ.get("MAIDN_PROXMOX_API_TOKEN"))
parser.add_argument("--node", required=True) parser.add_argument("--node", required=True)
parser.add_argument("--storage", required=True) parser.add_argument("--storage", required=True)
parser.add_argument("--image-url", required=True) parser.add_argument("--image-url", required=True)
parser.add_argument("--filename", required=True) parser.add_argument("--filename", required=True)
parser.add_argument("--force", action="store_true") parser.add_argument("--force", action="store_true")
args = parser.parse_args() args = parser.parse_args()
if not args.api_token:
parser.error("--api-token or MAIDN_PROXMOX_API_TOKEN is required")
try: try:
filename, downloaded = ensure_image(args.api_url, args.api_token, args.node, args.storage, args.image_url, args.filename, args.force) filename, downloaded = ensure_image(args.api_url, args.api_token, args.node, args.storage, args.image_url, args.filename, args.force)

View file

@ -7,6 +7,8 @@ resource "local_file" "talconfig" {
cluster_endpoint = local.cluster_endpoint cluster_endpoint = local.cluster_endpoint
cluster_domain = var.cluster_domain cluster_domain = var.cluster_domain
talos_version = var.talos_version talos_version = var.talos_version
kubernetes_version = var.kubernetes_version
architecture = var.talos_image_architecture
talos_factory_schematic_id = var.talos_factory_schematic_id talos_factory_schematic_id = var.talos_factory_schematic_id
control_plane_vip = var.control_plane_vip control_plane_vip = var.control_plane_vip
@ -20,34 +22,34 @@ resource "local_file" "talconfig" {
name = node.name, name = node.name,
role = node.role, role = node.role,
networks = node.networks, networks = node.networks,
tags = node.tags, tags = coalesce(node.tags, []),
proxmox_node = node.proxmox_node proxmox_node = coalesce(node.proxmox_node, var.proxmox_node)
} if node.role == "controlplane"] } if node.role == "controlplane"]
compute_workers = [for node in var.nodes : { compute_workers = [for node in var.nodes : {
name = node.name, name = node.name,
role = node.role, role = node.role,
networks = node.networks, networks = node.networks,
tags = node.tags, tags = coalesce(node.tags, []),
proxmox_node = node.proxmox_node proxmox_node = coalesce(node.proxmox_node, var.proxmox_node)
} if node.role == "worker" && contains(lookup(node, "tags", []), "compute")] } if node.role == "worker" && !contains(coalesce(node.tags, []), "storage") && !contains(coalesce(node.tags, []), "gpu")]
storage_workers = [for node in var.nodes : { storage_workers = [for node in var.nodes : {
name = node.name, name = node.name,
role = node.role, role = node.role,
networks = node.networks, networks = node.networks,
tags = node.tags, tags = coalesce(node.tags, []),
proxmox_node = node.proxmox_node, proxmox_node = coalesce(node.proxmox_node, var.proxmox_node),
additional_disk_size = lookup(node, "additional_disk_size", "") additional_disk_size = lookup(node, "additional_disk_size", "")
} if node.role == "worker" && contains(lookup(node, "tags", []), "storage")] } if node.role == "worker" && contains(coalesce(node.tags, []), "storage")]
gpu_workers = [for node in var.nodes : { gpu_workers = [for node in var.nodes : {
name = node.name, name = node.name,
role = node.role, role = node.role,
networks = node.networks, networks = node.networks,
tags = node.tags, tags = coalesce(node.tags, []),
proxmox_node = node.proxmox_node proxmox_node = coalesce(node.proxmox_node, var.proxmox_node)
} if node.role == "worker" && contains(lookup(node, "tags", []), "gpu")] } if node.role == "worker" && contains(coalesce(node.tags, []), "gpu")]
# Other variables # Other variables
cluster_pod_nets = var.cluster_pod_nets cluster_pod_nets = var.cluster_pod_nets
@ -63,16 +65,16 @@ output "node_organization" {
description = "How your nodes are organized for Talos configuration" description = "How your nodes are organized for Talos configuration"
value = { value = {
control_plane_nodes = [for node in var.nodes : { control_plane_nodes = [for node in var.nodes : {
name = node.name, ip = node.networks[0].ip, tags = lookup(node, "tags", []) name = node.name, ip = node.networks[0].ip, tags = coalesce(node.tags, [])
} if node.role == "controlplane"] } if node.role == "controlplane"]
compute_workers = [for node in var.nodes : { compute_workers = [for node in var.nodes : {
name = node.name, ip = node.networks[0].ip, tags = lookup(node, "tags", []) name = node.name, ip = node.networks[0].ip, tags = coalesce(node.tags, [])
} if node.role == "worker" && contains(lookup(node, "tags", []), "compute")] } if node.role == "worker" && !contains(coalesce(node.tags, []), "storage") && !contains(coalesce(node.tags, []), "gpu")]
storage_workers = [for node in var.nodes : { storage_workers = [for node in var.nodes : {
name = node.name, ip = node.networks[0].ip, tags = lookup(node, "tags", []), additional_disk = lookup(node, "additional_disk_size", null) name = node.name, ip = node.networks[0].ip, tags = coalesce(node.tags, []), additional_disk = node.additional_disk_size
} if node.role == "worker" && contains(lookup(node, "tags", []), "storage")] } if node.role == "worker" && contains(coalesce(node.tags, []), "storage")]
gpu_workers = [for node in var.nodes : { gpu_workers = [for node in var.nodes : {
name = node.name, ip = node.networks[0].ip, tags = lookup(node, "tags", []) name = node.name, ip = node.networks[0].ip, tags = coalesce(node.tags, [])
} if node.role == "worker" && contains(lookup(node, "tags", []), "gpu")] } if node.role == "worker" && contains(coalesce(node.tags, []), "gpu")]
} }
} }

View file

@ -2,6 +2,7 @@ clusterName: ${cluster_name}
endpoint: ${cluster_endpoint} endpoint: ${cluster_endpoint}
domain: ${cluster_domain} domain: ${cluster_domain}
talosVersion: ${talos_version} talosVersion: ${talos_version}
kubernetesVersion: "${kubernetes_version}"
cniConfig: cniConfig:
name: ${cni_name} name: ${cni_name}
allowSchedulingOnMasters: ${length(control_plane_nodes) == 1 ? "true" : "false"} allowSchedulingOnMasters: ${length(control_plane_nodes) == 1 ? "true" : "false"}
@ -26,7 +27,7 @@ nodes:
installDisk: /dev/sda installDisk: /dev/sda
machineSpec: machineSpec:
mode: metal mode: metal
arch: amd64 arch: ${architecture}
talosImageURL: "factory.talos.dev/installer/${talos_factory_schematic_id}" talosImageURL: "factory.talos.dev/installer/${talos_factory_schematic_id}"
patches: patches:
- |- - |-
@ -70,7 +71,7 @@ nodes:
installDisk: /dev/sda installDisk: /dev/sda
machineSpec: machineSpec:
mode: metal mode: metal
arch: amd64 arch: ${architecture}
talosImageURL: "factory.talos.dev/installer/${talos_factory_schematic_id}" talosImageURL: "factory.talos.dev/installer/${talos_factory_schematic_id}"
patches: patches:
- |- - |-
@ -111,7 +112,7 @@ nodes:
installDisk: /dev/sda installDisk: /dev/sda
machineSpec: machineSpec:
mode: metal mode: metal
arch: amd64 arch: ${architecture}
talosImageURL: "factory.talos.dev/installer/${talos_factory_schematic_id}" talosImageURL: "factory.talos.dev/installer/${talos_factory_schematic_id}"
patches: patches:
- |- - |-
@ -152,7 +153,7 @@ nodes:
installDisk: /dev/sda installDisk: /dev/sda
machineSpec: machineSpec:
mode: metal mode: metal
arch: amd64 arch: ${architecture}
talosImageURL: "factory.talos.dev/installer/${talos_factory_schematic_id}" talosImageURL: "factory.talos.dev/installer/${talos_factory_schematic_id}"
patches: patches:
- |- - |-

View file

@ -50,6 +50,12 @@ variable "cni_name" {
default = "none" default = "none"
} }
variable "kubernetes_version" {
description = "The Kubernetes version rendered into Talos configuration."
type = string
default = "v1.33.4"
}
# --- Global Network Configuration --- # --- Global Network Configuration ---
variable "dns_servers" { variable "dns_servers" {
description = "Global DNS servers for the cluster" description = "Global DNS servers for the cluster"
@ -133,7 +139,7 @@ variable "nodes" {
mac_address = string mac_address = string
cidr = string cidr = string
vlan_id = number vlan_id = number
ip = optional(string) # Static IP, required for control plane, optional for workers ip = optional(string) # Static IP required for every rendered Talos node
gateway = optional(string) # Should only be defined on the primary interface gateway = optional(string) # Should only be defined on the primary interface
})) }))
})) }))
@ -144,11 +150,17 @@ variable "nodes" {
} }
validation { validation {
condition = alltrue([for node in var.nodes : node.role == "controlplane" ? node.networks[0].ip != null : true]) condition = alltrue([for node in var.nodes : node.networks[0].ip != null])
error_message = "The first network for a 'controlplane' node must have a static IP address." error_message = "The first network for every node must have a static IP address."
} }
} }
variable "manage_network_bridges" {
description = "Whether Terraform owns Proxmox host bridges. Leave false for shared or pre-existing bridges."
type = bool
default = false
}
variable "default_physical_interface" { variable "default_physical_interface" {
description = "Default physical interface for nodes (fallback)" description = "Default physical interface for nodes (fallback)"
type = string type = string