Building this homelab, I had to pick how to handle infrastructure-as-code. The two obvious tools — Terraform and Ansible — both technically can do each other's job. Terraform has provisioners. Ansible has cloud-provider modules. So why not just pick one?
I tried. Both single-tool approaches fall apart for the same reason: you end up fighting the tool's design.
The shape of each tool
Terraform is declarative and state-driven. It excels at "here's what should exist; make it so" — VMs, networks, DNS records, firewall rules. It tracks state, detects drift, and tears down resources cleanly. It is not designed to handle the kind of idempotent, order-sensitive, service-level configuration where Ansible thrives.
Ansible is procedural and agentless. It excels at "connect to this host, ensure this state." It manages packages, services, users, and config files reliably across hundreds of hosts. It has no native concept of infrastructure state — ansible-playbook cannot tell you whether a VM exists before trying to connect to it.
What "using one for both" looks like
Ansible-only: You can use community.general.proxmox to create VMs. But Ansible has no state backend, so detecting drift is hand-rolled and fragile. Want to destroy a VM? You're either running a separate playbook or hoping idempotency catches it. The seams show fast.
Terraform-only: You can use provisioner "remote-exec" or provisioner "local-exec" to run Ansible after a VM comes up. HashiCorp explicitly discourages this — provisioners are a last resort, run only on resource creation (not updates), and produce no verifiable record of what configuration was applied. It works on day one and rots from there.
The split
| Tool | Layer | Responsibility |
|---|---|---|
| Terraform | Infrastructure | Create/destroy VMs, networks, DNS, firewall rules |
| Ansible | Software | Install packages, configure services, manage users, write configs |
The handoff point is the running VM: Terraform creates it from a cloud-init template, then steps away. Ansible takes over for everything inside the OS.
What this gets you
- Each tool runs at full strength inside its lane — no fighting the framework.
- Pipelines split naturally: Terraform pipeline (plan → approve → apply) for infra, Ansible pipeline for software changes. They run on different cadences and have different blast radii.
- State is verifiable in two independent places: Terraform state for what exists, Ansible inventory + roles for what's configured. Either can be inspected without trusting the other.
What it costs
Two tools means two pipelines, two sets of credentials, two languages (HCL + YAML/Jinja). For a brand-new VM that needs immediate configuration, you're coordinating two pipeline runs. Most of the time that's fine; occasionally it's friction.
Lesson
The "one tool to rule them all" instinct is real, but it ignores that tools have opinions. Ansible thinks the world is a fleet of running hosts; Terraform thinks the world is a graph of resources. Pretending one of them is wrong about that costs you more than running both.