OpenTofu: Open-Source Terraform for Homelabs
Infrastructure as Code without vendor lock-in. The community-backed Terraform alternative with state encryption.
Overview
In August 2023, HashiCorp changed Terraform’s license from the permissive Mozilla Public License (MPL) to the Business Source License (BSL), creating uncertainty for the open-source community and commercial users alike. Within days, the Linux Foundation announced OpenTofu - a true open-source fork of Terraform that preserves the original MPL license and community-driven development model.
For homelabbers, OpenTofu represents more than just a license choice. It’s a commitment to infrastructure-as-code principles without vendor lock-in, backed by industry giants including Spacelift, Gruntwork, Harness, env0, and Scalr. OpenTofu is 100% compatible with Terraform 1.5.x and earlier, making migration seamless while ensuring your infrastructure code remains truly open.
What you’ll learn:
- Why OpenTofu matters for homelab infrastructure management
- Installing and configuring OpenTofu on your preferred platform
- HashiCorp Configuration Language (HCL) fundamentals
- Managing Proxmox VMs with the Telmate provider
- State management strategies for homelabs
- Migrating existing Terraform projects to OpenTofu
- Advanced patterns for multi-environment homelab deployments
Why OpenTofu for Homelabs?
Instead of manually creating VMs through the Proxmox web interface, clicking through endless configuration screens, OpenTofu lets you define your entire infrastructure in code. Spin up identical dev/staging/prod environments with a single command. Version control your infrastructure alongside your application code. Destroy and recreate your entire homelab in minutes when experimenting.
Prerequisites
Before starting, ensure you have:
- Linux/macOS/Windows system with command-line access
- Proxmox VE (or other infrastructure platform you want to manage)
- Basic command-line proficiency (comfortable with terminal operations)
- Understanding of virtualization concepts (VMs, networks, storage)
- Git installed (for version control of infrastructure code)
- Text editor with HCL syntax support (VS Code recommended)
Hardware requirements:
- Any modern system (OpenTofu itself is lightweight)
- Access to infrastructure you want to manage (Proxmox, Docker, cloud, etc.)
Optional but helpful:
- Familiarity with Terraform (makes transition easier but not required)
- Experience with declarative configuration systems
- Basic programming/scripting knowledge
Understanding Infrastructure as Code
The Manual Problem
Picture this typical homelab workflow:
- Log into Proxmox web UI
- Click “Create VM”
- Fill out 15+ form fields (CPU, RAM, disk, network, etc.)
- Wait for VM creation
- Repeat steps 1-4 for each VM
- Document what you created in a spreadsheet or text file
- When you need to recreate something, reverse-engineer settings from screenshots
Now imagine you need to recreate your entire lab after hardware failure. Or replicate your production setup for testing. Or remember why you set that specific network configuration six months ago.
This is why infrastructure as code exists.
The OpenTofu Solution
With OpenTofu, that same workflow becomes:
resource "proxmox_vm_qemu" "web_server" {
name = "web-01"
target_node = "pve-node-01"
cores = 4
memory = 8192
scsihw = "virtio-scsi-pci"
disk {
size = "32G"
storage = "local-lvm"
type = "scsi"
}
network {
model = "virtio"
bridge = "vmbr0"
}
} Run tofu apply and your VM appears. Check this code into Git and your infrastructure is versioned, documented, and reproducible.
Key IaC Principles
Declarative Configuration You describe what you want, not how to create it:
- “I want a VM with 4 cores and 8GB RAM”
- OpenTofu figures out the steps to make it happen
Idempotent Operations Running the same configuration multiple times produces the same result:
- First run: Creates the VM
- Second run: Detects VM exists, does nothing
- Third run: Still does nothing (unless you changed the config)
State Management OpenTofu tracks what it created in a state file:
- Knows what resources exist
- Detects drift (manual changes outside OpenTofu)
- Enables updates and deletions
Provider Model Providers translate HCL into API calls for specific platforms:
- Proxmox provider → Proxmox API
- Docker provider → Docker API
- AWS provider → AWS API
OpenTofu vs. Terraform: What Changed?
Compatibility:
- OpenTofu 1.6.x is drop-in compatible with Terraform 1.5.x
- Same HCL syntax
- Same provider ecosystem
- Same state file format (for now)
What’s Different:
- License: MPL 2.0 (truly open-source) vs. BSL (source-available)
- Governance: Linux Foundation vs. HashiCorp
- Development: Community-driven vs. vendor-controlled
- Future: Independent roadmap vs. tied to commercial products
Migration Path:
- Replace
terraformbinary withtofu - Update provider registry URLs (automatic for most providers)
- Continue using existing
.tffiles unchanged
Installation and Initial Setup
Installing OpenTofu
Method 1: Package Manager (Recommended)
Ubuntu/Debian:
# Add OpenTofu repository
curl -fsSL https://get.opentofu.org/install-opentofu.sh -o install-opentofu.sh
sudo bash install-opentofu.sh
# Verify installation
tofu version macOS (Homebrew):
# Install via Homebrew
brew install opentofu
# Verify installation
tofu version Arch Linux:
# Install from AUR
yay -S opentofu
# Verify installation
tofu version Method 2: Manual Installation
Download the latest release from opentofu.org/downloads:
# Download (replace VERSION with latest)
wget https://github.com/opentofu/opentofu/releases/download/v1.6.0/tofu_1.6.0_linux_amd64.zip
# Extract
unzip tofu_1.6.0_linux_amd64.zip
# Move to PATH
sudo mv tofu /usr/local/bin/
# Verify
tofu version Method 3: Docker Container
# Create alias for containerized OpenTofu
alias tofu='docker run --rm -v $(pwd):/workspace -w /workspace ghcr.io/opentofu/opentofu:latest'
# Verify
tofu version Setting Up Your First Project
Create project structure:
# Create project directory
mkdir ~/homelab-infrastructure
cd ~/homelab-infrastructure
# Create subdirectories
mkdir -p {modules,environments/{dev,staging,prod},terraform.tfstate.d}
# Initialize Git repository
git init Create .gitignore:
cat > .gitignore << 'EOF'
# Local .terraform directories
**/.terraform/*
# .tfstate files (contain sensitive data)
*.tfstate
*.tfstate.*
# Crash log files
crash.log
# Exclude all .tfvars files (may contain secrets)
*.tfvars
# Ignore override files
override.tf
override.tf.json
# Ignore CLI configuration files
.terraformrc
terraform.rc
# Ignore lock files (or commit them, depending on preference)
# .terraform.lock.hcl
EOF Configuring VS Code for HCL
Install HashiCorp Terraform extension:
- Open VS Code
- Extensions → Search “HashiCorp Terraform”
- Install extension
- Restart VS Code
Configure extension:
Add to .vscode/settings.json:
{
"terraform.languageServer": {
"enabled": true,
"args": []
},
"terraform.experimentalFeatures": {
"validateOnSave": true
},
"[terraform]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "hashicorp.terraform"
}
} This provides syntax highlighting, auto-completion, and validation for OpenTofu/Terraform files.
HashiCorp Configuration Language (HCL) Basics
Understanding HCL Syntax
HCL is designed to be human-readable while remaining machine-parseable. It’s not quite JSON, not quite YAML - it’s purpose-built for infrastructure configuration.
Basic Structure:
# Comments start with #
// Or with double slashes
/*
Multi-line comments
use C-style syntax
*/
# Block structure
block_type "block_label" "optional_second_label" {
# Arguments use key = value syntax
argument_name = "value"
# Nested blocks
nested_block {
nested_argument = 123
}
} Data Types:
# String
name = "web-server"
# Number
count = 3
memory = 8192
# Boolean
enable_backups = true
# List
availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
# Map
tags = {
Environment = "production"
Project = "homelab"
ManagedBy = "opentofu"
}
# Null
optional_value = null Essential HCL Blocks
Provider Block:
terraform {
required_providers {
proxmox = {
source = "telmate/proxmox"
version = "~> 2.9"
}
}
}
provider "proxmox" {
pm_api_url = "https://proxmox.example.com:8006/api2/json"
pm_api_token_id = "root@pam!terraform"
pm_api_token_secret = var.proxmox_token
pm_tls_insecure = true # Only for self-signed certs
} Resource Block:
resource "resource_type" "resource_name" {
argument = "value"
nested_block {
nested_argument = "value"
}
} Variable Block:
variable "vm_count" {
description = "Number of VMs to create"
type = number
default = 3
validation {
condition = var.vm_count > 0 && var.vm_count <= 10
error_message = "VM count must be between 1 and 10"
}
} Output Block:
output "vm_ip_addresses" {
description = "IP addresses of created VMs"
value = proxmox_vm_qemu.web[*].default_ipv4_address
} Data Source Block:
data "proxmox_virtual_environment_nodes" "available_nodes" {
}
# Use data source output
locals {
first_node = data.proxmox_virtual_environment_nodes.available_nodes.names[0]
} Variables and Expressions
Referencing Variables:
# Define variable
variable "vm_memory" {
default = 4096
}
# Use variable
resource "proxmox_vm_qemu" "example" {
memory = var.vm_memory # References the variable
} String Interpolation:
variable "environment" {
default = "production"
}
resource "proxmox_vm_qemu" "web" {
name = "web-${var.environment}-01" # Produces "web-production-01"
description = <<-EOT
Web server for ${var.environment}
Managed by OpenTofu
Created on ${timestamp()}
EOT
} Conditional Expressions:
variable "environment" {
default = "dev"
}
resource "proxmox_vm_qemu" "web" {
# Use more memory in production
memory = var.environment == "production" ? 8192 : 4096
# Enable backups only in production
backup = var.environment == "production" ? true : false
} For Expressions:
variable "vm_names" {
default = ["web", "db", "cache"]
}
# Create uppercase version of each name
locals {
uppercase_names = [for name in var.vm_names : upper(name)]
# Result: ["WEB", "DB", "CACHE"]
# Create map with index
indexed_names = {
for idx, name in var.vm_names :
idx => name
}
# Result: { 0 = "web", 1 = "db", 2 = "cache" }
} Managing Proxmox Infrastructure
Setting Up Proxmox Provider
The Telmate Proxmox provider is the community standard for managing Proxmox with OpenTofu/Terraform.
Create API token in Proxmox:
- Log into Proxmox web interface
- Navigate to Datacenter → Permissions → API Tokens
- Click Add
- Configure:
- User:
root@pam - Token ID:
terraform - Privilege Separation: Unchecked (for full access)
- User:
- Copy the secret (you won’t see it again)
Configure provider:
Create providers.tf:
terraform {
required_version = ">= 1.6.0"
required_providers {
proxmox = {
source = "telmate/proxmox"
version = "~> 2.9"
}
}
}
provider "proxmox" {
pm_api_url = var.proxmox_api_url
pm_api_token_id = var.proxmox_token_id
pm_api_token_secret = var.proxmox_token_secret
pm_tls_insecure = true # Set to false with valid SSL cert
pm_parallel = 2 # Limit concurrent API requests
pm_timeout = 600 # Timeout for operations (seconds)
} Create variables file:
Create variables.tf:
variable "proxmox_api_url" {
description = "Proxmox API endpoint"
type = string
}
variable "proxmox_token_id" {
description = "Proxmox API token ID"
type = string
}
variable "proxmox_token_secret" {
description = "Proxmox API token secret"
type = string
sensitive = true
}
variable "target_node" {
description = "Proxmox node name"
type = string
default = "pve"
} Create terraform.tfvars (DO NOT COMMIT):
proxmox_api_url = "https://192.168.1.100:8006/api2/json"
proxmox_token_id = "root@pam!terraform"
proxmox_token_secret = "your-secret-here"
target_node = "pve-node-01" Initialize OpenTofu:
# Download provider and initialize
tofu init
# Verify provider installation
tofu providers Creating Your First VM
Basic VM configuration:
Create main.tf:
resource "proxmox_vm_qemu" "ubuntu_server" {
name = "ubuntu-web-01"
target_node = var.target_node
# VM Type
agent = 1 # Enable QEMU guest agent
os_type = "l26" # Linux 2.6+ kernel
qemu_os = "l26"
# CPU Configuration
cores = 2
sockets = 1
cpu = "host"
# Memory
memory = 4096
# BIOS and Machine Type
bios = "seabios"
scsihw = "virtio-scsi-pci"
boot = "order=scsi0;ide2;net0"
# Disk
disk {
size = "32G"
type = "scsi"
storage = "local-lvm"
iothread = 1
discard = "on"
}
# Network
network {
model = "virtio"
bridge = "vmbr0"
}
# Lifecycle
onboot = true
# Cloud-init (if using cloud-init image)
ipconfig0 = "ip=dhcp"
# Description
desc = "Ubuntu web server - Managed by OpenTofu"
} Apply configuration:
# Preview changes
tofu plan
# Apply changes
tofu apply
# Review output and type 'yes' to confirm Verify creation:
# Show state
tofu show
# List resources
tofu state list Advanced VM Configuration with Cloud-Init
Cloud-init enables automated VM provisioning. First, create a cloud-init enabled template in Proxmox:
# Download Ubuntu cloud image
wget https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-amd64.img
# Create VM template
qm create 9000 --name "ubuntu-22.04-cloud" --memory 2048 --cores 2 --net0 virtio,bridge=vmbr0
qm importdisk 9000 ubuntu-22.04-server-cloudimg-amd64.img local-lvm
qm set 9000 --scsihw virtio-scsi-pci --scsi0 local-lvm:vm-9000-disk-0
qm set 9000 --boot c --bootdisk scsi0
qm set 9000 --ide2 local-lvm:cloudinit
qm set 9000 --serial0 socket --vga serial0
qm set 9000 --agent enabled=1
qm template 9000 Use template with OpenTofu:
resource "proxmox_vm_qemu" "ubuntu_cloud" {
name = "web-server-${count.index + 1}"
count = 3 # Create 3 VMs
target_node = var.target_node
# Clone from template
clone = "ubuntu-22.04-cloud"
# Full clone (not linked clone)
full_clone = true
# VM Configuration
cores = 2
memory = 4096
agent = 1
# Cloud-init Configuration
cicustom = "user=local:snippets/cloud-init-user.yml"
# Network configuration with static IP
ipconfig0 = "ip=192.168.1.${10 + count.index}/24,gw=192.168.1.1"
# DNS
nameserver = "192.168.1.1"
searchdomain = "homelab.local"
# SSH Keys
sshkeys = file("~/.ssh/id_rsa.pub")
# User
ciuser = "ubuntu"
cipassword = var.default_password
} Create cloud-init user data:
Save to Proxmox at /var/lib/vz/snippets/cloud-init-user.yml:
#cloud-config
users:
- name: ubuntu
groups: sudo
shell: /bin/bash
sudo: ['ALL=(ALL) NOPASSWD:ALL']
ssh_authorized_keys:
- ssh-rsa AAAAB3Nza...your-public-key...
packages:
- qemu-guest-agent
- curl
- git
- vim
package_update: true
package_upgrade: true
runcmd:
- systemctl enable qemu-guest-agent
- systemctl start qemu-guest-agent Creating Multiple VMs with Count
variable "web_server_count" {
description = "Number of web servers to create"
type = number
default = 3
}
resource "proxmox_vm_qemu" "web_cluster" {
count = var.web_server_count
name = "web-${count.index + 1}"
target_node = var.target_node
clone = "ubuntu-22.04-cloud"
full_clone = true
cores = 2
memory = 4096
ipconfig0 = "ip=192.168.1.${20 + count.index}/24,gw=192.168.1.1"
disk {
size = "32G"
storage = "local-lvm"
type = "scsi"
}
network {
model = "virtio"
bridge = "vmbr0"
}
}
# Output all IP addresses
output "web_cluster_ips" {
value = proxmox_vm_qemu.web_cluster[*].default_ipv4_address
} State Management for Homelabs
Understanding OpenTofu State
OpenTofu tracks managed resources in a state file (terraform.tfstate). This file:
- Maps configuration to real-world resources
- Stores metadata (resource IDs, attributes)
- Enables updates and deletions
- Contains sensitive data (DO NOT COMMIT TO GIT)
Default state location:
project/
├── terraform.tfstate # Current state
├── terraform.tfstate.backup # Previous state Local State Management
Advantages:
- Simple setup
- No additional infrastructure
- Works offline
- Fast operations
Disadvantages:
- Not suitable for teams
- No locking (concurrent access risks)
- Manual backup management
- State on single machine
Best practices for local state:
# Backup state before major changes
cp terraform.tfstate terraform.tfstate.$(date +%Y%m%d-%H%M%S)
# Store backups separately
mkdir -p ~/homelab-infrastructure/state-backups
cp terraform.tfstate ~/homelab-infrastructure/state-backups/terraform.tfstate.$(date +%Y%m%d-%H%M%S)
# Encrypt state backups (contains secrets)
gpg --encrypt --recipient [email protected] terraform.tfstate Remote State Backends
For better reliability and team collaboration, use remote state backends.
Option 1: S3-Compatible Backend (Minio)
Install Minio in your homelab:
docker run -d -p 9000:9000 -p 9001:9001 --name minio -v /mnt/data/minio:/data -e "MINIO_ROOT_USER=admin" -e "MINIO_ROOT_PASSWORD=your-secure-password" quay.io/minio/minio server /data --console-address ":9001" Configure backend:
terraform {
backend "s3" {
bucket = "opentofu-state"
key = "homelab/terraform.tfstate"
region = "us-east-1" # Required but ignored
endpoint = "http://minio.homelab.local:9000"
skip_credentials_validation = true
skip_metadata_api_check = true
skip_region_validation = true
force_path_style = true
access_key = "your-access-key"
secret_key = "your-secret-key"
}
} Option 2: HTTP Backend (GitLab/Gitea)
If you run self-hosted Git:
terraform {
backend "http" {
address = "https://gitlab.homelab.local/api/v4/projects/123/terraform/state/homelab"
lock_address = "https://gitlab.homelab.local/api/v4/projects/123/terraform/state/homelab/lock"
unlock_address = "https://gitlab.homelab.local/api/v4/projects/123/terraform/state/homelab/lock"
username = "your-username"
password = var.gitlab_token
lock_method = "POST"
unlock_method = "DELETE"
retry_wait_min = 5
}
} Option 3: Local with Version Control
Compromise solution - local state with Git LFS:
# Install Git LFS
git lfs install
# Track state files
git lfs track "*.tfstate"
# Add .gitattributes
git add .gitattributes
git commit -m "Track state files with Git LFS"
# Encrypt state before committing
git-crypt init State File Encryption
Using SOPS (Secrets OPerationS):
# Install SOPS
brew install sops # macOS
# or
sudo apt install sops # Ubuntu
# Create .sops.yaml
cat > .sops.yaml << 'EOF'
creation_rules:
- path_regex: .tfstate$
encrypted_regex: '^(data|modules|resources)$'
pgp: 'YOUR_PGP_KEY_FINGERPRINT'
EOF
# Encrypt state
sops -e terraform.tfstate > terraform.tfstate.enc
# Decrypt when needed
sops -d terraform.tfstate.enc > terraform.tfstate State Operations
Inspect state:
# List all resources
tofu state list
# Show specific resource
tofu state show proxmox_vm_qemu.web_server
# Show all state
tofu show Modify state:
# Remove resource from state (doesn't destroy resource)
tofu state rm proxmox_vm_qemu.old_server
# Move/rename resource
tofu state mv proxmox_vm_qemu.old_name proxmox_vm_qemu.new_name
# Import existing resource
tofu import proxmox_vm_qemu.existing_vm pve:qemu/100 Recover from state issues:
# Pull remote state
tofu state pull > terraform.tfstate
# Push local state to remote
tofu state push terraform.tfstate
# Refresh state from actual infrastructure
tofu refresh Modular Infrastructure Design
Creating Reusable Modules
Modules enable code reuse and organization. A module is just a directory with .tf files.
Module structure:
modules/
└── proxmox-vm/
├── main.tf # Resources
├── variables.tf # Input variables
├── outputs.tf # Output values
└── README.md # Documentation Example: VM module
modules/proxmox-vm/variables.tf:
variable "vm_name" {
description = "Name of the VM"
type = string
}
variable "target_node" {
description = "Proxmox node name"
type = string
}
variable "cores" {
description = "Number of CPU cores"
type = number
default = 2
}
variable "memory" {
description = "Memory in MB"
type = number
default = 4096
}
variable "disk_size" {
description = "Disk size (e.g., '32G')"
type = string
default = "32G"
}
variable "storage" {
description = "Storage pool name"
type = string
default = "local-lvm"
}
variable "template_name" {
description = "Cloud-init template name"
type = string
}
variable "ip_address" {
description = "Static IP address (CIDR notation)"
type = string
}
variable "gateway" {
description = "Network gateway"
type = string
}
variable "ssh_keys" {
description = "SSH public keys"
type = list(string)
} modules/proxmox-vm/main.tf:
resource "proxmox_vm_qemu" "vm" {
name = var.vm_name
target_node = var.target_node
clone = var.template_name
full_clone = true
cores = var.cores
memory = var.memory
agent = 1
disk {
size = var.disk_size
storage = var.storage
type = "scsi"
iothread = 1
}
network {
model = "virtio"
bridge = "vmbr0"
}
ipconfig0 = "ip=${var.ip_address},gw=${var.gateway}"
nameserver = var.gateway
searchdomain = "homelab.local"
sshkeys = join("\n", var.ssh_keys)
ciuser = "ubuntu"
lifecycle {
ignore_changes = [
network,
]
}
} modules/proxmox-vm/outputs.tf:
output "vm_id" {
description = "VM ID"
value = proxmox_vm_qemu.vm.id
}
output "vm_name" {
description = "VM name"
value = proxmox_vm_qemu.vm.name
}
output "ip_address" {
description = "IP address"
value = proxmox_vm_qemu.vm.default_ipv4_address
} Using Modules
Root module using the VM module:
module "web_servers" {
source = "./modules/proxmox-vm"
count = 3
vm_name = "web-${count.index + 1}"
target_node = "pve-node-01"
cores = 2
memory = 4096
disk_size = "32G"
storage = "local-lvm"
template_name = "ubuntu-22.04-cloud"
ip_address = "192.168.1.${20 + count.index}/24"
gateway = "192.168.1.1"
ssh_keys = [file("~/.ssh/id_rsa.pub")]
}
module "database_server" {
source = "./modules/proxmox-vm"
vm_name = "postgres-01"
target_node = "pve-node-01"
cores = 4
memory = 16384
disk_size = "100G"
storage = "local-lvm"
template_name = "ubuntu-22.04-cloud"
ip_address = "192.168.1.30/24"
gateway = "192.168.1.1"
ssh_keys = [file("~/.ssh/id_rsa.pub")]
}
# Access module outputs
output "web_cluster_ips" {
value = module.web_servers[*].ip_address
}
output "database_ip" {
value = module.database_server.ip_address
} Multi-Environment Deployments
Directory structure:
homelab-infrastructure/
├── modules/
│ └── proxmox-vm/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ └── prod/
│ ├── main.tf
│ ├── variables.tf
│ └── terraform.tfvars
└── shared/
├── providers.tf
└── versions.tf environments/dev/main.tf:
module "web_server" {
source = "../../modules/proxmox-vm"
vm_name = "${var.environment}-web-01"
target_node = var.target_node
cores = 1 # Dev uses fewer resources
memory = 2048
disk_size = "20G"
storage = var.storage
template_name = var.template_name
ip_address = var.web_ip
gateway = var.gateway
ssh_keys = var.ssh_keys
} environments/dev/terraform.tfvars:
environment = "dev"
target_node = "pve-node-01"
storage = "local-lvm"
template_name = "ubuntu-22.04-cloud"
web_ip = "192.168.1.50/24"
gateway = "192.168.1.1" environments/prod/terraform.tfvars:
environment = "prod"
target_node = "pve-node-01"
storage = "fast-ssd"
template_name = "ubuntu-22.04-cloud"
web_ip = "192.168.1.20/24"
gateway = "192.168.1.1" Deploy to specific environment:
# Deploy to dev
cd environments/dev
tofu init
tofu apply
# Deploy to production
cd environments/prod
tofu init
tofu apply Migrating from Terraform to OpenTofu
Pre-Migration Checklist
- Backup all Terraform state files
- Document current Terraform version
- List all providers and their versions
- Test migration in dev/staging first
- Review OpenTofu compatibility
Migration Steps
Step 1: Install OpenTofu
Follow installation instructions from earlier in this guide.
Step 2: Verify Compatibility
# Check current Terraform version
terraform version
# Ensure you're on Terraform <= 1.5.x for easiest migration
# If using 1.6+, some features may not be compatible yet Step 3: Update Backend Configuration (if needed)
OpenTofu uses the same backend configuration, but update registry URLs if you’re using the Terraform Registry:
terraform {
required_providers {
# OLD (Terraform Registry)
proxmox = {
source = "telmate/proxmox"
version = "~> 2.9"
}
# NEW (OpenTofu Registry) - often the same
# No changes needed for most providers
}
} Step 4: Migrate State
# Backup existing state
cp terraform.tfstate terraform.tfstate.backup.$(date +%Y%m%d)
# Initialize with OpenTofu
tofu init -migrate-state
# OpenTofu will ask: "Do you want to copy existing state to the new backend?"
# Answer: yes
# Verify migration
tofu state list
# Compare with Terraform
terraform state list Step 5: Test Planning
# Run plan to verify no unexpected changes
tofu plan
# If plan shows changes, investigate before applying
# Expected: None (if infrastructure unchanged)
# Unexpected: Provider differences, state format changes Step 6: Update CI/CD Pipelines
Replace terraform commands with tofu:
# GitHub Actions example
- name: OpenTofu Apply
run: |
tofu init
tofu plan
tofu apply -auto-approve Step 7: Update Documentation
- Update README files
- Change references from Terraform to OpenTofu
- Update team runbooks
Handling Provider Differences
Most providers work identically, but verify:
# Check provider availability
tofu providers
# If provider not found in OpenTofu registry, use explicit source
terraform {
required_providers {
custom_provider = {
source = "registry.terraform.io/custom/provider"
# Explicitly reference Terraform registry if needed
}
}
} Rollback Plan
If migration fails, rollback is simple:
# Restore original state
cp terraform.tfstate.backup.20251216 terraform.tfstate
# Continue using Terraform
terraform init
terraform plan Advanced Patterns and Best Practices
Workspaces for Environment Management
Workspaces provide state isolation within a single configuration:
# Create workspaces
tofu workspace new dev
tofu workspace new staging
tofu workspace new prod
# List workspaces
tofu workspace list
# Switch workspace
tofu workspace select dev
# Show current workspace
tofu workspace show Use workspace in configuration:
locals {
environment = terraform.workspace
# Environment-specific values
vm_count = {
dev = 1
staging = 2
prod = 5
}
memory = {
dev = 2048
staging = 4096
prod = 8192
}
}
resource "proxmox_vm_qemu" "web" {
count = local.vm_count[local.environment]
name = "${local.environment}-web-${count.index + 1}"
memory = local.memory[local.environment]
# ... other configuration
} Dynamic Blocks for Flexibility
Dynamic blocks generate repeated nested blocks:
variable "additional_disks" {
type = list(object({
size = string
storage = string
}))
default = [
{ size = "50G", storage = "local-lvm" },
{ size = "100G", storage = "slow-hdd" },
]
}
resource "proxmox_vm_qemu" "storage_server" {
name = "storage-01"
# Primary disk
disk {
size = "32G"
storage = "fast-ssd"
type = "scsi"
}
# Additional disks dynamically
dynamic "disk" {
for_each = var.additional_disks
content {
size = disk.value.size
storage = disk.value.storage
type = "scsi"
}
}
} Terraform Functions for Power Users
File and template functions:
# Read file contents
locals {
ssh_keys = file("~/.ssh/id_rsa.pub")
}
# Template file with variables
data "template_file" "cloud_init" {
template = file("${path.module}/cloud-init.tpl")
vars = {
hostname = "web-01"
domain = "homelab.local"
}
}
# YAML/JSON encoding
locals {
cloud_init_config = yamlencode({
users = [
{
name = "admin"
groups = ["sudo"]
}
]
})
} Collection functions:
variable "vm_configs" {
type = map(object({
cores = number
memory = number
}))
default = {
web = { cores = 2, memory = 4096 }
db = { cores = 4, memory = 8192 }
}
}
# Create VMs from map
resource "proxmox_vm_qemu" "servers" {
for_each = var.vm_configs
name = each.key
cores = each.value.cores
memory = each.value.memory
}
# Lookup function
locals {
web_memory = lookup(var.vm_configs, "web", { cores = 2, memory = 2048 }).memory
} String manipulation:
locals {
# Join list into string
all_ips = join(",", proxmox_vm_qemu.web[*].default_ipv4_address)
# Split string into list
zones = split(",", "us-east-1a,us-east-1b,us-east-1c")
# Format strings
vm_name = format("web-%03d", 1) # Produces "web-001"
# Replace in string
safe_name = replace(var.environment, "_", "-")
} Provisioners (Use Sparingly)
Provisioners run scripts after resource creation. Use Ansible/cloud-init instead when possible.
resource "proxmox_vm_qemu" "web" {
name = "web-01"
# ... configuration
# Run local script after creation
provisioner "local-exec" {
command = "echo ${self.default_ipv4_address} >> inventory.txt"
}
# Run script on remote VM
provisioner "remote-exec" {
inline = [
"sudo apt-get update",
"sudo apt-get install -y nginx",
]
connection {
type = "ssh"
user = "ubuntu"
private_key = file("~/.ssh/id_rsa")
host = self.default_ipv4_address
}
}
# Cleanup on destroy
provisioner "local-exec" {
when = destroy
command = "echo 'VM destroyed' >> destroy.log"
}
} Import Existing Infrastructure
Bring existing resources under OpenTofu management:
# Find resource ID (Proxmox VM ID)
# VM ID 100 on node "pve"
# Import into OpenTofu
tofu import proxmox_vm_qemu.existing_vm pve:qemu/100
# Generate configuration from state
tofu show -json | jq '.values.root_module.resources[] | select(.address=="proxmox_vm_qemu.existing_vm")'
# Manually create matching configuration
# Then verify with:
tofu plan # Should show no changes Troubleshooting Common Issues
Provider Connection Errors
Error: Error creating VM: 401 Unauthorized
Solutions:
# Verify API token
curl -k https://proxmox.example.com:8006/api2/json/access/ticket -d "username=root@pam" -d "password=yourpassword"
# Check token permissions in Proxmox UI
# Datacenter → Permissions → API Tokens
# Ensure "Privilege Separation" is unchecked
# Test API access
curl -k -H "Authorization: PVEAPIToken=root@pam!terraform=YOUR-SECRET" https://proxmox.example.com:8006/api2/json/nodes State Lock Errors
Error: Error locking state: state blob is already locked
Solutions:
# Force unlock (use with caution)
tofu force-unlock LOCK_ID
# If using S3 backend, manually remove lock
aws s3api delete-object --bucket terraform-state --key homelab/terraform.tfstate.lock
# Prevent locks: use different workspaces for parallel operations Resource Already Exists
Error: A resource with this name already exists
Solutions:
# Option 1: Import existing resource
tofu import proxmox_vm_qemu.conflicting_vm pve:qemu/VM_ID
# Option 2: Rename in configuration
# Change 'name' attribute to something unique
# Option 3: Delete from Proxmox manually
# Then rerun tofu apply Cloud-Init Not Working
Symptoms: VM created but cloud-init doesn’t configure IP/SSH
Solutions:
# Verify template has cloud-init disk
qm config TEMPLATE_ID | grep ide2
# Should show: ide2: local-lvm:cloudinit
# Check cloud-init logs on VM
ssh ubuntu@VM_IP
sudo cat /var/log/cloud-init.log
sudo cloud-init status
# Rebuild cloud-init
sudo cloud-init clean
sudo cloud-init init Plan Shows Unexpected Changes
Symptom: tofu plan shows changes on every run
Solutions:
# Use lifecycle ignore_changes for attributes that drift
resource "proxmox_vm_qemu" "example" {
name = "example"
lifecycle {
ignore_changes = [
network, # Proxmox may modify MAC address
disk, # Disk size might show differently
ipconfig0, # DHCP vs static inconsistencies
]
}
}
# Or refresh state to match reality
tofu refresh Conclusion and Next Steps
OpenTofu brings infrastructure-as-code to your homelab with true open-source licensing, community governance, and a commitment to remaining free forever. You’ve learned how to:
- Install and configure OpenTofu
- Write HCL configurations for Proxmox infrastructure
- Manage state files securely
- Create reusable modules
- Migrate from Terraform
- Implement advanced patterns
Key Takeaways
- Declarative IaC makes homelab infrastructure reproducible and version-controlled
- OpenTofu is 100% compatible with Terraform 1.5.x configurations
- State management is critical - backup, encrypt, and protect your state files
- Modules enable reuse - write once, deploy many times
- Start simple - one VM, then expand to complex multi-environment deployments
Recommended Next Steps
- Deploy your first VM with OpenTofu today
- Version control everything - create Git repo for your infrastructure
- Build reusable modules for common patterns in your homelab
- Automate with CI/CD - use Gitea Actions or Woodpecker CI
- Explore other providers - Docker, Kubernetes, DNS, etc.
- Join the community - contribute to OpenTofu development
Related Guides
- Gitea Actions - CI/CD for your infrastructure code
- Ansible - Configuration management to complement OpenTofu
- Proxmox Advanced - Deep dive into Proxmox features
- Kubernetes with K3s - Manage K8s clusters with OpenTofu
Resources
- Official Documentation: opentofu.org/docs
- Provider Registry: github.com/opentofu/registry
- Community Forum: discuss.opentofu.org
- GitHub: github.com/opentofu/opentofu
- Proxmox Provider: github.com/Telmate/terraform-provider-proxmox
Document Version: 1.0 Last Updated: December 16, 2025 Based on: OpenTofu 1.6.x, Proxmox VE 8.x Estimated Completion Time: 90 minutes (installation + first deployment)
Happy infrastructure coding!