Terraform provisions infrastructure and Ansible configures machines, so pitting them against each other is the wrong question to ask.
Every few months someone on a team asks whether we should "standardize on Ansible or Terraform." It's a reasonable-sounding question, and it's the wrong one. The two tools sit at different layers of the stack. Asking which to pick is a bit like asking whether you should own a truck or a forklift: they both move heavy things, but you don't use one to do the other's job.
Let me draw the line clearly, then explain why so many people still line them up head to head.
Terraform provisions infrastructure. You hand it a description of what should exist (three VMs, a load balancer, a managed database, the network they sit in) and it talks to a cloud provider's API to make reality match that description. It's declarative, and it keeps a state file that records what it created so it can compute the diff between "what is" and "what you asked for."
Ansible configures machines that already exist. Once a server is up, Ansible connects over SSH, installs packages, writes config files, starts services, and deploys your app. It's agentless (no daemon to install on the target), it runs a procedural list of tasks top to bottom, and each task is written to be idempotent.
That's the whole thing. Terraform builds the boxes. Ansible sets up what runs inside them.
The overlap is real at the edges, which is where the confusion comes from. Terraform has provisioners that can run shell commands on a new VM. Ansible has cloud modules that can create an EC2 instance or an S3 bucket. So each tool can technically stumble into the other's territory.
But those features are escape hatches, not core competencies. Terraform's provisioners are famously discouraged even in Terraform's own docs, because they run once at create time and don't fit the state model. Ansible's cloud modules work, but you get none of the dependency graphing, planning, or drift detection that make Terraform worth using for infra. Use a tool for what it's built for and both get much simpler.
Both tools claim idempotency, but they mean slightly different things.
Terraform is declarative by construction. You describe the end state, and Terraform figures out the operations to reach it. Run apply twice with no changes and the second run does nothing, because the state already matches.
Ansible is procedural but idempotent per task. You write ordered steps, and each module is responsible for checking current state before acting. apt: name=nginx state=present installs nginx only if it isn't already there. That per-task discipline is why you can safely re-run an Ansible playbook; a naive shell script doing apt-get install in a loop would not have that property.
This is where philosophy shows up. Ansible's world is mutable: you have long-lived servers and you keep changing them in place, patch by patch, playbook run by playbook run. That's pragmatic, and for stateful boxes it's often the right call.
Terraform leans immutable, especially paired with pre-baked images. Instead of patching a running server, you build a new image, provision fresh instances from it, and destroy the old ones. Config drift stops being a problem because nothing lives long enough to drift.
Neither is universally correct. Immutable is cleaner for stateless app tiers. Mutable is more practical for the database server you can't casually replace.
Here's Terraform standing up a VM and a security group. Note that nothing here configures software.
resource "aws_security_group" "web" {
name = "web-sg"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["10.0.0.0/8"]
}
}
resource "aws_instance" "web" {
ami = "ami-0abcd1234"
instance_type = "t3.small"
vpc_security_group_ids = [aws_security_group.web.id]
tags = {
Name = "web-01"
Role = "web"
}
}
Terraform will create the security group first, then the instance, because it reads the dependency from the reference. You didn't order those steps; it inferred them.
Now Ansible takes over on the box Terraform just built:
---
- name: Configure web server
hosts: web
become: true
tasks:
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
- name: Deploy site config
ansible.builtin.template:
src: templates/site.conf.j2
dest: /etc/nginx/sites-available/site.conf
notify: reload nginx
- name: Enable and start nginx
ansible.builtin.service:
name: nginx
state: started
enabled: true
handlers:
- name: reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
Run this ten times and nginx gets installed once, the config gets written once (and reloads only when it actually changed), and the service stays running. That's idempotency doing its job.
In practice most mature setups run both, in sequence. Terraform provisions the infrastructure and produces an inventory of what it made. Ansible reads that inventory and configures each host. Terraform outputs the instance IPs; a dynamic inventory plugin feeds them straight into Ansible so you never hand-maintain a hosts file.
The clean handoff is: Terraform: anything with a cloud API behind it. Ansible: anything that happens over SSH after the box boots. Keep the seam there and you avoid the messy middle where both tools fight over the same resource.
For a fuller map of the ecosystem, see our roundup of the best Infrastructure-as-Code tools.
Reach for Terraform when the work is creating, changing, or destroying cloud resources: networks, VMs, managed databases, DNS, IAM, Kubernetes clusters. If it has an API and a lifecycle, Terraform is the right layer.
Reach for Ansible when the work is inside a machine that already exists: package installs, config files, service management, app deploys, one-off orchestration across a fleet, or configuring network gear that has no real provisioning API.
Reach for both when you're building full environments from scratch, which is most of the time. Terraform lays the foundation, Ansible furnishes the rooms.
You genuinely don't need Ansible if your servers are fully immutable and baked with Packer, since there's nothing left to configure at runtime. And you don't need Terraform if you're only ever configuring hardware someone else provisioned.
Stop treating this as a versus. If you're running anything non-trivial in the cloud, adopt Terraform for provisioning first, because unmanaged infrastructure is the more expensive problem. Layer Ansible in for configuration once you have boxes to configure, and wire Terraform's outputs into Ansible's inventory so the two stay in sync automatically.
If we had to name the one anti-pattern to avoid, it's using Terraform provisioners to run configuration logic. It feels convenient on day one and turns into an unstateful, un-rerunnable mess by month three. Let each tool own its layer, and the boundary between them becomes the cleanest part of your pipeline.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
A team was burning 40,000 CI minutes a month and could not say why. Here is how GitHub Actions billing actually works and where the money leaks.
Static keys leak. The question isn't if but how fast you notice and how clean your response runbook is when the pager goes off.
Explore more articles in this category
The IaC landscape fractured after the Terraform license change. This is the map to what each tool is actually best at, and how to choose without regret.
Terragrunt keeps large Terraform setups DRY and orchestrated, but small teams often pay its learning curve for little return.
A practical comparison of Kubernetes-native continuous reconciliation against the classic CLI-driven, state-file IaC model for platform teams.
Evergreen posts worth revisiting.