Talos met behulp van terraform op Proxmox

Om te gaan testen met kubernetes wilde ik een test omgeving hebben die ik snel op en af kan bouwen. Het meerst hanidige is dan om gebruik te maken van terraform. Met een terraform apply maak je dan een kubernetes cluster aan en met terraform destroy kan je hem dan weer helemaal afbreken. Er kunnen nog wat verbeteringen aan onderstaande worden aangebracht, maar het moest snel 🙂
Zorg ervoor dat je een machine hebt waarop je terraform geinstalleerd hebt en die naar de proxmox node kan om API calls te doen. Zorg er dus ook voor dat je een user hebt die een API token heeft.
Verder moet je ook talosctl en kubectl installeren.
Maak ook in je DHCP server enkele reserveringen met de MAC addressen uit de main.tf en de ip adressen eraan gekoppeld. Aangezien we name,ijk de metal-amd64.iso gebruiken kunnen we geen ip adressen opgeven bij het starten van de talos VM.
Maak vervolgens het bestand main.tf aan met de volgende inhoud. Pas onder local de gegevens aan zoals je ze graag zou willen hebben.



terraform {
  required_providers {
    proxmox = {
      source  = "telmate/proxmox"
      version = "3.0.2-rc04"
    }
    talos = {
      source  = "siderolabs/talos"
      version = "0.9.0"
    }
    local = {
      source  = "hashicorp/local"
      version = "~> 2.4"
    }
  }
}

# variable "proxmox_server" {}
# variable "proxmox_tokenid" {}
# variable "proxmox_api_token_secret" {
#   sensitive = true
# }

provider "proxmox" {
  pm_api_url          = "https://${var.proxmox_server}:8006/api2/json"
  pm_api_token_id     = var.proxmox_tokenid
  pm_api_token_secret = var.proxmox_api_token_secret
  pm_tls_insecure     = true
}

locals {
  cluster_name   = "talos-cluster"
  k8s_endpoint   = "https://192.168.11.130:6443"

  iso_file       = "ssd-disk1:iso/metal-amd64.iso"
  proxmox_node   = "pmx"
  network_bridge = "vmbr0"
  disk_storage   = "ssd-disk1"
  gateway        = "192.168.11.1"
  subnet_cidr    = "24"

  nodes = {
    controlplane = {
      ip     = "192.168.11.130"
      vmid   = 140
      role   = "controlplane"
      cores  = 2
      mac    = "BC:24:11:21:6A:01"
      memory = 4096
      disk   = "/dev/sda"
      hostname = "talos-cp"
    }
    worker1 = {
      ip     = "192.168.11.131"
      vmid   = 141
      role   = "worker"
      cores  = 2
      mac    = "BC:24:11:D4:E0:F7"
      memory = 2048
      disk   = "/dev/sda"
      hostname = "talos-w1"
    }
    worker2 = {
      ip     = "192.168.11.132"
      vmid   = 142
      role   = "worker"
      cores  = 2
      mac    = "BC:24:11:51:D1:E1"
      memory = 2048
      disk   = "/dev/sda"
      hostname = "talos-w2"
    }
    worker3 = {
      ip     = "192.168.11.133"
      vmid   = 143
      role   = "worker"
      cores  = 2
      mac    = "BC:24:11:B7:09:E4"
      memory = 2048
      disk   = "/dev/sda"
      hostname = "talos-w3"
    }
  }
}

# --- Talos Secrets ---
resource "talos_machine_secrets" "machine_secrets" {}

# --- Talos Machine Configurations ---
data "talos_machine_configuration" "config" {
  for_each         = local.nodes
  cluster_name     = local.cluster_name
  cluster_endpoint = local.k8s_endpoint
  machine_type     = each.value.role
  talos_version    = "v1.7.0"

  machine_secrets = talos_machine_secrets.machine_secrets.machine_secrets

  config_patches = [
    yamlencode({
      machine = {
        install = {
          disk       = each.value.disk
          bootloader = true
        }
        network = {
          nameservers = ["192.168.10.1"]
          interfaces = [{
            interface = "eth0"
            dhcp      = false
            addresses = ["${each.value.ip}/${local.subnet_cidr}"]
            routes    = [{
              network = "0.0.0.0/0"
              gateway = local.gateway
            }]
          }]
          hostname = each.value.hostname
        }
      }
    })
  ]
}

