Making Proxmox Cloud-Like: OpenTofu, the API, and the Placement Gap
Most of this series has been a tour of platforms you would migrate to: OpenStack and CloudStack as full clouds, Incus as the lightweight cluster that auto-places, XCP-ng as the Xen successor. This post is for the much larger group of readers who are not migrating anywhere. You already run Proxmox VE. It works. The cluster is healthy, Ceph is humming, backups land every night, and the last thing you want is to rip out a platform your team actually understands.
The question this post answers is narrower and more useful: how close can you get to a self-service cloud experience on the Proxmox you already have, and where exactly does that effort hit a wall? The answer involves a genuinely good REST API, an OpenTofu provider that makes “a VM plus a network plus a disk” a declarative artifact in version control, and one honest gap that has narrowed dramatically in the last year but has not closed: Proxmox does not, at the moment you provision a VM, go shopping across the cluster for the best node to put it on. You tell it the node. That single fact is the difference between “infrastructure as code on a cluster” and “a cloud,” and the whole post is really about how much that difference matters to you.
What you already have, and why it is more than you think
It is worth being precise about Proxmox’s foundation, because the cloud-like layer is thinner than people assume — most of what you need is already running.
A Proxmox VE cluster is a set of nodes glued together by Corosync for cluster membership and a shared configuration filesystem (pmxcfs, mounted at /etc/pve) that replicates every guest config, every storage definition, and every user across all nodes. Any node can manage any guest. On top of that sits:
- Two workload types under one roof. KVM virtual machines for full isolation and arbitrary operating systems, and LXC system containers for dense, near-native Linux workloads. This is the same system-container-plus-VM duality that makes Incus attractive, and it is native here — no add-on.
- Storage that is already cluster-aware. Ceph RBD for hyperconverged block storage, ZFS for local-with-replication, plus NFS, iSCSI, and LVM-thin. Shared storage is the prerequisite for live migration, and Proxmox ships the tooling to stand up Ceph from the same web UI.
- High availability. The
pve-ha-managerstack (5.2.0 as of the 9.2 line) watches HA-managed guests and restarts them elsewhere when a node dies. Node failure is a solved problem. - A real REST API. Not an afterthought bolted on for a marketing checkbox — the web UI is itself a client of this API. Anything you can click, you can script.
That last point is the hinge. The entire cloud-like story is “drive the API you already expose, declaratively.” Everything below builds on it.
The API: everything the web UI does, you can do
Proxmox exposes a JSON REST API at https://<node>:8006/api2/json/. Three things make it pleasant to automate:
API tokens. You do not script with a password. You create a token tied to a user — ideally a dedicated automation@pve user — and scope its privileges. A token looks like automation@pve!terraform=<uuid> and can be revoked independently of the user. Create one from the CLI:
# Create a user and a role with just the privileges automation needs
pveum user add automation@pve
pveum role add Provisioner -privs "VM.Allocate VM.Config.Disk VM.Config.CPU \
VM.Config.Memory VM.Config.Network VM.Config.Cloudinit VM.PowerMgmt \
VM.Audit Datastore.AllocateSpace Datastore.Audit SDN.Use"
pveum acl modify / -user automation@pve -role Provisioner
pveum user token add automation@pve terraform --privsep 0
That last command prints a secret exactly once. The --privsep 0 makes the token inherit the user’s full privileges; set it to 1 and grant the token its own ACLs if you want tighter separation.
pvesh, the API as a shell. Before you reach for any provider, you can walk the entire API tree from any node with pvesh, which is invaluable for discovering what is possible:
pvesh get /nodes # list nodes and their status
pvesh get /nodes/pve1/qemu # VMs on a node
pvesh get /cluster/resources --type vm # every guest, cluster-wide
pvesh create /nodes/pve1/qemu -vmid 9001 -name test -memory 2048 -cores 2
Cluster-wide endpoints. /cluster/resources and /cluster/nextid mean you do not have to know which node owns a guest to query it, and you can ask the cluster for the next free VM ID atomically. This is the connective tissue that lets a single API call reason about the whole cluster — and, foreshadowing the gap, it is also the data a smarter scheduler would use to pick a node, which Proxmox mostly leaves to you at provision time.
OpenTofu: turning the API into declarative infrastructure
Hitting the API by hand is fine for one-offs. The cloud-like experience is declarative: a file in git that says “this VM exists, with this network, this disk, this cloud-init config,” and a tool that reconciles reality to match. That tool is OpenTofu (or Terraform — the configuration is identical), and for Proxmox the provider question has a clear answer in 2026.
bpg versus telmate: pick bpg
There are two providers, and the choice is no longer close.
| bpg/proxmox | telmate/proxmox | |
|---|---|---|
| Status | Actively maintained, v0.101.0 | Legacy, latest is a release candidate (v3.0.2-rc) |
| Scope | 111 resources, 87 data sources | ~5 resources |
| OpenTofu | Explicitly supported | Works, but stagnant |
| PVE 9.x | Supported (tracks 9.1/9.2) | Lagging |
| Covers | VMs, LXC, SDN, storage, users, ACLs, pools, HA, downloads, cloud-init | Core VM and LXC provisioning |
| Recommendation | New work | Only to maintain existing state |
The Telmate provider is the one most old blog posts reference because it came first and was the only game in town for years. It is battle-tested for basic VM creation and nothing more. The bpg provider is the one to build on now: it is comprehensive, actively released, and models nearly the whole API surface — including the SDN, storage, and IAM resources you need if you want the self-service layer to provision networks and users, not just VMs.
A real example: a VM from declarative config
Here is the shape of “a VM plus a network plus a disk from a CLI.” It clones a cloud-init-enabled template and parameterizes the guest:
|
|
tofu plan shows the diff, tofu apply makes it real, and the state file becomes the source of truth. Wrap this in a module with variables for size and VLAN, expose it through a CI pipeline or a thin self-service portal, and a developer can open a pull request that provisions a VM. That is genuinely cloud-like — version-controlled, reviewable, reproducible, with a clear audit trail.
And then look at line 16. node_name = "pve1". That is the gap, in one line of HCL.
The placement gap, stated precisely
When you provision a VM through the API or the OpenTofu provider, you specify the target node. The provider does not query /cluster/resources, score your nodes by free CPU and memory, and place the guest on the best fit. It puts the VM exactly where you told it to. If pve1 is at 90% memory and pve3 is idle, and your HCL says pve1, the VM lands on pve1.
Contrast this with a full cloud. In OpenStack you ask Nova for “a VM of this flavor” and the scheduler picks the host. In CloudStack the allocator does the same. In Incus, clustering lands new instances on the least-loaded member automatically — you do not name a node. That automatic, provision-time, “find the best home for this workload” decision is DRS in VMware’s vocabulary, and it is the single capability that most cleanly separates “a cluster you script” from “a cloud you request from.”
Here is the decision, drawn out:
A FULL CLOUD (OpenStack/CloudStack/Incus) PROXMOX + OPENTOFU (provision time)
request: "give me a 4c/8G VM" request: "create VM on pve1"
| |
v v
+--------------------+ +--------------------+
| SCHEDULER | reads live cluster state | (no scheduler) | you already
| scores all hosts | CPU/mem/affinity/... | | decided: pve1
+--------------------+ +--------------------+
| |
v v
lands on best host lands on pve1, whatever
the system chose shape pve1 is in
You can close this gap, and the rest of the post is the two honest ways to do it — one inside Proxmox, one in your own glue code — plus the part of it that finally got fixed.
What Proxmox does schedule: the CRS, and what changed
Proxmox is not placement-blind. It has the Cluster Resource Scheduler (CRS), introduced back in 7.3 — but historically the CRS only governed HA-managed guests, and only at specific moments. Understanding exactly when it kicks in is the key to knowing how much of the gap is already closed.
The CRS has modes, set at Datacenter → Options → Cluster Resource Scheduling:
| Mode | What it does | When it acts |
|---|---|---|
| Basic (default) | Minimal logic; gets HA guests running without seriously weighing load | HA recovery, HA start |
| Static Load | Picks the best-fitting node using configured CPU/memory of active guests (their quotas, not live usage) | HA recovery, HA start, and — with ha-rebalance-on-start — when starting any HA guest |
| Dynamic Load (9.2) | Continuously monitors real-time node and guest utilization and live-migrates HA guests to flatten imbalance | Continuously, in the background |
Two changes in the last year matter enormously to this story.
Proxmox VE 9.0 replaced HA “groups” with affinity rules. The old model — assign guests to a “group” of preferred nodes with priorities — is gone, migrated automatically on upgrade. In its place are two expressive rule types: node affinity rules (pin or prefer a guest to specific nodes) and resource affinity rules (keep guests together, or force them apart — true anti-affinity, so your two database replicas never share a node). This is the constraint vocabulary a real scheduler needs.
Proxmox VE 9.2, released May 21, 2026, added the Dynamic Load Balancer. This is the big one. In dynamic mode the CRS watches live CPU and memory across the cluster and, when imbalance crosses a threshold, automatically live-migrates HA-managed guests to level the load — while respecting every affinity rule and resource restriction you have defined. For the first time, Proxmox does something genuinely DRS-shaped out of the box.
So is the gap closed? Mostly, for HA-managed guests, after they are already running. Read that qualification carefully, because it is the whole subtlety:
- The dynamic balancer rebalances existing HA guests. It does not change the fact that at provision time your OpenTofu still names a node. The VM is born on the node you specified; the balancer may later move it if the cluster is lopsided and the guest is HA-managed.
- It only acts on HA-managed guests. A VM you provisioned without adding it to HA is invisible to the CRS — it sits wherever you put it, forever.
- It is reactive load-leveling, not request-time placement. The mental model is “the cluster self-heals toward balance,” not “ask for a VM and the system chooses its home.”
That is a real and meaningful improvement — a year ago this section would have read “Proxmox does no dynamic balancing at all.” But it does not turn the OpenTofu provider into Nova. The provision-time decision is still yours.
Closing the rest of the gap yourself
If you want provision-time placement — the VM to be born on the right node, not just rebalanced later — you encode it. There are three honest levels of effort.
1. Let the dynamic balancer do it (least effort). Provision every VM as HA-managed, set CRS to dynamic mode, and accept “born somewhere, balanced shortly after.” For many homelabs and small clusters this is genuinely enough. Your OpenTofu still names a node — pick a sensible default or round-robin in the module — and the cluster sorts out imbalance within minutes. The cost is a brief period of suboptimal placement and a live migration’s worth of traffic.
2. Compute the node in your provisioning layer (moderate effort). Before tofu apply, query the cluster and choose the node yourself, then feed it in as a variable. A few lines against the API:
# Pick the node with the most free memory, feed it to OpenTofu
BEST=$(pvesh get /cluster/resources --type node --output-format json \
| jq -r 'map(select(.status=="online"))
| max_by(.maxmem - .mem) | .node')
tofu apply -var "target_node=${BEST}"
This is a crude scheduler in four lines — “most free memory wins.” You can make it as sophisticated as you like (weight CPU, respect anti-affinity by checking what already runs where, exclude nodes in maintenance), and because it runs before provisioning, the VM is born in the right place. The catch is that you now own this logic, including its edge cases and race conditions if two provisions run at once.
3. Adopt a placement layer on top (most effort, most cloud-like). Put something that is a scheduler in front of Proxmox. This is where the rest of the series re-enters: you could run Incus on top, which auto-places; or use a portal/orchestrator that implements bin-packing; or, if you find yourself building a quota system, a tenant model, and a scheduler, recognize that you are reimplementing OpenStack or CloudStack one feature at a time.
That last recognition is the real decision point.
When to stop fighting it
The whole appeal of the Proxmox-plus-OpenTofu approach is that it is additive. You keep the platform your team knows and bolt on declarative provisioning, which for a huge range of users is the entire job. A homelab, a small business, a team of a dozen engineers who each want to tofu apply a VM now and then — this stack serves them beautifully, and PVE 9.2’s dynamic balancer quietly removed the most common complaint.
You should stop bolting and adopt a real cloud platform when you start needing the things a scheduler is the tip of, not the whole iceberg:
- Multi-tenancy with hard isolation. When “developers can provision VMs” becomes “twelve teams each get an isolated quota, network, and project they cannot escape,” you want OpenStack/CloudStack tenancy, not Proxmox pools and ACLs stretched past their design.
- Self-service for non-operators. A polished catalog, a quota dashboard, per-tenant billing, an API tenants build their own tooling against. Proxmox’s API is excellent for operators; it is not a multi-tenant cloud API.
- Request-time placement as a contract, not a best-effort. When “the system must choose the optimal host on every request, honoring complex affinity and capacity rules, at scale” is a requirement rather than a nice-to-have, a purpose-built scheduler beats your
jqone-liner. - You are spending more time on the glue than the workloads. The clearest signal. If your bin-packing script, your quota enforcement, and your tenant model have become a project of their own, you have built a worse cloud than the ones that already exist. That is the moment to migrate.
The honest framing is that Proxmox plus OpenTofu is not a cloud and was never trying to be — it is infrastructure as code on a cluster that handles its own failures and, as of 9.2, balances its own load. For most readers of this series, that sentence describes exactly what they need, and the migration posts are interesting rather than urgent. For the readers who genuinely need a cloud, the value of this post is the same: knowing precisely which wall you hit, and why, so the decision to move is made on evidence instead of frustration.
The honest scorecard
| Capability | Proxmox + OpenTofu (PVE 9.2) | A full cloud (OpenStack/CloudStack) |
|---|---|---|
| Declarative VM/network/disk | Yes (bpg provider) | Yes |
| Real REST API | Yes, excellent | Yes |
| KVM + system containers | Yes (LXC native) | Varies (LXC less common) |
| Node-failure HA | Yes | Yes |
| Live load rebalancing | Yes, dynamic (9.2), HA guests | Yes |
| Affinity / anti-affinity rules | Yes (9.0) | Yes |
| Request-time placement | No — you name the node | Yes — scheduler chooses |
| Hard multi-tenancy / quotas | Limited (pools + ACLs) | Yes, first-class |
| Self-service tenant portal | Roll your own | Built-in |
| Effort to operate | Low (you already run it) | High |
The single bold “No” is the entire reason the other private-cloud posts exist. Everything else, Proxmox already gives you or 9.2 just added. Whether that one gap is a deal-breaker or a footnote depends entirely on whether you are running a cluster or running a cloud — and most people, honestly, are running a cluster and would be happier admitting it.
Sources
- bpg/terraform-provider-proxmox — Terraform/OpenTofu Provider for Proxmox VE
- bpg/proxmox provider docs (Terraform Registry)
- telmate/proxmox (OpenTofu Registry)
- Proxmox VE 9.2 with Dynamic Load Balancer released (press release)
- Proxmox VE 9.2 new features: Dynamic Load Balancer, SDN, and Ceph Tentacle (4sysops)
- What is New in Proxmox VE 9 (affinity rules) (ComputingForGeeks)
- Proxmox 9.1.8 Finally Rebalances HA Workloads Automatically (Virtualization Howto)
- High Availability — Proxmox VE wiki (CRS modes, ha-rebalance-on-start)
- Cluster Resource Scheduler introduced in PVE 7.3 (press release)
Comments