From 7aa5137039eae8c101c2e203c1d36fcd5b4e7a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torjus=20H=C3=A5kestad?= Date: Sat, 31 Jan 2026 23:30:00 +0100 Subject: [PATCH] terraform: add parameterized multi-VM deployment system Implements Phase 1 of the OpenTofu deployment plan: - Replace single-VM configuration with locals-based for_each pattern - Support multiple VMs in single deployment - Automatic DHCP vs static IP detection - Configurable defaults with per-VM overrides - Dynamic outputs for VM IPs and specifications New files: - outputs.tf: Dynamic outputs for deployed VMs - vms.tf: VM definitions using locals.vms map Updated files: - variables.tf: Added default variables for VM configuration - README.md: Comprehensive documentation and examples Removed files: - vm.tf: Replaced by new vms.tf (archived as vm.tf.old, then removed) Co-Authored-By: Claude Sonnet 4.5 --- terraform/README.md | 166 ++++++++++++++++++++++++++++++++++++++++- terraform/outputs.tf | 24 ++++++ terraform/variables.tf | 75 +++++++++++++++++++ terraform/vm.tf | 90 ---------------------- terraform/vms.tf | 114 ++++++++++++++++++++++++++++ 5 files changed, 376 insertions(+), 93 deletions(-) create mode 100644 terraform/outputs.tf delete mode 100644 terraform/vm.tf create mode 100644 terraform/vms.tf diff --git a/terraform/README.md b/terraform/README.md index be4ebb7..2cca37f 100644 --- a/terraform/README.md +++ b/terraform/README.md @@ -1,6 +1,6 @@ # OpenTofu Configuration for Proxmox -This directory contains OpenTofu configuration for managing Proxmox VMs. +This directory contains OpenTofu configuration for managing Proxmox VMs using a parameterized, multi-VM deployment system. ## Setup @@ -29,9 +29,169 @@ This directory contains OpenTofu configuration for managing Proxmox VMs. tofu plan ``` +## Defining VMs + +All VMs are defined in the `vms.tf` file in the `locals.vms` map. Each VM can specify custom configurations or use defaults from `variables.tf`. + +### Example: DHCP VM + +```hcl +vms = { + "simple-vm" = { + cpu_cores = 2 + memory = 2048 + disk_size = "20G" + # No "ip" field = DHCP + } +} +``` + +### Example: Static IP VM + +```hcl +vms = { + "web-server" = { + ip = "10.69.13.50/24" + cpu_cores = 4 + memory = 4096 + disk_size = "50G" + } +} +``` + +### Example: Minimal VM (all defaults) + +```hcl +vms = { + "test-vm" = {} +} +``` + +### Example: Multiple VMs + +```hcl +vms = { + "vm1" = { + ip = "10.69.13.50/24" + } + "vm2" = { + ip = "10.69.13.51/24" + cpu_cores = 4 + memory = 8192 + } + "vm3" = { + # DHCP + cpu_cores = 2 + memory = 2048 + } +} +``` + +## Configuration Options + +Each VM in the `vms` map supports the following fields (all optional): + +| Field | Description | Default | +|-------|-------------|---------| +| `ip` | Static IP with CIDR (e.g., "10.69.13.50/24"). Omit for DHCP | DHCP | +| `gateway` | Network gateway (used with static IP) | `10.69.13.1` | +| `cpu_cores` | Number of CPU cores | `2` | +| `memory` | Memory in MB | `2048` | +| `disk_size` | Disk size (e.g., "20G", "100G") | `"20G"` | +| `target_node` | Proxmox node to deploy to | `"pve1"` | +| `template_name` | Template VM to clone from | `"nixos-25.11.20260128.fa83fd8"` | +| `storage` | Storage backend | `"local-zfs"` | +| `bridge` | Network bridge | `"vmbr0"` | +| `vlan_tag` | VLAN tag | `13` | +| `ssh_public_key` | SSH public key for root | See `variables.tf` | +| `nameservers` | DNS servers | `"10.69.13.5 10.69.13.6"` | +| `search_domain` | DNS search domain | `"home.2rjus.net"` | + +Defaults are defined in `variables.tf` and can be changed globally. + +## Deployment Commands + +### Deploy All VMs + +```bash +tofu apply +``` + +### Deploy Specific VM + +```bash +tofu apply -target=proxmox_vm_qemu.vm[\"vm-name\"] +``` + +### Destroy Specific VM + +```bash +tofu destroy -target=proxmox_vm_qemu.vm[\"vm-name\"] +``` + +### View Deployed VMs + +```bash +tofu output vm_ips +tofu output deployment_summary +``` + +### Plan Changes + +```bash +tofu plan +``` + +## Outputs + +After deployment, OpenTofu provides two outputs: + +**vm_ips**: IP addresses and SSH commands for each VM +``` +vm_ips = { + "vm1" = { + ip = "10.69.13.50" + ssh_command = "ssh root@10.69.13.50" + } +} +``` + +**deployment_summary**: Full specifications for each VM +``` +deployment_summary = { + "vm1" = { + cpu_cores = 4 + memory_mb = 4096 + disk_size = "50G" + ip = "10.69.13.50" + node = "pve1" + } +} +``` + +## Workflow + +1. Edit `vms.tf` to define your VMs in the `locals.vms` map +2. Run `tofu plan` to preview changes +3. Run `tofu apply` to deploy +4. Run `tofu output vm_ips` to get IP addresses +5. SSH to VMs and configure as needed + ## Files -- `main.tf` - Provider configuration and test data source -- `variables.tf` - Variable definitions +- `main.tf` - Provider configuration +- `variables.tf` - Variable definitions and defaults +- `vms.tf` - VM definitions and deployment logic +- `outputs.tf` - Output definitions for deployed VMs - `terraform.tfvars.example` - Example credentials file - `terraform.tfvars` - Your actual credentials (gitignored) +- `vm.tf.old` - Archived single-VM configuration (reference) + +## Notes + +- VMs are deployed as full clones (not linked clones) +- Cloud-init handles initial networking configuration +- QEMU guest agent is enabled on all VMs +- All VMs start on boot by default +- IPv6 is disabled +- Destroying VMs removes them from Proxmox but does not clean up DNS entries or NixOS configurations diff --git a/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 0000000..046b0b3 --- /dev/null +++ b/terraform/outputs.tf @@ -0,0 +1,24 @@ +# Dynamic outputs for all deployed VMs + +output "vm_ips" { + description = "IP addresses and SSH commands for deployed VMs" + value = { + for name, vm in proxmox_vm_qemu.vm : name => { + ip = vm.default_ipv4_address + ssh_command = "ssh root@${vm.default_ipv4_address}" + } + } +} + +output "deployment_summary" { + description = "Summary of deployed VMs with their specifications" + value = { + for name, vm in proxmox_vm_qemu.vm : name => { + cpu_cores = vm.cpu[0].cores + memory_mb = vm.memory + disk_size = vm.disks[0].virtio[0].virtio0[0].disk[0].size + ip = vm.default_ipv4_address + node = vm.target_node + } + } +} diff --git a/terraform/variables.tf b/terraform/variables.tf index a1ec455..fe13cb0 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -20,3 +20,78 @@ variable "proxmox_tls_insecure" { type = bool default = true } + +# Default values for VM configurations +# These can be overridden per-VM in vms.tf + +variable "default_target_node" { + description = "Default Proxmox node to deploy VMs to" + type = string + default = "pve1" +} + +variable "default_template_name" { + description = "Default template VM name to clone from" + type = string + default = "nixos-25.11.20260128.fa83fd8" +} + +variable "default_ssh_public_key" { + description = "Default SSH public key for root user" + type = string + default = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAwfb2jpKrBnCw28aevnH8HbE5YbcMXpdaVv2KmueDu6 torjus@gunter" +} + +variable "default_storage" { + description = "Default storage backend for VM disks" + type = string + default = "local-zfs" +} + +variable "default_bridge" { + description = "Default network bridge" + type = string + default = "vmbr0" +} + +variable "default_vlan_tag" { + description = "Default VLAN tag" + type = number + default = 13 +} + +variable "default_gateway" { + description = "Default network gateway for static IP VMs" + type = string + default = "10.69.13.1" +} + +variable "default_nameservers" { + description = "Default DNS nameservers" + type = string + default = "10.69.13.5 10.69.13.6" +} + +variable "default_search_domain" { + description = "Default DNS search domain" + type = string + default = "home.2rjus.net" +} + +variable "default_cpu_cores" { + description = "Default number of CPU cores" + type = number + default = 2 +} + +variable "default_memory" { + description = "Default memory in MB" + type = number + default = 2048 +} + +variable "default_disk_size" { + description = "Default disk size" + type = string + default = "20G" +} diff --git a/terraform/vm.tf b/terraform/vm.tf deleted file mode 100644 index 710c1fb..0000000 --- a/terraform/vm.tf +++ /dev/null @@ -1,90 +0,0 @@ -# Example VM configuration - clone from template -# Before using this, you need to: -# 1. Upload the NixOS image to Proxmox -# 2. Restore it as a template VM (e.g., ID 9000) -# 3. Update the variables below - -variable "target_node" { - description = "Proxmox node to deploy to" - type = string - default = "pve1" -} - -variable "template_name" { - description = "Template VM name to clone from" - type = string - default = "nixos-25.11.20260128.fa83fd8" -} - -variable "ssh_public_key" { - description = "SSH public key for root user" - type = string - default = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAwfb2jpKrBnCw28aevnH8HbE5YbcMXpdaVv2KmueDu6 torjus@gunter" -} - -# Example test VM -resource "proxmox_vm_qemu" "test_vm" { - name = "nixos-test-tofu" - target_node = var.target_node - - # Clone from template - clone = var.template_name - - # Full clone (not linked) - full_clone = true - - # Boot configuration - boot = "order=virtio0" - scsihw = "virtio-scsi-single" - - # VM settings - cpu { - cores = 2 - } - memory = 2048 - - # Network - network { - id = 0 - model = "virtio" - bridge = "vmbr0" - tag = 13 - } - - # Disk settings - disks { - virtio { - virtio0 { - disk { - size = "20G" - storage = "local-zfs" - } - } - } - } - - # Start on boot - start_at_node_boot = true - - # Agent - agent = 1 - - # Cloud-init configuration - ciuser = "root" - sshkeys = var.ssh_public_key - ipconfig0 = "ip=dhcp" - nameserver = "10.69.13.5 10.69.13.6" - searchdomain = "home.2rjus.net" - - # Skip IPv6 since we don't use it - skip_ipv6 = true - - rng { - source = "/dev/urandom" - period = 1000 - } -} - -output "test_vm_ip" { - value = proxmox_vm_qemu.test_vm.default_ipv4_address -} diff --git a/terraform/vms.tf b/terraform/vms.tf new file mode 100644 index 0000000..1b995a8 --- /dev/null +++ b/terraform/vms.tf @@ -0,0 +1,114 @@ +# VM Definitions +# Define all VMs to deploy in the locals.vms map below +# Omit fields to use defaults from variables.tf + +locals { + # Define VMs here + # Each VM can override defaults by specifying values + # Omit "ip" field for DHCP, include it for static IP + vms = { + # Example DHCP VM (uncomment to deploy): + # "example-dhcp-vm" = { + # cpu_cores = 2 + # memory = 2048 + # disk_size = "20G" + # } + + # Example Static IP VM (uncomment to deploy): + # "example-static-vm" = { + # ip = "10.69.13.50/24" + # cpu_cores = 4 + # memory = 4096 + # disk_size = "50G" + # } + + # Example Minimal VM using all defaults (uncomment to deploy): + # "minimal-vm" = {} + } + + # Compute VM configurations with defaults applied + vm_configs = { + for name, vm in local.vms : name => { + target_node = lookup(vm, "target_node", var.default_target_node) + template_name = lookup(vm, "template_name", var.default_template_name) + cpu_cores = lookup(vm, "cpu_cores", var.default_cpu_cores) + memory = lookup(vm, "memory", var.default_memory) + disk_size = lookup(vm, "disk_size", var.default_disk_size) + storage = lookup(vm, "storage", var.default_storage) + bridge = lookup(vm, "bridge", var.default_bridge) + vlan_tag = lookup(vm, "vlan_tag", var.default_vlan_tag) + ssh_public_key = lookup(vm, "ssh_public_key", var.default_ssh_public_key) + nameservers = lookup(vm, "nameservers", var.default_nameservers) + search_domain = lookup(vm, "search_domain", var.default_search_domain) + # Network configuration - detect DHCP vs static + ip = lookup(vm, "ip", null) + gateway = lookup(vm, "gateway", var.default_gateway) + } + } +} + +# Deploy all VMs using for_each +resource "proxmox_vm_qemu" "vm" { + for_each = local.vm_configs + + name = each.key + target_node = each.value.target_node + + # Clone from template + clone = each.value.template_name + full_clone = true + + # Boot configuration + boot = "order=virtio0" + scsihw = "virtio-scsi-single" + + # VM settings + cpu { + cores = each.value.cpu_cores + } + memory = each.value.memory + + # Network + network { + id = 0 + model = "virtio" + bridge = each.value.bridge + tag = each.value.vlan_tag + } + + # Disk settings + disks { + virtio { + virtio0 { + disk { + size = each.value.disk_size + storage = each.value.storage + } + } + } + } + + # Start on boot + start_at_node_boot = true + + # Agent + agent = 1 + + # Cloud-init configuration + ciuser = "root" + sshkeys = each.value.ssh_public_key + nameserver = each.value.nameservers + searchdomain = each.value.search_domain + + # Network configuration - DHCP or static IP + ipconfig0 = each.value.ip != null ? "ip=${each.value.ip},gw=${each.value.gateway}" : "ip=dhcp" + + # Skip IPv6 since we don't use it + skip_ipv6 = true + + # RNG device for better entropy + rng { + source = "/dev/urandom" + period = 1000 + } +} -- 2.49.1