Overview
Terraform and Ansible get shelved together as "infrastructure as code," which hides the fact that they were built for different layers of the problem. Terraform is a provisioning tool: it creates, changes, and destroys infrastructure, networks, VMs, databases, DNS, by declaring a desired end state and diffing it against a state file that records what it built. Ansible is a configuration management and automation tool: it connects to machines that already exist (over SSH or WinRM, agentless) and runs ordered, idempotent tasks, install packages, template config files, restart services, orchestrate a rolling deploy.
The overlap that causes the confusion is real, Ansible has cloud modules that can create a VM, and Terraform has provisioners that can run scripts on one, but each is weakest exactly where the other is strongest.
Feature comparison
| Terraform | Ansible | |
|---|---|---|
| Primary job | Provisioning infrastructure | Configuring machines and orchestrating tasks |
| Model | Declarative; describe end state, tool computes the diff | Task-based; ordered plays of idempotent modules |
| Language | HCL | YAML playbooks (plus Jinja2 templating) |
| State | Explicit state file; drift is detectable via plan | No central state; each run re-checks reality per task |
| Dry run | terraform plan, exact preview of every change | --check mode, best-effort per module |
| Dependency handling | Automatic resource graph | You order tasks yourself (handlers, notify, roles) |
| Agents / access | None; talks to provider APIs with credentials | None; SSH/WinRM to targets |
| Deleting things | First-class; removing config destroys the resource | Weak; absent tasks change nothing, deletion is explicit |
| Ecosystem | Provider registry (thousands of APIs) | Collections via Ansible Galaxy, huge module library |
| License | Business Source License 1.1 (open-source fork: OpenTofu) | GPLv3 (ansible-core), backed by Red Hat |
| Typical trigger | Infrastructure change in version control | Provisioning hand-off, app deploys, ad-hoc ops, patching |
Where Terraform tends to win
The lifecycle of infrastructure itself. Because Terraform tracks what it created, it can show you an exact plan of what will change, detect drift between code and reality, and, critically, destroy what you remove from the code. Its dependency graph orders creation across dozens of interrelated resources (VPC before subnet before VM) without you scripting the sequence. For standing up environments repeatably, dev/staging/prod from the same modules, a declarative tool with state is the right shape, and Ansible's create-a-VM modules give you none of that lifecycle tracking.
Where Ansible tends to win
Everything that happens on and across running machines. Configuring an OS, rolling out an application version across a fleet with health checks between batches, patching, and one-off operational tasks ("restart nginx on every web node in rack 3") are procedural jobs, and Ansible's ordered plays, inventory groups, and thousands of modules are built for exactly that. Being agentless matters here: anything reachable over SSH, bare metal, network switches, legacy VMs no cloud API knows about, is automatable with nothing installed on the target.
Note
The strongest pattern is not choosing between them: Terraform provisions the infrastructure and hands off, via inventory generated from its outputs or a dynamic inventory plugin, to Ansible, which configures what Terraform built. On cloud-native stacks, immutable images (Packer) or user-data/cloud-init can shrink Ansible's role, but the division of labor, lifecycle vs configuration, stays the same.
Verdict
Ask what you're automating. If the answer is the existence of infrastructure, creating it, changing it, tearing it down, in sync with version-controlled code, that's Terraform. If the answer is the behavior of machines, packages, config files, services, deploy sequences, that's Ansible. Teams that force one tool to do both jobs end up either shell-scripting configuration through Terraform provisioners or losing lifecycle tracking by provisioning through Ansible; using each at its own layer is simpler than either workaround.