feat(terraform): add Proxmox VM and Unifi DHCP modules (Phase 2)

This commit is contained in:
2026-02-27 19:12:25 -06:00
parent b33bd5f252
commit 7e0b0a859b
10 changed files with 321 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
# ─── VM ID Derivation ────────────────────────────────────────────────────────
# Convention carried from existing Ansible playbooks:
# VM ID = {third_octet}{fourth_octet:03d}
# Example: 10.1.71.35 → 71035
locals {
ip_octets = split(".", var.ip_address)
vm_id = tonumber("${local.ip_octets[2]}${format("%03d", tonumber(local.ip_octets[3]))}")
}
# ─── Template Lookup ─────────────────────────────────────────────────────────
# Find the template VM ID by name on the clone node
data "proxmox_virtual_environment_vms" "templates" {
node_name = var.clone_node
filter {
name = "template"
values = ["true"]
}
filter {
name = "name"
values = [var.template_name]
}
}
# ─── VM Resource ─────────────────────────────────────────────────────────────
resource "proxmox_virtual_environment_vm" "vm" {
name = var.hostname
vm_id = local.vm_id
node_name = var.target_node
description = "Provisioned by mk-labs pipeline on ${timestamp()}"
tags = ["managed", "pipeline"]
# Clone from template on fantasyland
clone {
vm_id = data.proxmox_virtual_environment_vms.templates.vms[0].vm_id
node_name = var.clone_node
full = true
datastore_id = var.datastore
}
# Cloud-init configuration
# Uses DHCP — Unifi DHCP reservation is created before VM starts
initialization {
ip_config {
ipv4 {
address = "dhcp"
}
}
dns {
servers = var.dns_servers
domain = var.search_domain
}
datastore_id = var.datastore
}
# Network interface
network_device {
bridge = var.bridge
model = "virtio"
vlan_id = var.vlan_id
}
# VM stays STOPPED after clone.
# n8n handles: DHCP reservation → HA affinity rule → VM start
# This ensures DHCP is in place before first boot.
started = false
# QEMU guest agent is installed in template (via Packer).
# Note: Proxmox 9 requires VM.GuestAgent.Audit privilege on the API token.
agent {
enabled = true
}
# Prevent Terraform from fighting with manual changes
lifecycle {
ignore_changes = [
description,
tags,
]
}
}

View File

@@ -0,0 +1,24 @@
output "vm_id" {
description = "Proxmox VM ID of the created VM."
value = proxmox_virtual_environment_vm.vm.vm_id
}
output "mac_address" {
description = "MAC address of the VM's first network interface."
value = proxmox_virtual_environment_vm.vm.network_device[0].mac_address
}
output "hostname" {
description = "Hostname of the created VM."
value = var.hostname
}
output "ip_address" {
description = "IP address of the created VM."
value = var.ip_address
}
output "node_name" {
description = "Proxmox node where the VM is running."
value = proxmox_virtual_environment_vm.vm.node_name
}

View File

@@ -0,0 +1,20 @@
terraform {
required_version = ">= 1.5.0"
required_providers {
proxmox = {
source = "bpg/proxmox"
version = ">= 0.74.0"
}
}
}
provider "proxmox" {
endpoint = var.proxmox_api_url
api_token = var.proxmox_api_token
insecure = true
ssh {
agent = true
}
}

View File

@@ -0,0 +1,13 @@
# Copy this file to terraform.tfvars and fill in the values.
# terraform.tfvars is in .gitignore — never commit secrets.
proxmox_api_url = "https://fantasyland.mk-labs.net:8006"
proxmox_api_token = "terraform@pve!terraform-token=your-token-secret-here"
# These values come from n8n via -var flags at runtime:
# hostname = "test-vm"
# ip_address = "10.1.71.100"
# target_node = "fantasyland"
# template_name = "ubuntu-24.04-small"
# datastore = "liberty-tree"
# vlan_id = 71

View File

@@ -0,0 +1,88 @@
# ─── Provider Authentication ─────────────────────────────────────────────────
variable "proxmox_api_url" {
type = string
description = "Proxmox API endpoint URL (e.g., https://fantasyland.mk-labs.net:8006)"
}
variable "proxmox_api_token" {
type = string
description = "Proxmox API token in format: user@realm!token-name=token-secret"
sensitive = true
}
# ─── VM Identity ─────────────────────────────────────────────────────────────
variable "hostname" {
type = string
description = "VM hostname. Used as the Proxmox VM name and cloud-init hostname."
}
variable "ip_address" {
type = string
description = "Primary IP address for the VM (without CIDR, e.g., 10.1.71.100). Used for VM ID derivation and DHCP reservation."
}
variable "dns_servers" {
type = list(string)
description = "DNS servers for the VM."
default = ["10.1.71.102", "10.1.71.1"]
}
variable "search_domain" {
type = string
description = "DNS search domain for the VM."
default = "mk-labs.net"
}
# ─── Proxmox Placement ──────────────────────────────────────────────────────
variable "clone_node" {
type = string
description = "Proxmox node where templates live. Cloning always happens here."
default = "fantasyland"
}
variable "target_node" {
type = string
description = "Proxmox node for final VM placement. VM is migrated here after clone."
}
variable "datastore" {
type = string
description = "Proxmox storage pool for VM disks."
default = "liberty-tree"
}
# ─── Template Selection ─────────────────────────────────────────────────────
variable "template_name" {
type = string
description = "Name of the Proxmox VM template to clone."
validation {
condition = contains([
"ubuntu-24.04-small",
"ubuntu-24.04-medium",
"ubuntu-24.04-large",
"ubuntu-24.04-large-plus",
"ubuntu-24.04-xlarge",
"ubuntu-24.04-xlarge-plus",
], var.template_name)
error_message = "template_name must be a valid Ubuntu 24.04 template."
}
}
# ─── Network ─────────────────────────────────────────────────────────────────
variable "vlan_id" {
type = number
description = "VLAN tag for the VM network interface."
default = 71
}
variable "bridge" {
type = string
description = "Proxmox network bridge for the VM."
default = "vmbr0"
}