Chapter 1: Foundations
Topic 04

Ansible vs Terraform

ConceptProvisioning

Ansible and Terraform are framed as competitors constantly, and they are not. They operate at different layers, and the strongest stacks run both: Terraform provisions the infrastructure — the VM, the network, the load balancer — and remembers it in state; Ansible configures what runs inside that infrastructure and keeps no state at all. The two are partners across a boundary, not alternatives to choose between.

Getting that boundary right is the difference between a clean pipeline and one tool bent painfully into the other's job. This topic draws the line precisely, because almost every real argument about "Ansible or Terraform" is actually a question about which layer you are standing on at that moment.

Two Different Questions

Terraform answers a question about existence: what infrastructure should be there? Given that declaration it will create the servers, networks, and load balancers, and — just as importantly — destroy them when you remove them from the config. Bringing infrastructure into being and tearing it down is its entire job.

Ansible answers a question about configuration: how should this machine, which already exists, be set up? Given a host it will install packages, render templates, write files, and restart services. It has no concept of destroying a server, because creating one was never in its remit. The two questions look adjacent but they belong to different tools, and conflating them is the root of most pipeline pain.

Different layers, different jobs
Terraform
Provisions infrastructure, keeps a state file, creates and destroys what it owns.
Ansible
Configures an existing machine, keeps no state, never destroys a server.

State vs No State

Terraform's state file is both its power and its burden. It maps every line of config to a real resource, which is exactly what lets Terraform know that deleting a block means destroying a specific load balancer rather than guessing. That memory has to be stored, locked against concurrent runs, protected, and kept honest with reality — and when it drifts or corrupts, recovery is real work.

Ansible deliberately has no state file, and this is the spine of the whole course: the managed node is the record. Every run gathers the machine's current facts and reconciles them against your declared state, live. The upside is simplicity — nothing to lock, corrupt, or check into git by accident. The limit is the flip side of Terraform's strength: with no memory of what it created, Ansible cannot clean up what it no longer manages. Remove a task and Ansible simply stops touching that thing; it never knew it owned it.

Declarative Graph vs Ordered Tasks

Terraform builds a dependency graph of resources and converges it. You declare that a subnet needs a VPC and an instance needs that subnet, and Terraform works out the order, parallelizing what it can. You describe the end state of the infrastructure; the planner figures out the sequence. There is no natural notion of "do this step, wait, then do that one" — the graph just resolves.

Ansible runs an ordered list of tasks, top to bottom, across the hosts you target. That ordering is exactly why orchestration is natural in Ansible and awkward in Terraform: a rolling deploy is "drain this host, update it, health-check it, then move to the next," and that is a sequence, not a graph. The mismatch runs both ways — forcing a dependency graph into an ordered task list is as painful as forcing an ordered deploy into a convergence engine.

The Handoff

The two tools meet at a clean seam. Terraform builds the fleet and emits outputs — the IP addresses and hostnames of what it just created. Those outputs feed Ansible's inventory, so the machines Terraform stood up are precisely the machines Ansible configures. The canonical pipeline is two commands in order: terraform apply to build, then ansible-playbook to configure.

