Terraform for Homelabs: Infrastructure as Code
Manage your homelab with Terraform. Providers for Proxmox, Docker, and cloud services.
Terraform transforms homelab management from manual clicking and scripting into declarative, version-controlled infrastructure as code (IaC). This guide covers comprehensive Terraform usage for Proxmox, including VMs, LXC containers, networking, storage, and advanced patterns for production-ready homelab automation.
Understanding Infrastructure as Code
What is Infrastructure as Code?
Infrastructure as Code treats your infrastructure configuration as software code:
- Declarative: Describe desired state, not steps to achieve it
- Version Controlled: Track changes with Git
- Repeatable: Rebuild infrastructure consistently
- Testable: Validate before applying
- Documented: Code serves as documentation
Why Terraform for Homelabs?
Benefits:
- Manage Proxmox VMs and containers programmatically
- Version control your entire homelab configuration
- Rebuild infrastructure quickly after failures
- Test changes before applying
- Share and reuse configurations
- Integrate with CI/CD pipelines
Alternatives:
- Ansible: Better for configuration management, not infrastructure provisioning
- Pulumi: Uses programming languages instead of HCL
- CloudFormation: AWS-specific
- Manual CLI/API: Error-prone, not repeatable
Part 1: Installation and Setup
Prerequisites
# Update system
sudo apt update && sudo apt upgrade -y
# Install dependencies
sudo apt install -y wget unzip git Install Terraform
Method 1: Official HashiCorp Repository (Recommended)
# Add HashiCorp GPG key
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
# Add repository
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
# Install Terraform
sudo apt update && sudo apt install terraform
# Verify installation
terraform version Method 2: Binary Installation
# Download latest version (check https://www.terraform.io/downloads)
cd /tmp
wget https://releases.hashicorp.com/terraform/1.6.6/terraform_1.6.6_linux_amd64.zip
# Extract and install
unzip terraform_1.6.6_linux_amd64.zip
sudo mv terraform /usr/local/bin/
# Verify
terraform version Configure Terraform Shell Completion
# Bash
terraform -install-autocomplete
# Or manually
cat >> ~/.bashrc << 'EOF'
complete -C /usr/local/bin/terraform terraform
EOF
source ~/.bashrc Install Terraform-Docs (Optional but Recommended)
# Install terraform-docs for documentation generation
cd /tmp
curl -Lo terraform-docs.tar.gz https://github.com/terraform-docs/terraform-docs/releases/download/v0.17.0/terraform-docs-v0.17.0-linux-amd64.tar.gz
tar -xzf terraform-docs.tar.gz
sudo mv terraform-docs /usr/local/bin/
terraform-docs --version Part 2: Proxmox Provider Setup
Proxmox API User Creation
Terraform needs API access to Proxmox:
# SSH into Proxmox host
ssh root@YOUR-PROXMOX-IP
# Create Terraform user
pveum user add terraform@pve --password YOUR-SECURE-PASSWORD
# Create Terraform role with necessary permissions
pveum role add TerraformRole -privs "VM.Allocate VM.Clone VM.Config.CDROM VM.Config.CPU VM.Config.Cloudinit VM.Config.Disk VM.Config.HWType VM.Config.Memory VM.Config.Network VM.Config.Options VM.Monitor VM.Audit VM.PowerMgmt Datastore.AllocateSpace Datastore.Audit Pool.Allocate Sys.Audit Sys.Console Sys.Modify SDN.Use"
# Assign role to user
pveum aclmod / -user terraform@pve -role TerraformRole
# Verify
pveum user list Alternative: API Token (More Secure)
# Create API token
pveum user token add terraform@pve terraform-token --privsep=0
# Output will show: full-tokenid=terraform@pve!terraform-token
# Save the secret shown - you cannot retrieve it later
# Assign permissions to token
pveum aclmod / -token terraform@pve!terraform-token -role TerraformRole Project Structure
# Create Terraform project directory
mkdir -p ~/terraform-homelab
cd ~/terraform-homelab
# Recommended structure:
mkdir -p {modules,environments,templates}
# Directory structure:
# terraform-homelab/
# ├── main.tf # Main configuration
# ├── variables.tf # Variable declarations
# ├── outputs.tf # Output declarations
# ├── terraform.tfvars # Variable values (add to .gitignore)
# ├── versions.tf # Provider version constraints
# ├── modules/ # Reusable modules
# │ ├── proxmox-vm/
# │ ├── proxmox-lxc/
# │ └── network/
# ├── environments/ # Environment-specific configs
# │ ├── prod/
# │ ├── dev/
# │ └── test/
# └── templates/ # Cloud-init, scripts
# ├── cloud-init/
# └── scripts/ Part 3: Basic Terraform Configuration
versions.tf - Provider Configuration
terraform {
required_version = ">= 1.6.0"
required_providers {
proxmox = {
source = "telmate/proxmox"
version = "~> 2.9.14"
}
}
# Optional: Remote state storage
# backend "s3" {
# bucket = "terraform-state"
# key = "homelab/terraform.tfstate"
# region = "us-east-1"
# }
}
provider "proxmox" {
pm_api_url = var.proxmox_api_url
pm_api_token_id = var.proxmox_api_token_id
pm_api_token_secret = var.proxmox_api_token_secret
pm_tls_insecure = true # Set to false in production with valid certs
# Alternative: Username/password authentication
# pm_user = var.proxmox_user
# pm_password = var.proxmox_password
pm_log_enable = true
pm_log_file = "terraform-plugin-proxmox.log"
pm_debug = true
pm_log_levels = {
_default = "debug"
_capturelog = ""
}
} variables.tf - Variable Declarations
variable "proxmox_api_url" {
description = "Proxmox API URL"
type = string
default = "https://YOUR-PROXMOX-IP:8006/api2/json"
}
variable "proxmox_api_token_id" {
description = "Proxmox API Token ID"
type = string
sensitive = true
}
variable "proxmox_api_token_secret" {
description = "Proxmox API Token Secret"
type = string
sensitive = true
}
variable "proxmox_node" {
description = "Proxmox node name"
type = string
default = "pve"
}
variable "vm_template_name" {
description = "Name of the VM template to clone"
type = string
default = "ubuntu-22.04-template"
}
variable "ssh_public_key" {
description = "SSH public key for cloud-init"
type = string
}
variable "default_gateway" {
description = "Default gateway for VMs"
type = string
default = "192.168.1.1"
}
variable "dns_servers" {
description = "DNS servers for VMs"
type = list(string)
default = ["1.1.1.1", "8.8.8.8"]
}
variable "vm_specs" {
description = "VM specifications"
type = map(object({
cores = number
memory = number
disk = string
storage = string
ip = string
}))
default = {
web = {
cores = 2
memory = 4096
disk = "32G"
storage = "local-lvm"
ip = "192.168.1.100/24"
}
db = {
cores = 4
memory = 8192
disk = "100G"
storage = "local-lvm"
ip = "192.168.1.101/24"
}
}
} terraform.tfvars - Variable Values
# Add to .gitignore - contains secrets!
proxmox_api_url = "https://192.168.1.10:8006/api2/json"
proxmox_api_token_id = "terraform@pve!terraform-token"
proxmox_api_token_secret = "your-secret-token-here"
proxmox_node = "pve"
ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC... your-key-here"
default_gateway = "192.168.1.1"
dns_servers = ["192.168.1.1", "1.1.1.1"]
vm_specs = {
web01 = {
cores = 2
memory = 4096
disk = "32G"
storage = "local-lvm"
ip = "192.168.1.100/24"
}
web02 = {
cores = 2
memory = 4096
disk = "32G"
storage = "local-lvm"
ip = "192.168.1.101/24"
}
db01 = {
cores = 4
memory = 8192
disk = "100G"
storage = "local-lvm"
ip = "192.168.1.110/24"
}
} main.tf - Resource Definitions
# Create VMs from template
resource "proxmox_vm_qemu" "web_servers" {
for_each = var.vm_specs
name = each.key
target_node = var.proxmox_node
desc = "Managed by Terraform - ${each.key}"
# Clone from template
clone = var.vm_template_name
full_clone = true
# VM settings
cores = each.value.cores
sockets = 1
memory = each.value.memory
# Boot order
boot = "order=scsi0"
# Enable QEMU agent
agent = 1
# OS type
os_type = "cloud-init"
# CPU type
cpu = "host"
# BIOS
bios = "ovmf"
# EFI disk
efidisk {
storage = each.value.storage
}
# Disk configuration
disks {
scsi {
scsi0 {
disk {
size = each.value.disk
storage = each.value.storage
iothread = true
}
}
}
ide {
ide2 {
cloudinit {
storage = each.value.storage
}
}
}
}
# Network configuration
network {
model = "virtio"
bridge = "vmbr0"
}
# Cloud-init configuration
ipconfig0 = "ip=${each.value.ip},gw=${var.default_gateway}"
nameserver = join(" ", var.dns_servers)
ciuser = "ubuntu"
sshkeys = var.ssh_public_key
# Lifecycle
lifecycle {
ignore_changes = [
network,
disk,
]
}
# Wait for cloud-init to complete
# Requires QEMU agent
automatic_reboot = true
}
# LXC Container Example
resource "proxmox_lxc" "containers" {
for_each = {
docker-host = {
cores = 2
memory = 4096
disk = "20G"
ip = "192.168.1.120/24"
}
}
hostname = each.key
target_node = var.proxmox_node
ostemplate = "local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst"
# Container settings
cores = each.value.cores
memory = each.value.memory
swap = 512
# Features
unprivileged = true
features {
nesting = true
keyctl = true
}
# Disk
rootfs {
storage = "local-lvm"
size = each.value.disk
}
# Network
network {
name = "eth0"
bridge = "vmbr0"
ip = each.value.ip
gw = var.default_gateway
}
# SSH key
ssh_public_keys = var.ssh_public_key
# Startup behavior
onboot = true
start = true
} outputs.tf - Output Values
output "vm_ip_addresses" {
description = "IP addresses of created VMs"
value = {
for k, v in proxmox_vm_qemu.web_servers :
k => v.default_ipv4_address
}
}
output "vm_ids" {
description = "VM IDs in Proxmox"
value = {
for k, v in proxmox_vm_qemu.web_servers :
k => v.vmid
}
}
output "lxc_ip_addresses" {
description = "IP addresses of LXC containers"
value = {
for k, v in proxmox_lxc.containers :
k => v.network[0].ip
}
} Part 4: Working with Terraform
Initialize Terraform
# Initialize - downloads providers
terraform init
# Output:
# Initializing provider plugins...
# - Finding telmate/proxmox versions matching "~> 2.9.14"...
# - Installing telmate/proxmox v2.9.14... Validate Configuration
# Check syntax
terraform validate
# Format code
terraform fmt -recursive
# Show planned changes
terraform plan
# Save plan to file
terraform plan -out=tfplan Apply Configuration
# Apply changes
terraform apply
# Review plan and type 'yes' to confirm
# Or auto-approve (use with caution)
terraform apply -auto-approve
# Apply specific resource
terraform apply -target=proxmox_vm_qemu.web_servers["web01"] View State
# List resources in state
terraform state list
# Show specific resource
terraform state show proxmox_vm_qemu.web_servers["web01"]
# Pull current state
terraform state pull > terraform.tfstate.backup Destroy Resources
# Destroy everything
terraform destroy
# Destroy specific resource
terraform destroy -target=proxmox_vm_qemu.web_servers["web01"]
# Preview destruction
terraform plan -destroy Part 5: Advanced Patterns
Module: Reusable VM Configuration
Create modules/proxmox-vm/main.tf:
variable "vm_name" {
type = string
}
variable "target_node" {
type = string
}
variable "template" {
type = string
}
variable "cores" {
type = number
default = 2
}
variable "memory" {
type = number
default = 4096
}
variable "disk_size" {
type = string
default = "32G"
}
variable "storage" {
type = string
default = "local-lvm"
}
variable "ip_address" {
type = string
}
variable "gateway" {
type = string
}
variable "ssh_keys" {
type = string
}
resource "proxmox_vm_qemu" "vm" {
name = var.vm_name
target_node = var.target_node
clone = var.template
cores = var.cores
memory = var.memory
agent = 1
os_type = "cloud-init"
cpu = "host"
disks {
scsi {
scsi0 {
disk {
size = var.disk_size
storage = var.storage
}
}
}
}
network {
model = "virtio"
bridge = "vmbr0"
}
ipconfig0 = "ip=${var.ip_address},gw=${var.gateway}"
sshkeys = var.ssh_keys
}
output "vm_id" {
value = proxmox_vm_qemu.vm.vmid
}
output "ip_address" {
value = var.ip_address
} Use module in main.tf:
module "web_servers" {
source = "./modules/proxmox-vm"
for_each = {
web01 = { ip = "192.168.1.100/24", cores = 2, memory = 4096 }
web02 = { ip = "192.168.1.101/24", cores = 2, memory = 4096 }
}
vm_name = each.key
target_node = var.proxmox_node
template = var.vm_template_name
cores = each.value.cores
memory = each.value.memory
disk_size = "32G"
storage = "local-lvm"
ip_address = each.value.ip
gateway = var.default_gateway
ssh_keys = var.ssh_public_key
} Cloud-Init Configuration
Create templates/cloud-init/user-data.yaml:
#cloud-config
package_update: true
package_upgrade: true
packages:
- qemu-guest-agent
- curl
- wget
- git
- vim
- htop
# Run commands on first boot
runcmd:
- systemctl enable qemu-guest-agent
- systemctl start qemu-guest-agent
- timedatectl set-timezone America/New_York
# Configure users
users:
- name: admin
groups: sudo
shell: /bin/bash
sudo: ['ALL=(ALL) NOPASSWD:ALL']
ssh_authorized_keys:
- YOUR-SSH-PUBLIC-KEY
# Install Docker
write_files:
- path: /tmp/install-docker.sh
permissions: '0755'
content: |
#!/bin/bash
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
usermod -aG docker admin
systemctl enable docker
runcmd:
- /tmp/install-docker.sh
# Reboot after cloud-init completes
power_state:
mode: reboot
timeout: 300
condition: True Use with Terraform:
resource "proxmox_vm_qemu" "vm" {
# ... other config ...
cicustom = "user=local:snippets/user-data.yaml"
} Dynamic VM Scaling
variable "web_server_count" {
description = "Number of web servers"
type = number
default = 3
}
resource "proxmox_vm_qemu" "web_cluster" {
count = var.web_server_count
name = "web-${count.index + 1}"
target_node = var.proxmox_node
clone = var.vm_template_name
cores = 2
memory = 4096
# ... disk and network config ...
ipconfig0 = "ip=192.168.1.${100 + count.index}/24,gw=${var.default_gateway}"
sshkeys = var.ssh_public_key
}
# Output all IPs
output "web_cluster_ips" {
value = proxmox_vm_qemu.web_cluster[*].default_ipv4_address
} Data Sources for Existing Resources
# Query existing VM template
data "proxmox_template" "ubuntu_template" {
target_node = var.proxmox_node
filter {
name = "name"
values = ["ubuntu-22.04-template"]
}
}
# Use in resource
resource "proxmox_vm_qemu" "vm" {
clone = data.proxmox_template.ubuntu_template.name
# ... rest of config ...
} Conditional Resource Creation
variable "create_monitoring_vm" {
type = bool
default = false
}
resource "proxmox_vm_qemu" "monitoring" {
count = var.create_monitoring_vm ? 1 : 0
name = "monitoring"
# ... rest of config ...
} Remote State Storage
Use S3-compatible storage for shared state:
terraform {
backend "s3" {
bucket = "terraform-state"
key = "homelab/terraform.tfstate"
region = "us-east-1"
endpoint = "https://s3.homelab.local"
skip_credentials_validation = true
skip_metadata_api_check = true
force_path_style = true
}
} Or use Terraform Cloud:
terraform {
cloud {
organization = "my-homelab"
workspaces {
name = "homelab-infrastructure"
}
}
} Part 6: Integration with Other Tools
Ansible Post-Provisioning
resource "proxmox_vm_qemu" "vm" {
# ... VM configuration ...
provisioner "local-exec" {
command = <<-EOT
sleep 60
ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook \
-i '${self.default_ipv4_address},' \
-u ubuntu \
--private-key ~/.ssh/id_rsa \
playbook.yml
EOT
}
} Packer Integration
# Build template with Packer first
resource "null_resource" "packer_build" {
provisioner "local-exec" {
command = "packer build ubuntu-template.pkr.hcl"
}
}
# Then use template in Terraform
resource "proxmox_vm_qemu" "vm" {
depends_on = [null_resource.packer_build]
clone = "ubuntu-packer-template"
# ... rest of config ...
} GitLab CI/CD Pipeline
Create .gitlab-ci.yml:
stages:
- validate
- plan
- apply
variables:
TF_ROOT: ${CI_PROJECT_DIR}
TF_ADDRESS: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/production
cache:
paths:
- ${TF_ROOT}/.terraform
before_script:
- cd ${TF_ROOT}
- terraform init
validate:
stage: validate
script:
- terraform validate
- terraform fmt -check
plan:
stage: plan
script:
- terraform plan -out=tfplan
artifacts:
paths:
- tfplan
apply:
stage: apply
script:
- terraform apply -auto-approve tfplan
when: manual
only:
- main
dependencies:
- plan GitHub Actions
Create .github/workflows/terraform.yml:
name: Terraform
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.6.6
- name: Terraform Init
run: terraform init
env:
TF_VAR_proxmox_api_token_id: ${{ secrets.PROXMOX_TOKEN_ID }}
TF_VAR_proxmox_api_token_secret: ${{ secrets.PROXMOX_TOKEN_SECRET }}
- name: Terraform Format
run: terraform fmt -check
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
run: terraform plan
env:
TF_VAR_proxmox_api_token_id: ${{ secrets.PROXMOX_TOKEN_ID }}
TF_VAR_proxmox_api_token_secret: ${{ secrets.PROXMOX_TOKEN_SECRET }}
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve
env:
TF_VAR_proxmox_api_token_id: ${{ secrets.PROXMOX_TOKEN_ID }}
TF_VAR_proxmox_api_token_secret: ${{ secrets.PROXMOX_TOKEN_SECRET }} Part 7: Best Practices
State Management
# Always use remote state for teams
# Never commit terraform.tfstate to Git
# .gitignore
*.tfstate
*.tfstate.*
.terraform/
terraform.tfvars
*.tfplan
# Use state locking
terraform {
backend "s3" {
bucket = "terraform-state"
key = "homelab/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
} Security Best Practices
# Never hardcode credentials
# Use environment variables or secret management
# BAD:
# provider "proxmox" {
# pm_api_token_id = "terraform@pve!token"
# pm_api_token_secret = "secret-value"
# }
# GOOD:
provider "proxmox" {
pm_api_token_id = var.proxmox_api_token_id
pm_api_token_secret = var.proxmox_api_token_secret
}
# Use sensitive variable flag
variable "proxmox_api_token_secret" {
type = string
sensitive = true
}
# Encrypt state files
# Use HTTPS for remote state
# Implement least-privilege access Code Organization
# Project structure for large deployments:
terraform-homelab/
├── environments/
│ ├── prod/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── terraform.tfvars
│ │ └── backend.tf
│ ├── dev/
│ │ └── ...
│ └── staging/
│ └── ...
├── modules/
│ ├── compute/
│ │ ├── vm/
│ │ └── lxc/
│ ├── network/
│ └── storage/
├── policies/
│ └── sentinel/
└── scripts/
└── helpers/ Resource Naming
# Use consistent naming conventions
locals {
name_prefix = "${var.environment}-${var.project}"
common_tags = {
Environment = var.environment
Project = var.project
ManagedBy = "Terraform"
Owner = var.owner
}
}
resource "proxmox_vm_qemu" "vm" {
name = "${local.name_prefix}-web-${count.index + 1}"
desc = jsonencode(local.common_tags)
# ... rest of config ...
} Testing Strategy
# Use terraform-compliance for policy testing
pip install terraform-compliance
# Create tests/security.feature
Feature: Security requirements
Scenario: Ensure VMs use QEMU agent
Given I have proxmox_vm_qemu defined
Then it must contain agent
And its value must be 1
# Run tests
terraform-compliance -f tests/ -p tfplan.json
# Validate with OPA (Open Policy Agent)
# Use Sentinel policies for Terraform Cloud Part 8: Troubleshooting
Common Issues
Issue: Provider Plugin Not Found
# Solution: Re-initialize
rm -rf .terraform .terraform.lock.hcl
terraform init Issue: State Lock Error
# Solution: Force unlock (use with caution)
terraform force-unlock LOCK_ID
# Better: Wait for other process to finish
# Or check DynamoDB/state backend for stale locks Issue: VM Clone Fails
# Check template exists
# Verify Proxmox API credentials
# Check Proxmox logs
tail -f /var/log/pve/tasks/active
# Enable debug logging
export TF_LOG=DEBUG
terraform apply Issue: Drift Detection
# Check for configuration drift
terraform plan -refresh-only
# Import manually created resources
terraform import proxmox_vm_qemu.existing pve/qemu/100
# Show current state
terraform show Performance Optimization
# Use parallelism flag
terraform apply -parallelism=10
# Target specific resources
terraform apply -target=module.web_servers
# Refresh state less frequently
terraform plan -refresh=false
# Use smaller state files (separate workspaces) Part 9: Real-World Examples
Complete Homelab Infrastructure
# Full homelab with multiple tiers
module "infrastructure" {
source = "./modules/homelab"
# Web tier
web_servers = {
count = 3
cores = 2
memory = 4096
disk = "50G"
}
# Application tier
app_servers = {
count = 2
cores = 4
memory = 8192
disk = "100G"
}
# Database tier
db_servers = {
count = 2
cores = 8
memory = 16384
disk = "200G"
}
# Monitoring
monitoring = {
cores = 4
memory = 8192
disk = "100G"
}
# Storage
storage_nodes = {
count = 3
cores = 2
memory = 4096
disk = "500G"
}
} Disaster Recovery Setup
# Primary site
module "primary_site" {
source = "./modules/site"
site_name = "primary"
proxmox_node = "pve-primary"
# ... configuration ...
}
# DR site
module "dr_site" {
source = "./modules/site"
site_name = "dr"
proxmox_node = "pve-dr"
# Mirror primary configuration
} Conclusion
Terraform transforms homelab infrastructure management from manual and error-prone to automated, repeatable, and version-controlled. By treating infrastructure as code, you gain consistency, documentation, and the ability to rebuild your entire environment with a single command.
Key Takeaways:
- Terraform enables declarative infrastructure management
- Use modules for reusability and organization
- Always use remote state for team collaboration
- Integrate with CI/CD for automated deployments
- Combine with Packer for complete automation
- Security: never commit secrets, use variables and secret management
- Test configurations before applying to production
Next Steps:
- Create VM templates with Packer (see our Packer guide)
- Implement automated testing with terraform-compliance
- Set up GitOps workflow with Git + Terraform + CI/CD
- Explore Terragrunt for DRY configurations
- Integrate with Vault for secret management
- Implement policy as code with Sentinel or OPA
Happy automating!