Proxmox VM Provisioning with OpenTofu
Proxmox VM Provisioning with OpenTofu
Automated VM provisioning on Proxmox VE using OpenTofu (Terraform-compatible), Ubuntu cloud images, and cloud-init. VMs are cloned from a template and configured with a static IP, SSH access, and a Hetzner Storage Box CIFS mount.
Architecture
Your Machine
└── OpenTofu
└── Proxmox API (port 8006)
└── Clone VM from template (Ubuntu cloud image)
└── cloud-init (first boot)
├── Create user + SSH key
├── Install cifs-utils + kernel modules
└── Mount Hetzner Storage Box → /mnt/storagebox
Prerequisites
- OpenTofu installed locally
- Proxmox VE node accessible via HTTPS
- Hetzner Storage Box (optional, for cold storage)
Part 1 — Proxmox Host Setup
1.1 Create an API Token
In the Proxmox web UI:
- Go to Datacenter → Permissions → API Tokens
- Add a token for
root@pam(e.g. token ID:opentofu1) - Uncheck Privilege Separation so it inherits root permissions
- Save the token secret — it is shown only once
The token format will be: root@pam!opentofu1=<uuid>
1.2 Create the Ubuntu Cloud Image Template
SSH into the Proxmox host and run:
# Download Ubuntu Noble (24.04) cloud image
wget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
# Resize to 32G
qemu-img resize noble-server-cloudimg-amd64.img 32G
# Create the template VM (ID 112)
qm create 112 --name ubuntu-cloud-template --memory 2048 --cores 2 --net0 virtio,bridge=vmbr1
# Import the disk into your datastore
qm importdisk 112 noble-server-cloudimg-amd64.img data-thin
# Attach and configure the disk
qm set 112 --scsihw virtio-scsi-pci --scsi0 data-thin:vm-112-disk-0
qm set 112 --ide2 local:cloudinit
qm set 112 --boot c --bootdisk scsi0
qm set 112 --serial0 socket --vga serial0
# Convert to template
qm template 112
Note: Replace
data-thinwith your actual datastore name. Check available datastores withpvesm status.
1.3 Create the cloud-init Snippet
The cloud-init snippet configures the VM on first boot: creates the user, sets up SSH, installs packages, and mounts the storage box.
nano /var/lib/vz/snippets/cloud-init-user-data.yaml
Paste the following (no leading spaces on any line):
#cloud-config
users:
- name: ubuntu
groups: sudo
shell: /bin/bash
sudo: ALL=(ALL) NOPASSWD:ALL
ssh_authorized_keys:
- ssh-ed25519 YOUR_PUBLIC_KEY_HERE
lock_passwd: false
passwd: YOUR_HASHED_PASSWORD_HERE
packages:
- cifs-utils
runcmd:
- apt-get install -y linux-modules-extra-$(uname -r)
- modprobe cifs
- mkdir -p /mnt/storagebox
- |
cat > /etc/storagebox-credentials << 'EOF'
username=YOUR_STORAGEBOX_USER
password=YOUR_STORAGEBOX_SAMBA_PASSWORD
EOF
- chmod 600 /etc/storagebox-credentials
- echo "//YOUR_STORAGEBOX_HOST/backup /mnt/storagebox cifs credentials=/etc/storagebox-credentials,ip=YOUR_STORAGEBOX_IP,uid=0,gid=0,vers=3.0,sec=ntlmssp,iocharset=utf8,nodfs,nosharesock,_netdev,nofail 0 0" >> /etc/fstab
- mount /mnt/storagebox
- mkdir -p /mnt/storagebox/saas-factory
Generate a password hash on your local machine:
python3 -c "import crypt; print(crypt.crypt('YOUR_PASSWORD', crypt.mksalt(crypt.METHOD_SHA512)))"
Get your SSH public key:
cat ~/.ssh/id_ed25519.pub
Get the Hetzner Storage Box IP:
dig +short YOUR_STORAGEBOX_HOST
Important: The Samba/CIFS password for the Hetzner Storage Box is separate from the main account password. Set it in Hetzner Robot → Storage Box → Password.
Important:
linux-modules-extrais required for the CIFS kernel module on Ubuntu cloud images. Without it, the storage box mount will fail silently.
Part 2 — Local OpenTofu Setup
2.1 Directory Structure
proxmox/
├── tofu-codebase/
│ ├── main.tf # VM and provider configuration
│ ├── variables.tf # Variable declarations
│ ├── terraform.tfvars # Secret values (gitignored)
│ └── .terraform.lock.hcl
├── cloud-init/
│ └── readme.md
├── .gitignore
└── README.md
2.2 Configure Variables
Copy and fill in your values:
cd tofu-codebase
nano terraform.tfvars
proxmox_endpoint = "https://YOUR_PROXMOX_IP:8006/"
proxmox_api_token = "root@pam!opentofu1=YOUR_TOKEN_UUID"
proxmox_insecure = true
vm_username = "ubuntu"
vm_password = "YOUR_VM_PASSWORD"
vm_ssh_key = "ssh-ed25519 YOUR_PUBLIC_KEY"
terraform.tfvarsis gitignored — never commit it.
2.3 Initialize OpenTofu
cd tofu-codebase
tofu init
This downloads the bpg/proxmox provider.
2.4 Plan
Preview what will be created:
tofu plan
Expected output: 1 to add, 0 to change, 0 to destroy — one VM cloned from template 112.
2.5 Apply
tofu apply
Type yes when prompted. The VM will be:
- Cloned from template 112
- Assigned IP
172.16.10.30/24 - Configured via cloud-init on first boot
Cloud-init takes 2–5 minutes on first boot.
Part 3 — Verify the VM
3.1 SSH into the VM
ssh ubuntu@172.16.10.30
3.2 Check cloud-init completed
cloud-init status
# Expected: status: done
3.3 Check storage box is mounted
mount | grep storagebox
df -TH | grep storagebox
3.4 Write a test file and verify from Proxmox host
# On the VM
echo "hello" > /mnt/storagebox/saas-factory/test.txt
# On the Proxmox host
cat /mnt/pve/backup/saas-factory/test.txt
3.5 If cloud-init failed, check logs
cat /var/log/cloud-init-output.log
Part 4 — Teardown
To destroy the VM:
tofu destroy
The template (VM 112) is not managed by OpenTofu and will not be deleted.
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
HTTP 595 Connection refused |
Wrong node_name |
Check node name: pvesh get /nodes |
storage does not exist |
Wrong datastore in initialization |
Use local for cloud-init snippets |
Permission denied (publickey) |
SSH key not injected | Ensure user_data_file_id is set and VM was recreated |
STATUS_LOGON_FAILURE on CIFS |
Wrong Samba password | Reset Samba password in Hetzner Robot |
| CIFS mount fails silently | Missing kernel module | Add linux-modules-extra-$(uname -r) to cloud-init runcmd |
| cloud-init didn't run | Template built from ISO not cloud image | Rebuild template from Ubuntu cloud image |
No comments to display
No comments to display