
September 10, 2026
13 min read
By Kokil Thapa | Last reviewed: September 2026
Ansible vs Terraform: Config vs Provisioning is one of the first decisions teams face when they move from manual server setup to repeatable infrastructure. Terraform creates and tracks cloud resources—VPCs, disks, DNS records, load balancers. Ansible installs packages, copies configs, and keeps software state aligned on machines that already exist. The confusion starts because both tools are YAML-friendly, both run from a laptop or CI pipeline, and both claim to automate "infrastructure." In practice they solve different layers of the stack. This guide maps each tool to its job, shows how they fit around a typical Linux production deployment, and explains when one alone is enough versus when you need both.
What is the difference between provisioning and configuration management?
Provisioning means creating infrastructure objects that did not exist before. A new EC2 instance, a DigitalOcean droplet, a Cloudflare DNS A record, or a MySQL managed database all fall here. The output is durable infrastructure with identifiers, quotas, and billing attached.
Configuration management means shaping software on a host that already exists. Installing PHP 8.4, tuning PHP-FPM pools, deploying Nginx vhosts, creating deploy users, and enabling UFW rules are configuration tasks. The host might be bare metal, a VPS, or a VM Terraform created five minutes earlier.
Think of provisioning as building the house. Configuration management is wiring the electrical panel and hanging doors. You can paint walls before the roof exists, but the sensible order is structure first, then internals.
Terraform is declarative and provider-driven. You describe desired infrastructure; the Terraform engine calculates a plan against remote APIs and a local or remote state file. Ansible is primarily imperative at the playbook level. Tasks run top to bottom unless handlers or conditionals change flow. Ansible has no built-in cloud resource graph comparable to Terraform state.
For background on the same split from another angle, see the companion piece on Terraform provisioning versus Ansible configuration management.
Core terminology side by side
- Terraform resource — a managed object such as
aws_instanceordigitalocean_droplet. - Terraform state — a JSON snapshot of real-world IDs Terraform owns.
- Ansible inventory — the list of hosts Ansible connects to over SSH or WinRM.
- Ansible playbook — ordered tasks applied to inventory groups.
- Drift — Terraform detects when live cloud resources differ from state; Ansible detects when packages or files differ from playbooks on next run.
When should you choose Terraform over Ansible?
Choose Terraform when the primary work is creating, updating, or destroying cloud infrastructure with clear dependencies. If your task is "stand up a staging VPC with two app servers and a managed database," Terraform is the right default. Its graph engine understands that a subnet must exist before an instance attaches to it.
Choose Ansible when servers already exist and you need repeatable software setup. If your task is "install PHP 8.4, Redis 8.10, and Certbot on Ubuntu 24.04," Ansible is faster to write and easier for application teams to read than a chain of cloud-init scripts buried inside Terraform.
| Criteria | Terraform | Ansible |
|---|---|---|
| Primary job | Provision cloud/network/storage resources | Configure OS packages, services, files on hosts |
| Execution model | Declarative plan/apply with dependency graph | Task list over SSH (push) or pull via ansible-pull |
| State tracking | Central state file (local, S3, Terraform Cloud, etc.) | No resource graph; idempotency per module/task |
| Agent required | No agent on targets for cloud APIs | No agent; needs SSH access to hosts |
| Destroy / teardown | terraform destroy removes tracked resources | Playbooks rarely destroy cloud objects cleanly |
| Best fit team | Platform / DevOps owning cloud accounts | App ops / sysadmins owning server baselines |
| Learning curve for app devs | Steeper (HCL, providers, state backends) | Gentler YAML tasks mirroring shell steps |
A common mistake is forcing all server bootstrap into Terraform user_data or remote-exec provisioners. Those blocks run once at creation time. They are hard to test, hard to re-run safely, and they tie application config to infrastructure lifecycle. I've moved several client stacks away from long cloud-init scripts toward Terraform for the droplet plus Ansible for everything inside the OS.
If you are starting from a single VPS, read the Terraform for VPS provisioning beginners guide first. Keep Ansible for the LAMP or Laravel stack on top.
How do you use Ansible and Terraform together in production?
The pattern I use on shared EC2 infrastructure—and on sister legal-tech sites deployed through Deployer 7 and GitLab CI—is Terraform first, Ansible second, application deploy third. Terraform outputs IP addresses and hostnames. Ansible reads them from a generated inventory. Deployer or GitLab CI pushes Laravel releases after the baseline exists.
On projects like Notary Kathmandu and related sister sites, the provisioning layer stays small and stable. Configuration changes far more often than VPC topology. Splitting concerns keeps terraform apply out of daily deploy paths.
Step 1 — Provision with Terraform
A minimal DigitalOcean example creates the droplet and a firewall. Pin provider versions in a versions.tf block per HashiCorp guidance.
# main.tf — Terraform 1.x with digitalocean provider
terraform {
required_providers {
digitalocean = {
source = "digitalocean/digitalocean"
version = "~> 2.0"
}
}
}
resource "digitalocean_droplet" "app" {
name = "laravel-app-staging"
region = "sgp1"
size = "s-2vcpu-4gb"
image = "ubuntu-24-04-x64"
ssh_keys = [var.ssh_key_fingerprint]
tags = ["env:staging", "role:app"]
}
resource "digitalocean_firewall" "app" {
name = "laravel-app-fw"
droplet_ids = [digitalocean_droplet.app.id]
inbound_rule {
protocol = "tcp"
port_range = "22"
source_addresses = ["YOUR_OFFICE_IP/32"]
}
inbound_rule {
protocol = "tcp"
port_range = "80"
source_addresses = ["0.0.0.0/0", "::/0"]
}
inbound_rule {
protocol = "tcp"
port_range = "443"
source_addresses = ["0.0.0.0/0", "::/0"]
}
}
output "app_ip" {
value = digitalocean_droplet.app.ipv4_address
}
Run terraform init, terraform plan, and terraform apply. Store state remotely when more than one person touches the stack. The guide to managing Terraform state safely covers S3 backends and locking.
Step 2 — Generate Ansible inventory from Terraform output
After apply, pipe the IP into a static or dynamic inventory file. A simple approach uses a shell wrapper in CI:
terraform output -raw app_ip > .terraform/app_ip
echo "[laravel_app]" > inventory/hosts.ini
echo "$(cat .terraform/app_ip) ansible_user=deploy" >> inventory/hosts.ini
Validate the inventory JSON if you automate further—drop it through the JSON formatter when debugging dynamic scripts.
Step 3 — Configure with Ansible
An Ansible playbook installs the runtime your Laravel 13 app expects on PHP 8.3 or higher. See the dedicated Ansible playbooks for PHP server provisioning walkthrough for a full role layout.
# site.yml
- name: Baseline Laravel app server
hosts: laravel_app
become: true
vars:
php_version: "8.4"
app_user: deploy
tasks:
- name: Install base packages
ansible.builtin.apt:
name:
- nginx
- "php{{ php_version }}-fpm"
- "php{{ php_version }}-mysql"
- "php{{ php_version }}-mbstring"
- "php{{ php_version }}-xml"
- "php{{ php_version }}-curl"
- redis-server
- certbot
- python3-certbot-nginx
state: present
update_cache: true
- name: Create deploy user
ansible.builtin.user:
name: "{{ app_user }}"
groups: www-data
shell: /bin/bash
create_home: true
- name: Allow deploy user sudo for deploy scripts
ansible.builtin.copy:
dest: /etc/sudoers.d/deploy
content: "deploy ALL=(ALL) NOPASSWD: /bin/systemctl reload php{{ php_version }}-fpm\n"
mode: "0440"
Run ansible-playbook -i inventory/hosts.ini site.yml. Re-run the same playbook after package updates. Changed tasks only; unchanged tasks stay green.
Step 4 — Keep application deploy separate
Do not run git pull inside Terraform provisioners. Use Deployer, GitLab CI, or a dedicated Ansible role tagged deploy that runs only on release. Infrastructure apply should be weekly or on-demand. Application deploy may be daily. Mixing them causes unnecessary reprovisioning risk.
For reusable Ansible structure, adopt roles from the Ansible roles and Galaxy guide. For Terraform reuse, extract modules per the Terraform modules article.
How does Terraform state compare to Ansible's approach?
Terraform state is the source of truth for cloud object identity. It stores resource IDs, attributes, and dependency metadata. Without state, Terraform cannot safely update or destroy what it created. Remote backends with locking—documented in HashiCorp's Terraform state documentation—prevent two engineers from corrupting the same stack.
Ansible does not maintain a global graph of "all nginx configs across the fleet." Each task checks current node state via module logic and changes only what drifts. You discover drift by re-running the playbook or using check mode (--check). For secrets, encrypt vars with Ansible Vault as covered in the Ansible Vault for secrets post.
That difference drives operational habits:
- Run
terraform planbefore every apply and treat unexpected destroys as stop signals. - Store Terraform state remotely with versioning enabled on the backend bucket.
- Run Ansible playbooks on a schedule or after OS security patches.
- Never hand-edit cloud resources Terraform manages without importing or removing them from state.
- Tag Ansible roles by concern:
baseline,webserver,monitoring.
Terraform also excels at resources Ansible modules touch awkwardly—DNS at scale, IAM bindings, object storage buckets with lifecycle rules. Ansible excels at procedural sequences—restart FPM after ini change, run Certbot once per vhost, template a 40-line Nginx config from variables.
Some teams try Ansible's cloud modules as a Terraform replacement. It works for small footprints. At scale, lack of unified state and weak destroy semantics become painful. The infrastructure as code with Terraform practical guide explains why stateful provisioning wins for multi-resource stacks.
What are common mistakes when mixing Ansible and Terraform?
These failures show up repeatedly on production Laravel and WordPress hosts I maintain through support and maintenance engagements.
Using Terraform provisioners for all configuration
remote-exec and local-exec provisioners block apply completion, hide logs, and cannot be re-run independently. If PHP needs a module added six months later, you should not taint the entire droplet. Move that work to Ansible.
Letting Ansible create cloud resources without state
Ansible can call AWS or DigitalOcean APIs. Without Terraform's plan/destroy lifecycle, orphaned disks and stale DNS records accumulate. Use Ansible cloud modules for bootstrap hacks only, not long-lived production topology.
Splitting ownership with no contract between layers
Define a clear handoff: Terraform outputs IP, hostname, and firewall IDs. Ansible owns everything inside port 22/80/443. Application deploy owns /var/www releases. Document it in your internal runbook.
Ignoring secrets boundaries
Store cloud API tokens in CI variables or Terraform Cloud. Store database passwords and app keys in Ansible Vault or your existing secret manager. Do not commit either to Git. The Ansible Vault encrypt guide shows file-level patterns.
Running apply and playbook in the wrong order on brownfield servers
Import existing servers into Terraform before first apply, or restrict Terraform to net-new environments. Blind apply against manually built VPS instances causes duplicate resources or destructive replaces.
On brownfield website migration projects, I often import existing DNS and compute into Terraform while leaving Ansible to converge software state over one or two maintenance windows. That beats a big-bang rebuild for budget-sensitive Nepal clients.
For deeper Ansible-only workflows, the Ansible playbooks practical guide and automate server setup with Ansible posts cover inventory patterns without cloud coupling. Compare adjacent tools in Ansible vs Puppet vs Chef vs Salt if you are picking a config engine from scratch.
Official references worth bookmarking: Red Hat's Ansible getting started documentation for module index and connection plugins, plus HashiCorp's provider registry for supported cloud APIs in 2026.
Key Takeaways
- Terraform provisions cloud infrastructure with tracked state; Ansible configures software on existing hosts—different layers, not interchangeable substitutes.
- Run Terraform apply first to create VPS, network, DNS, and firewalls; run Ansible second to install PHP, Nginx, Redis, and system users.
- Avoid long
user_dataorremote-execscripts in Terraform—move OS configuration to idempotent Ansible playbooks you can re-run safely. - Store Terraform state remotely with locking; store application secrets in Ansible Vault or CI secret stores, never in plain Git.
- Keep daily Laravel or WordPress deploys in Deployer or GitLab CI, separate from infrastructure apply cycles.
- For a single small VPS, Terraform plus one Ansible playbook beats either tool trying to do both jobs poorly.
People Also Ask
Can Ansible replace Terraform?
Ansible can create some cloud resources through modules, but it lacks Terraform's unified state file, plan/destroy lifecycle, and dependency graph. For multi-resource cloud stacks with teardown requirements, Ansible alone is a poor replacement. Use Ansible to configure what Terraform provisions.
Can Terraform replace Ansible?
Terraform can run boot scripts via provisioners, but they execute once at creation and are painful to maintain. Terraform cannot replace ongoing configuration management—package updates, vhost edits, certificate renewals, or FPM tuning—where Ansible's idempotent tasks excel.
Which tool is easier for Laravel developers?
Ansible is easier for application-focused developers because playbooks mirror shell steps in YAML. Terraform requires learning HCL, providers, and state backends. Most Laravel teams touch Ansible playbooks occasionally; fewer need to write Terraform unless they own cloud accounts.
Do you need both for a single VPS?
Not strictly. Manual VPS purchase plus Ansible alone works for one server. Terraform adds value when you want reproducible infrastructure, documented firewalls, DNS in code, and safe teardown. For client sites on shared EC2, I still split the layers even at small scale because rebuild time drops from hours to minutes.
Pick the right layer, then automate the rest
Ansible vs Terraform: Config vs Provisioning is not a winner-take-all choice. Terraform owns the cloud graph. Ansible owns the operating system and middleware. Application deploy stays in your existing Git pipeline. That three-part split matches how production Laravel, WooCommerce, and legal-tech portals actually run on Ubuntu servers in 2026.
If you want help designing a Terraform plus Ansible baseline—or migrating off manual VPS setup—review the Adventure Third Pole Trek deployment stack in the portfolio and reach out through contact us for a scoped infrastructure review. For ongoing server care after automation lands, see Linux system administration services and domain and hosting setup.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

