~/thilagan-digital
← back to logs

Part 4

Teaching Claude & Gemini to Build Proxmox: The Agentic IaC Pipeline

homelabaiterraformansibleci-cd

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.