The question "Ansible or Terraform" comes up for everyone automating server deployment for the first time. The short answer: these are different tools for different jobs, and in real projects they almost always work together. Terraform creates infrastructure — the server, the disk, the network — while Ansible configures what already exists. Let's look at where the line runs and how to build one working pipeline out of both tools.
What fundamentally separates Ansible and Terraform
Terraform is a tool for managing infrastructure as code. It describes the desired end state: how many servers are needed, with which disks, in which network, and it decides on its own what to create, change, or delete to reach that state. Ansible is a configuration management tool. It does not create servers by itself; it runs a sequence of steps on machines that already exist: install a package, place a file, restart a service.
The line is simple: Terraform answers "how many servers and what kind" — VDS instances, disks, a private network, firewall rules at the provider. Ansible answers "what is inside the server" — which web server, which PHP version, which users and cron jobs.
When to choose Terraform
Terraform is the right tool when infrastructure is described through a cloud provider's API: ordering a VDS, attaching a floating IP, setting up a private network between servers, creating a load balancer. If tomorrow you need to spin up an identical cluster of servers in another region, Terraform does it in one command.
- Ordering and removing servers through the provider API without manual clicks in a panel
- Versioning infrastructure in git — changes are visible in a diff before they are applied
- Reproducibility: staging and production are built from the same set of files
When to choose Ansible
Ansible is the right tool where servers already exist and need to be brought to the same state: install nginx and PHP-FPM, roll out SSH keys, configure a firewall at the OS level, apply security updates. It does not care where the server came from — Terraform, a manual order in the ZevsHost panel, or an old fleet of hardware.
- Configuring software and services on servers that already exist
- Applying changes across dozens of servers with a single command
- Tasks that don't fit the term "infrastructure": code deployment, log rotation, certificate renewal
Declarative versus procedural: the difference in practice
Terraform is declarative: the file describes the desired state, not a list of steps. Run terraform apply twice in a row with no changes in the files — the second run does nothing. Ansible is procedural in playbook structure but idempotent at the module level: the apt module will not reinstall an already installed package, and the copy module will not overwrite a file with the same content.
| Criterion | Terraform | Ansible |
|---|---|---|
| What it describes | Cloud resources: servers, disks, networks | OS and application state on the server |
| Model | Declarative, with a state file | Sequence of tasks against an inventory |
| State storage | State file, usually in S3 or Terraform Cloud | No stored state, checked on every run |
| Speed of a single-parameter change | Requires apply, may recreate the resource | Targeted playbook run on the needed host |
How to connect Terraform and Ansible in one pipeline
The standard scheme: Terraform creates the server and exposes its IP address through an output, Ansible takes that address as an inventory and configures the server. The example VDS provider here is Hetzner Cloud, but the scheme works with any provider that has an API.
resource "hcloud_server" "web" {
name = "web-01"
server_type = "cx22"
image = "ubuntu-24.04"
ssh_keys = [hcloud_ssh_key.main.id]
}
output "web_ip" {
value = hcloud_server.web.ipv4_address
}
Next, a dynamic inventory for Ansible is built from Terraform's output with a single command, and the playbook installs the base set of packages:
- hosts: web
become: true
tasks:
- name: Install nginx and PHP-FPM
apt:
name: ["nginx", "php-fpm"]
state: present
update_cache: true
- name: Start and enable nginx
service:
name: nginx
state: started
enabled: true
The full process — from ordering the server to a ready-to-work environment — is usually set up as a separate stage in a GitLab CI pipeline: first a job with terraform apply, then a job with ansible-playbook that gets the address from the first step's artifacts. For more on describing infrastructure as code, see the article on Terraform for VPS.
Common mistakes when combining the two
The first mistake is duplicated responsibility: part of the server setup is written through cloud-init inside Terraform, and part through Ansible, and six months later nobody remembers where to change what. The second is keeping Terraform's state file locally on a developer's laptop without a backup: if the file is lost, Terraform stops knowing what already exists and tries to recreate resources from scratch.
- Mixing OS configuration tasks between cloud-init and Ansible in one project
- A local Terraform state file with no remote backend and no locking
- Running Ansible without first checking the playbook with the
--checkmode
Summary: a short choice checklist
If you need to manage servers, disks, and networks at the provider — use Terraform. If you need to configure what is already running — use Ansible. In most projects both tools are used together, each in its own area of responsibility.
- Cloud infrastructure and its versions in git — Terraform
- Packages, configs, and services inside the server — Ansible
- Keep Terraform state remote, with locking
- Connect both steps with one CI pipeline instead of running them by hand