Ansible vs Terraform: Which One You Need?
Terraform dey create your VPS, Ansible dey configure am. See the real split, handoff commands, why provisioners cause wahala, and when Ansible alone fit do.
Ansible vs Terraform for one sentence
Ansible vs Terraform no be choice between two tools wey dey do the same work. Terraform dey declare wetin infrastructure get: servers, disks, networks, DNS records. Ansible dey declare wetin suppose dey true inside machine wey already exist: packages, users, config files, running services. Terraform dey create the VPS. Ansible dey turn that VPS to web server.
Both na declarative tools, and dem both dey called infrastructure as code (IaC). The real difference na wetin dem dey remember. Terraform dey write state file wey map every resource for your code to real object wey e create through API. So e fit know say deleting five lines mean say one server suppose destroy. Ansible no dey remember anything between runs. E dey connect through SSH, inspect the machine, and change only wetin no already match the playbook.
That one difference explain the rest of this guide, including why mixing the two jobs inside one tool dey cause problems.
Wetin Terraform dey actually do
Terraform dey talk to an API through provider plugin. Your provider registry page dey define the resource types wey you fit write, so server for one host and server for another host na different resource names with different arguments.
resource "cloud_server" "web" {
name = "web1"
image = "ubuntu-24.04"
type = "small"
}
output "web_ip" {
value = cloud_server.web.ipv4_address
}Replace cloud_server with the resource type wey your provider document. The output block na the important part for this guide, because na how the address dey comot from Terraform.
terraform init
terraform fmt -check
terraform validate
terraform plan -out=tfplan
terraform apply tfplanterraform init dey download the provider and write lock file. terraform plan dey show the difference between your code and the state file, and e go end with line like Plan: 1 to add, 0 to change, 0 to destroy. Read that line every time. Some arguments no fit change in place, and the plan go show am with # forces replacement beside the attribute, followed by 1 to add, 0 to change, 1 to destroy. If you apply that plan, e go delete the server and build new empty one. Na so people dey lose data wey dem think say safe.
If you save the plan to file and apply the file, instead of running bare terraform apply, the thing wey you review na the same thing wey go run. Between the two commands, another person fit don change the infrastructure.
terraform.tfstate na the memory. If you lose am, Terraform no longer know say those servers belong to you, so the next apply go try create duplicates. Keep am for remote backend as soon as more than one person dey run the commands, because if two people apply at the same time, e go produce this:
Error: Error acquiring the state lockOpenTofu na fork of Terraform wey get the same commands and same file format. As of July 2026, everything for this guide dey work if you type tofu instead of terraform.
Wetin Ansible really dey do
Ansible no need agent or API. E dey open SSH connection, copy small Python module go target, run am, then delete am. Anything wey you fit reach with SSH and sudo password, Ansible fit configure.
- name: Base web server
hosts: web
become: true
tasks:
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
- name: Ensure nginx is running at boot
ansible.builtin.service:
name: nginx
state: started
enabled: trueansible -i inventory.ini web -m ansible.builtin.ping
ansible-playbook -i inventory.ini site.yml --check --diff
ansible-playbook -i inventory.ini site.ymlThe ping module dey confirm SSH, Python, and sudo before you start debug playbook. Healthy result na web1 | SUCCESS => {"ping": "pong"}. The --check --diff run na the closest thing wey Ansible get to plan: e report wetin go change without changing am. But tasks wey depend on earlier tasks fit report wrong for check mode, because the earlier change never really happen.
Every run dey end with recap like ok=6 changed=2 unreachable=0 failed=0. Run the same playbook two times. The second run suppose report changed=0. Task wey report changed for every run no dey idempotent. Most times, na command or shell task wey suppose don be real module. If this matter still new to you, start with your first Ansible playbook for one VPS and build from there.
Where the two tools overlap, and where they fight
Terraform fit run commands for new server with the remote-exec provisioner. HashiCorp own documentation call provisioners last resort. Good reasons dey.
Provisioner dey run only when dem create the resource. If you edit the script, nothing happen for existing server, because for Terraform side, the resource already match the code. Provisioner steps no dey show for terraform plan, so your review no go see any sign of dem. If the script fail, Terraform mark the resource as tainted, and the next apply go destroy and rebuild server wey probably dey okay.
The failure timing sef bad. Provider report server as created immediately API talk say e don create am, while operating system still dey boot and sshd never dey listen yet.
Error: remote-exec provisioner error
timeout - last error: dial tcp 203.0.113.10:22: connect: connection refusedAnsible get the opposite temptation. Cloud modules fit create servers, and for small number of machines, e dey work. But you lose the dependency graph and state file. Ansible go gladly create resource, but if you delete the task from your playbook, the resource go remain running and billing go continue, because nothing record say na your resource.
The rule wey come from this one be say: make Terraform own objects wey API dey create and destroy, while Ansible own everything inside operating system wey don boot.
The handoff wey work
The handoff na boundary, e no be integration. Terraform finish, publish one address, then stop. Ansible start from that address.
terraform apply -auto-approve
terraform output -raw web_ip
printf '[web]\n%s ansible_user=root\n' "$(terraform output -raw web_ip)" > inventory.ini
ansible -i inventory.ini web -m ansible.builtin.ping
ansible-playbook -i inventory.ini site.ymlterraform output -raw dey print one value without quotes and without JSON wrapper. Na so you want am inside shell substitution. If na several servers, use terraform output -json and build the inventory from there, because -raw fit handle only one string, number, or boolean.
The ping step between the two tools worth keeping. E separate “Terraform give me the wrong address” from “my playbook get bug.” Both problems go look the same when na the playbook first thing wey ever touch the new box.
Reading Terraform state as an Ansible inventory
If you no wan write inventory file at all, the cloud.terraform collection go read the state directly.
ansible-galaxy collection install cloud.terraformWrite terraform.yml beside your playbook:
plugin: cloud.terraform.terraform_provider
project_path: /home/deploy/infraansible-inventory -i terraform.yml --graph
ansible-playbook -i terraform.yml site.ymlYou need know two things before you depend on am. The plugin dey run terraform show against project_path, so that directory must don initialize before; otherwise, the plugin go fail. E also no dey create hosts from your server resources by itself. E dey read ansible_host and ansible_group resources, wey you declare inside your Terraform code with the Ansible provider. Nothing go show for ansible-inventory --graph until you add dem.
Plain generated inventory file dey easier to debug, and e dey work with any provider. The plugin become useful when inventory don pass small number of machines and manual editing start dey cause typos. Na for that same point managing several Linux servers from one control machine don become real workflow, no be just habit.
You really need Terraform?
Most people wey dey read this no need am, at least not yet. Terraform dey worth the cost when creating and destroying infrastructure na repeated task by itself. If you order one VPS through control panel and plan to keep am for two years, Terraform go describe something wey happen once, plus state file wey you must not lose.
Use Terraform when you dey rebuild environments often, when staging must match production exactly, when different people dey change infrastructure and you want plan wey people fit review before anything delete, or when wetin you manage don pass servers reach DNS records, load balancers, and firewall rules wey dey inside provider API.
Use Ansible alone when servers dey run for long and dem no plenty, and when the daily question na "this box dey configured correctly?" instead of "this box dey exist?" One playbook wey harden fresh server fit cover the same work as the first ten minutes for new VPS, plus e get advantage say e go run the same way for the next server.
The order to learn dem follow from this. Ansible go pay back from the first server wey you own. Terraform go pay back from the third environment wey you rebuild.
Handoff wey spoil
Server no ready. Terraform succeed, Ansible fail immediately.
fatal: [web1]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: ssh: connect to host 203.0.113.10 port 22: Connection refused", "unreachable": true}API return address before sshd start listen. Wait for the port instead of adding fixed sleep. Ansible get ansible.builtin.wait_for_connection for exactly this purpose; run am as the first task for the play. Once the same playbook dey target group instead of one fresh box, decide beforehand wetin suppose happen when one host remain unreachable, because Ansible go remove that host from the rest of the run, and recap line na the only place wey e tell you.
Host key don change. You destroy and recreate server, and the new one answer for the same address with new key.
Host key verification failed.Remove the stale entry with ssh-keygen -R 203.0.113.10. This one dey happen often once Terraform dey rebuild things, so e make sense to avoid frequent rebuilds for machines wey hold data.
Sudo fail. fatal: [web1]: FAILED! => {"msg": "Missing sudo password"} mean say become: true need password for that host. Either configure passwordless sudo for the deploy user, or pass --ask-become-pass.
Terraform want destroy something wey you no touch. Plan show changes wey you never write, meaning real infrastructure don drift from the code. Usually na because person change setting for provider web panel. Run terraform plan -refresh-only to see that difference by itself, then decide whether na the code or the live resource get problem. Never apply destructive plan wey you no fit explain line by line.
Ansible report changed for every run. A shell task wey no get creates or when guard dey run every time. This no be cosmetic problem, because e mean say you no fit use changed=0 again as the signal say server dey the state wey you request.
FAQ
Terraform fit replace Ansible?
No be for configuration inside server. Terraform fit call scripts with the remote-exec provisioner, but dem go run only when resource dey create. Dem no go ever appear for terraform plan, and if dem fail, dem go taint the resource. This one go schedule destroy and rebuild for the next apply. Terraform no get equivalent of module wey go check whether nginx don already install and do nothing if e dey there. Use Terraform create the machine, then hand over to Ansible.
Ansible fit replace Terraform?
For small number of long-lived servers, yes. Ansible get cloud modules wey fit create servers. If you order two VPS instances and keep dem, na enough. Wetin you lose na the state file and dependency graph. If you remove task from playbook, the resource go still dey run and billing go still continue, because Ansible never record say na e create am. Terraform for don plan destroy.
Which one I suppose learn first?
Ansible, if you get servers today. E go pay back from the first machine, e need only SSH, and the skill go apply to server wey you order by hand. Terraform go pay back later, when you dey rebuild environments often or manage provider resources beyond servers, like DNS records and firewall rules.
How I fit pass the new server IP from Terraform enter Ansible?
Declare an output for your Terraform code, then read am after apply. terraform output -raw web_ip go print the bare value for shell substitution, while terraform output -json go give you all outputs at once when several hosts dey. Write the value enter inventory file, or install the cloud.terraform collection and point ansible-inventory -i terraform.yml --graph to the project directory.
Why my playbook dey fail immediately after Terraform finish?
The provider dey report the server as created once the API talk say so, but the operating system still dey boot. Because of this, SSH go refuse connection for the first few seconds. The error na UNREACHABLE! with Connection refused. Make ansible.builtin.wait_for_connection the first task for the play instead of guessing sleep duration, because boot time dey change based on the image and the plan.