# --- Proxmox VM Creation ---
resource "proxmox_vm_qemu" "talos_nodes" {
  for_each    = local.nodes
  name        = "talos-${each.key}"
  target_node = local.proxmox_node
  vmid        = each.value.vmid

  cores  = each.value.cores
  memory = each.value.memory
  bios   = "ovmf"
  scsihw = "virtio-scsi-pci"

  disk {
    slot    = "scsi0"
    type    = "disk"
    storage = local.disk_storage
    size    = "32G"
  }

  network {
    id      = 0
    bridge  = local.network_bridge
    model   = "virtio"
    macaddr = each.value.mac
  }

  disk {
    slot = "ide2"
    type = "cdrom"
    iso  = local.iso_file
  }

  boot  = "order=scsi0;ide2"
  agent = 0
}

# --- Talos Client Config ---
data "talos_client_configuration" "client" {
  cluster_name         = local.cluster_name
  client_configuration = talos_machine_secrets.machine_secrets.client_configuration
  endpoints            = [local.nodes.controlplane.ip]
  nodes                = [for n in local.nodes : n.ip]
}

resource "local_file" "talosconfig_file" {
  content  = data.talos_client_configuration.client.talos_config
  filename = "${path.module}/talosconfig"
}

# # --- Apply Configurations ---
# resource "talos_machine_configuration_apply" "apply" {
#   for_each = local.nodes
#   client_configuration        = talos_machine_secrets.machine_secrets.client_configuration
#   machine_configuration_input = data.talos_machine_configuration.config[each.key].machine_configuration
#   node                        = each.value.ip
#   apply_mode                  = "auto"

#   depends_on = [
#     proxmox_vm_qemu.talos_nodes,
#     local_file.talosconfig_file
#   ]
# }


# Apply CP first
resource "talos_machine_configuration_apply" "controlplane" {
  for_each                   = { for k,v in local.nodes : k => v if v.role == "controlplane" }
  client_configuration        = talos_machine_secrets.machine_secrets.client_configuration
  machine_configuration_input = data.talos_machine_configuration.config[each.key].machine_configuration
  node                        = each.value.ip
  apply_mode                  = "auto"
  # Optional: give install time
  # timeouts { create = "20m" } # adjust if your storage is slow
}

# Apply workers after CP
resource "talos_machine_configuration_apply" "workers" {
  depends_on                  = [talos_machine_configuration_apply.controlplane]
  for_each                    = { for k,v in local.nodes : k => v if v.role == "worker" }
  client_configuration        = talos_machine_secrets.machine_secrets.client_configuration
  machine_configuration_input = data.talos_machine_configuration.config[each.key].machine_configuration
  node                        = each.value.ip
  apply_mode                  = "auto"
  # timeouts { create = "20m" }
}


# --- Update Boot Order & Reboot via Proxmox API ---
# resource "null_resource" "update_boot_order" {
#   depends_on = [talos_machine_configuration_apply.workers]

#   provisioner "local-exec" {
#     command = <<-EOT
#       echo "Updating boot order to disk-only and rebooting VMs via Proxmox API..."
#       for vmid in ${join(" ", [for n in local.nodes : n.vmid])}; do
#         curl -s -k -X PUT \
#           -H "Authorization: PVEAPIToken=${var.proxmox_tokenid}=${var.proxmox_api_token_secret}" \
#           -d "boot=order=scsi0" \
#           "https://${var.proxmox_server}:8006/api2/json/nodes/${local.proxmox_node}/qemu/$vmid/config"
#         curl -s -k -X POST \
#           -H "Authorization: PVEAPIToken=${var.proxmox_tokenid}=${var.proxmox_api_token_secret}" \
#           "https://${var.proxmox_server}:8006/api2/json/nodes/${local.proxmox_node}/qemu/$vmid/status/reboot"
#       done
#     EOT
#   }
# }

# --- Bootstrap Cluster ---
resource "talos_machine_bootstrap" "bootstrap" {
  depends_on          = [talos_machine_configuration_apply.workers]
  node                = local.nodes.controlplane.ip
  client_configuration = talos_machine_secrets.machine_secrets.client_configuration
  endpoint            = local.nodes.controlplane.ip
}

