Skip to main content

Terraform Setup — PVE02

Overview

Terraform manages the infrastructure layer of pve02 — creating, modifying, and tracking LXC containers and VMs as code. It uses the bpg/proxmox provider to talk to the Proxmox API.

Terraform runs on pve02 from /root/ansible/terraform/proxmox/. The configuration lives in the same git repository as Ansible.

Division of responsibilities:

ToolHandles
TerraformContainer/VM creation, resources (CPU/RAM/disk), network config, tags
AnsibleOS configuration, packages, SSH, users, security, monitoring, service deployment

Source of Truth — Ansible Inventory

The Ansible inventory is the single source of truth for hostnames, IPs, VMIDs, and LXC specs. Terraform reads directly from it — there is no duplication between Ansible and Terraform config.

FilePurpose
inventory/hosts.ymlHostname, IP (ansible_host), VMID per host
inventory/group_vars/{zone}.ymlVLAN ID and gateway per zone
inventory/host_vars/{host}/vars.ymlPer-host LXC spec (lxc_cores, lxc_memory, etc.) + Ansible vars

A host is automatically Terraform-managed as soon as lxc_cores is present in its host_vars. No separate Terraform resource block needed.

LXC spec keys in host_vars

# Required
lxc_description: "Human-readable description"
lxc_tags: ["zone", "role"]
lxc_cores: 2
lxc_memory: 2048 # MB
lxc_swap: 512 # MB
lxc_disk: 20 # GB

# Optional
lxc_started: false # default: true
lxc_unprivileged: false # default: true
lxc_keyctl: true # default: false — needed for e.g. synapse

Directory Structure

/root/ansible/terraform/proxmox/
├── provider.tf # bpg/proxmox provider and version lock
├── variables.tf # Input variable definitions
├── terraform.tfvars # Non-secret variable values (committed)
├── secrets.tfvars # API token + SSH keys — gitignored, stays on pve02 only
├── outputs.tf # IP address summary output (reads from inventory)
├── lxc_managed.tf # for_each resource — all standard LXC containers
├── lxc_infra.tf # MGMT zone special-case containers (pbs01, unifi)
├── lxc_services.tf # SERVERS zone special-case containers (podman, copyparty, paperless)
├── lxc_gaming.tf # DMZ zone — placeholder (all DMZ hosts use lxc_managed.tf)
├── lxc_authentik.tf # Authentik — placeholder (uses lxc_managed.tf)
└── vm_automation.tf # HOMELAB zone VMs (haos)

lxc_managed.tf — how it works

Reads the inventory and resolves the full config dynamically:

hosts.yml → vmid, ansible_host (IP)
group_vars/{zone} → vlan_id, vlan_gateway
host_vars/{host} → lxc_cores, lxc_memory, lxc_disk, lxc_tags, ...

Only hosts with lxc_cores in their host_vars appear in the for_each — the rest are ignored automatically.


Managed Resources

Standard (via lxc_managed.tfproxmox_virtual_environment_container.lxc["{host}"])

HostVMIDZoneIP
authentik268SERVERS10.69.20.68
pulse260SERVERS10.69.20.60
uptime-kuma275SERVERS10.69.20.75
onedev276SERVERS10.69.20.76
hookshot740DMZ10.69.70.40
mumble710DMZ10.69.70.10
minecraft720DMZ10.69.70.20
synapse730DMZ10.69.70.30

Special-case (standalone resources — mount points, GPU, idmaps, or DHCP)

ResourceVMIDZoneIPReason
pbs01200MGMT10.69.10.25Untagged network, special mount
unifi115MGMT10.69.10.15 (DHCP)No static IP yet
podman100SERVERS10.69.20.10GPU passthrough, privileged
copyparty220SERVERS10.69.20.20HDD mount point
paperless272SERVERS10.69.20.72idmaps, HDD mount, firewall
haos101HOMELABDHCPVM — Home Assistant OS

VMID naming scheme: encodes VLAN + IP — e.g. 720 = VLAN 70, IP .20. Use this convention for new resources.


