Part 4
Teaching Claude & Gemini to Build Proxmox: The Agentic IaC Pipeline
Handing an LLM a shell and a Proxmox API token is a bad idea in exactly the way it sounds. The interesting problem isn't "can an AI write Terraform" — it clearly can — it's how you get consistent, reviewable, idempotent infrastructure code out of a tool that is, by design, a little different every time you ask it something.
The rule that matters most: narrow the brief
The agent never gets "manage the homelab." It gets one scoped change at a time — add this module, adjust this variable, fix this specific plan diff — against a repo that already documents its own conventions. The single rule that catches the most trouble before it starts: scripts must be idempotent. Re-running a play or a plan should converge toward the desired state, not fail because something already exists. That one constraint rules out most of the sloppy one-shot code an LLM will produce if you don't ask for it explicitly.
Terraform: the module the agent actually maintains
The provisioning module clones a cloud-init template per node and joins it to the private network at first boot:
# illustrative — module shape, not the real module
module "service_vm" {
source = "./modules/vm"
name = "example-service"
target_node = "node-a"
cores = 2
memory_mb = 4096
tags = ["homelab", "managed"]
}
The real gotcha the agent had to be taught, explicitly, in the module's own
comments: a VM's clone configuration is write-once against the Proxmox API.
Terraform can't reconcile it from a refresh, so an innocent-looking edit to
an unrelated field can make the provider try to destroy and recreate the
whole VM. The fix is a documented ignore_changes block on the fields that
are legitimately immutable post-clone — and it only had to be explained once,
because it's now sitting in the module for the next prompt to read.
Ansible: order is the whole story
The convergence playbook runs roles in a fixed sequence, and the sequence
itself is the interesting part. The container runtime role has to run
before the hardening role, because the firewall ruleset the hardening role
installs references the container network bridge by name — on a genuinely
fresh host, running them in the "obvious" order fails validation outright
because that bridge doesn't exist yet. That's not a mistake an LLM would
guess at; it's a lesson learned once and then written into site.yml as an
explicit comment, so it never has to be relearned.
# illustrative role order
roles:
- base
- tailscale
- docker # must precede security: firewall rules reference its bridge
- security
- compose_stack
Where the mistakes actually get caught
Neither Terraform nor Ansible runs unsupervised against production state. A pull request produces a plan; the plan is the review artifact, not the code. Ansible convergence runs the same way on the same triggers, so a malformed YAML block or a stale variable reference shows up as a CI failure, not a 3am page. The dev and CI paths use the identical command and the identical auth — if a plan is clean locally, it's clean in CI, which means there's exactly one thing to debug when it isn't.
The net effect: the agent gets to be genuinely useful — scaffolding modules, proposing role changes, fixing its own plan errors — without ever being the thing standing between a bad diff and a live cluster. That's what CI is for.
Next post: what happens after provisioning, when the same kind of agent starts watching the cluster instead of just building it.