# --- Retrieve Kubeconfig ---
data "talos_cluster_kubeconfig" "kubeconfig" {
  depends_on           = [talos_machine_bootstrap.bootstrap]
  node                 = local.nodes.controlplane.ip
  client_configuration = talos_machine_secrets.machine_secrets.client_configuration
}

resource "local_file" "kubeconfig_file" {
  content  = data.talos_cluster_kubeconfig.kubeconfig.kubeconfig_raw
  filename = "${path.module}/kubeconfig"
}

output "config" {
  value     = data.talos_client_configuration.client.talos_config
  sensitive = true
}

output "kubeconfig" {
  value     = data.talos_cluster_kubeconfig.kubeconfig.kubeconfig_raw
  sensitive = true
}


Vervolgens maken we een bestand aan terraform.tfvars met de volgende inhoud. Pas ook hier weer alles aan zoals je het graag zou willen hebben:


proxmox_server   = "192.168.11.12"
proxmox_user     = "root"
proxmox_password = "Password"  # Sensitive data
proxmox_tokenid  = "root@pam!terraform" 
proxmox_api_token_secret = "secret_token_api" # Sensitive data
proxmox_node     = "pmx"
storage_name     = "ssd-disk1"  # Or whatever your actual storage name is
iso_file         = "ssd-disk1:iso/metal-amd64.iso"  # Optional, you can modify as needed



En nog een variables.tf met de volgende inhoud


variable "proxmox_server" {
  description = "The Proxmox server's IP or hostname"
  type        = string
}

variable "proxmox_user" {
  description = "The Proxmox username"
  type        = string
}

variable "proxmox_password" {
  description = "The Proxmox password"
  type        = string
  sensitive   = true
}

variable "proxmox_api_token_secret" {
  description = "The Proxmox secret"
  type        = string
  sensitive   = true
}

variable "proxmox_tokenid" { 
  description = "The Proxmox tokenid"
  type        = string
  sensitive   = true
}

variable "proxmox_node" {
  description = "The Proxmox node to deploy the VMs"
  type        = string
}

variable "storage_name" {
  description = "The storage resource name in Proxmox"
  type        = string
}

variable "iso_file" {
  description = "The path to the Talos ISO file"
  type        = string
  default     = "iso/metal-amd64.iso"  # Optional default value
}

En daarna kunnen we een terraform init, terraform validate, terraform plan en terraform apply doen. Ons cluster wordt dan opgebouwd.
Om vervolgens kubectl en talosctl, welke je ook moet installeren, te gebruiken kun je de volgende commando's gebruiken vanuit de directopry van waarin je terraform apply gestart hebt;
kubectl --kubeconfig kubeconfig get nodes
en
talosctl --talosconfig ./talosconfig -n 192.168.11.131 -e 192.168.11.130 health
De bestanden kubeconfig en talosconfig worden aagemaakt met de terraform apply.

Het is handig om de pods ook van buiten het clustyer bereikbaar te maken. Omdat het om een baremetal omgeving gaat is daar metallb handig voor. Om die te kunnen gebruiken doen we het volgende:
kubectl apply --kubeconfig kubeconfig -f https://raw.githubusercontent.com/metallb/metallb/v0.14.5/config/manifests/metallb-native.yaml

Daarna maken we een ipaddresses.yml aan met de volgende content:


apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: first-pool
  namespace: metallb-system
spec:
  addresses:
  - 192.168.11.136-192.168.11.139

En een bestand layer2.yml


apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: first-pool
  namespace: metallb-system

En die gebruiken we dan in het volgende commando's:
kubectl create -f metallb/ipaddresses.yml --kubeconfig kubeconfig
kubectl create -f metallb/layer2.yml --kubeconfig kubeconfig

Leuk om ook iets te laten draaien via deze load balancer. We doen een nginx deployment
kubectl create deploy nginx --image nginx:latest --kubeconfig kubeconfig
En exposen deze dan via de loadbalancer met;
kubectl expose deploy nginx --port 80 --type LoadBalancer --kubeconfig kubeconfig
Als alles goed is gegaan kunnen we nu met het volgende commando kijken wat het "externe" ip adres is:
kubectl get svc --kubeconfig kubeconfig