Authentication

Terraform authenticates to the Proxmox API using a dedicated token:

User: terraform@pve
Role: PVEAdmin
Token: terraform@pve!terraform=<secret>

The token secret and SSH public keys are stored in secrets.tfvars (gitignored — never committed):

proxmox_api_token = "terraform@pve!terraform=YOUR-SECRET-HERE"
ssh_public_keys = [
"ssh-ed25519 AAAA... user@host",
]

SSH keys are injected into new containers at creation time via initialization.user_account.keys. Since initialization is in ignore_changes, this only applies on first creation — existing containers are not affected.

Note: The terraform@pve token cannot modify feature flags on privileged containers — that requires root@pam. Affected resources (pbs01, podman) have features in their lifecycle.ignore_changes.

Recreating the token

If the token is lost:

pveum user token remove terraform@pve terraform
pveum user token add terraform@pve terraform --privsep=0
# Update secrets.tfvars with the new secret

Common Operations

Check current state

cd ~/ansible/terraform/proxmox
terraform plan -var-file="secrets.tfvars"

Should show no changes when everything matches. If it shows changes, review carefully before applying.

Apply changes

terraform apply -var-file="secrets.tfvars"

Show current state

terraform state list
terraform show

Show IP summary

terraform output

Adding a New Container

  1. Add to inventoryinventory/hosts.yml under the correct zone group:

    servers:
    hosts:
    myservice:
    ansible_host: 10.69.20.77
    vmid: 277
  2. Create host_varsinventory/host_vars/myservice/vars.yml with lxc_* keys:

    lxc_description: "My new service"
    lxc_tags: ["servers", "myservice"]
    lxc_cores: 2
    lxc_memory: 1024
    lxc_swap: 256
    lxc_disk: 10
  3. Plan and apply:

    terraform plan -var-file="secrets.tfvars"
    terraform apply -var-file="secrets.tfvars"
  4. Bootstrap with Ansible:

    ansible-playbook playbooks/bootstrap.yml -l myservice -k
    ansible-playbook playbooks/site.yml -l myservice

That's it — no Terraform resource block needed. The lxc_managed.tf for_each picks it up automatically.


Special-case Containers

Containers that can't use the generic for_each (GPU passthrough, idmaps, mount points, DHCP) have their own standalone resource blocks in the respective lxc_*.tf file. When adding such a container, write a resource block manually following the existing patterns in lxc_services.tf or lxc_infra.tf.


Lifecycle Notes

Most resources use lifecycle.ignore_changes for fields that are either:

  • Managed externally (community scripts set IP/hostname at creation)
  • Proxmox-internal (console settings, OS template reference)
  • Permission-restricted (features on privileged containers)
lifecycle {
ignore_changes = [initialization, operating_system, console]
}

This means Terraform tracks the resource exists and manages its core config (CPU, RAM, disk, network, tags) but won't fight with Proxmox over runtime details.


Moving Resources to the for_each

If you need to bring an existing standalone resource into lxc_managed.tf (after adding lxc_* keys to its host_vars), move the Terraform state first to avoid destroy/recreate:

terraform state mv proxmox_virtual_environment_container.myresource \
'proxmox_virtual_environment_container.lxc["myhost"]'

Then remove the standalone resource block and run terraform plan to verify no changes.


Git Workflow

Terraform config is part of the same repo as Ansible:

# Local machine — edit files, then push
git add inventory/ terraform/
git commit -m "add myservice"
git push

# pve02 — pull and apply
cd ~/ansible && git pull
cd terraform/proxmox
terraform plan -var-file="secrets.tfvars"
terraform apply -var-file="secrets.tfvars"

secrets.tfvars is gitignored and only exists on pve02. Never commit it.


Storage Reference

DatastoreTypeUsed for
SSD-StorageZFSrootfs for all containers and VMs
HDDZFSMedia, large data, PBS backup volume
localdirISO images, LXC templates
pbs-backup-localPBSBackup target via pbs01