The clean way to wire that seam is dynamic inventory rather than copied IP addresses. Ansible reads the same cloud (or Terraform's state) that Terraform wrote, so a rebuilt fleet reconfigures itself with no hand-edited host list to drift. A later chapter builds this handoff in full; for now the shape is what matters — Terraform's output is Ansible's input, and the boundary between them is a feature.

Terraform outputs the addresses; Ansible's dynamic inventory reads the same cloud
# Terraform: stand up the web tier and expose its addresses
output "web_public_ips" {
  value = aws_instance.web[*].public_ip
}

# Ansible: aws_ec2.yml reads the live cloud, no copied IPs
plugin: amazon.aws.aws_ec2
filters:
  tag:Project: larkspur
keyed_groups:
  - key: tags.Role      # a Role=web tag becomes the `web` group

The Terraform block names the addresses it built; the Ansible plugin config asks the same cloud which hosts exist and groups them by tag. Neither side copies anything from the other by hand — the cloud is the shared source of truth, and the fleet can be destroyed and rebuilt without anyone editing an inventory.

When People Force One to Do Both

The anti-pattern this topic exists to prevent has two symmetrical forms. The first is using Terraform's provisioner "remote-exec" or local-exec to configure a box — run a shell script at create time to install software. These run once, are not idempotent, and HashiCorp itself documents them as a last resort; they turn Terraform into a worse Ansible.

The second is driving cloud infrastructure lifecycle from Ansible's cloud modules and expecting Terraform-style cleanup. Ansible can call cloud APIs to create a VM, but with no state it has no idea what to destroy when you delete the task — you get creation without a lifecycle. The rule is simple and it ends the argument: if a thing has a lifecycle to create and destroy, Terraform owns it; if it is software and config on a host that already exists, Ansible owns it.

Terraform vs Ansible, layer by layer

Terraform — provisioning. A declarative resource graph backed by a state file that maps config to real infrastructure, so it knows what to create and what to destroy. It answers "what infrastructure should exist?" Use it to build the servers, networks, and load balancers.

Ansible — configuration and orchestration. An ordered list of tasks with no state, re-reading the live machine on every run. It answers "how should this existing machine be configured?" Use it to install, template, and restart on hosts Terraform already built. Reach for the other tool the moment you feel yourself fighting the one you're in.

Common Mistakes
  • Using Terraform's remote-exec or local-exec provisioners to install and configure software — they run once at create time, are not idempotent, and HashiCorp documents them as a last resort for exactly this reason.
  • Driving cloud infrastructure lifecycle from Ansible's cloud modules and expecting Terraform-style cleanup — with no state, Ansible cannot know what to delete when you remove a task, so you get creation without teardown.
  • Duplicating the inventory by hand after Terraform builds the servers, then watching it drift as the fleet changes — let Terraform's output drive a dynamic inventory instead of copying IPs.
  • Arguing which tool is "better" instead of placing each at its layer — the question is never Ansible-or-Terraform, it is which layer you are operating at right now, and each owns its own.
  • Expecting Terraform's resource graph to express an ordered rolling deploy, or Ansible's task list to track and destroy infrastructure — each fights the job the other was built for.
Best Practices
  • Let Terraform provision and Ansible configure, with Terraform outputs feeding Ansible's inventory, so each tool does the job its model was built for.
  • Keep the boundary crisp: if it has a lifecycle to create and destroy, Terraform owns it; if it is software and config on an existing host, Ansible owns it.
  • Wire the handoff with dynamic inventory rather than copying IP addresses, so a rebuilt fleet reconfigures itself with no manual edits to a stale host list.
  • Avoid Terraform provisioners for configuration; if Terraform must kick off configuration, have it trigger an Ansible run rather than inline shell that runs once and never converges again.
  • When you feel yourself fighting one tool, check whether you have crossed the layer boundary — that friction is almost always the signal to switch to the other.
Comparable tools Terraform · Pulumi · CloudFormation the provisioning layer with state Packer bakes images, a third related layer cloud-init first-boot configuration Ansible cloud modules can provision in a pinch, but without state

Knowledge Check

Which tool owns creating the server, and which owns configuring it?

  • Terraform creates and can destroy the infrastructure; Ansible configures what runs on a machine that already exists
  • Ansible creates and can destroy the server's infrastructure, while Terraform installs the packages and writes config on it
  • Both tools create servers and configure them equally, so you simply pick whichever one you prefer for any given job
  • Terraform handles both the provisioning and configuration layers, and Ansible is only for one-off ad-hoc shell commands

What does Terraform's state file give it that Ansible deliberately lacks?

  • A record mapping config to real resources, so it knows what to destroy when you remove a block — Ansible has no such memory of what it created
  • The ability to connect to and authenticate against managed hosts over SSH, something Ansible has no built-in way to do and must outsource to another tool
  • Idempotency — the guarantee that re-running a play makes no change when nothing has drifted, which Ansible modules cannot provide
  • A way to run tasks in a strict, predictable top-to-bottom order, which a stateless tool like Ansible cannot express

Why is using Terraform's remote-exec provisioner to configure a host considered an anti-pattern?

  • It runs once at create time and is not idempotent, so it cannot re-converge a drifted host — HashiCorp documents it as a last resort
  • It is somewhat slower than Ansible to run but otherwise equivalent, fully idempotent, and perfectly safe for configuration
  • It cannot open an SSH connection to the target host, so it never actually reaches the machine to run any of the configuration steps at all
  • It deletes the entire Terraform state file from the backend every single time it runs

What is the clean way to connect Terraform's output to Ansible's inventory?

  • A dynamic inventory that reads the same cloud Terraform wrote, so a rebuilt fleet reconfigures with no copied IP addresses
  • Copying the IPs out of Terraform's output and pasting them into a static inventory file by hand after every single apply you run
  • Storing all of Ansible's playbooks and roles inside Terraform's state file so the two stay together
  • Having Ansible run first against the fleet and pass its results back into Terraform as input variables

You got correct