Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Ansible vs Terraform: Config vs Provisioning

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.

Provisioning vs Configuration StackTerraform — Provisioning LayerVPC / NetworkCompute / VPSDNS RecordsStorageAnsible — Configuration LayerOS PackagesWeb ServerPHP / RuntimeApp Deploy User
Ansible vs Terraform: Config vs Provisioning mapped to infrastructure layers from cloud resources down to application runtime

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_instance or digitalocean_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.

CriteriaTerraformAnsible
Primary jobProvision cloud/network/storage resourcesConfigure OS packages, services, files on hosts
Execution modelDeclarative plan/apply with dependency graphTask list over SSH (push) or pull via ansible-pull
State trackingCentral state file (local, S3, Terraform Cloud, etc.)No resource graph; idempotency per module/task
Agent requiredNo agent on targets for cloud APIsNo agent; needs SSH access to hosts
Destroy / teardownterraform destroy removes tracked resourcesPlaybooks rarely destroy cloud objects cleanly
Best fit teamPlatform / DevOps owning cloud accountsApp ops / sysadmins owning server baselines
Learning curve for app devsSteeper (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.

Which Tool When?What are you changing?Cloud API objectsVPC, DNS, LB, diskSoftware on a hostPHP, Nginx, usersBoth layersTypical prod stackUse TerraformUse AnsibleTerraform thenAnsibleMost Laravel VPS projects land in the combined path
Decision flow for Ansible vs Terraform: Config vs Provisioning based on whether you touch cloud APIs or host software

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.

Combined Terraform + Ansible PipelineDeveloperpush / MRGitLab CIplan stageterraform applycreates VPSansible-playbookconfigures OSCloud: Droplet + Firewall + DNS (Terraform state)Outputs IP to dynamic inventoryHost: Nginx + PHP-FPM + Redis (Ansible idempotent)Re-run anytime without reprovisioningDeployer 7 — daily Laravel releases
Typical Ansible vs Terraform: Config vs Provisioning pipeline for Laravel on Ubuntu with GitLab CI and Deployer

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:

  1. Run terraform plan before every apply and treat unexpected destroys as stop signals.
  2. Store Terraform state remotely with versioning enabled on the backend bucket.
  3. Run Ansible playbooks on a schedule or after OS security patches.
  4. Never hand-edit cloud resources Terraform manages without importing or removing them from state.
  5. 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.

Anti-Pattern vs Recommended SplitBefore: Everything in user_dataSingle bash script at bootRuns once — fails silentlyHard to test locallyTaint droplet to change PHPNo idempotent re-runLogs lost in cloud consoleAfter: Terraform + AnsibleTerraform — small HCLDroplet, firewall, DNSPlan/apply with state lockAnsible — readable YAMLPHP, Nginx, Redis, CertbotRe-run anytime — check modeFixSame outcome — safer day-two operations
Replacing monolithic cloud-init with Ansible vs Terraform: Config vs Provisioning separation improves testability and day-two changes

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_data or remote-exec scripts 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

Provisioning creates infrastructure that did not exist before, such as EC2 instances, DigitalOcean droplets, DNS records, or managed databases. Configuration management shapes software on hosts that already exist, like installing PHP, tuning PHP-FPM, deploying Nginx vhosts, creating deploy users, and enabling firewall rules.

Terraform provisions cloud infrastructure with tracked state. Ansible configures existing servers by installing packages and applying settings. Use Terraform first to build the environment, then Ansible to prepare the application runtime.

Choose Terraform when the primary work is creating, updating, or destroying cloud infrastructure with clear dependencies, such as standing up a staging VPC with app servers and a managed database. Its graph engine understands that subnets, instances, and related resources must be created in the correct order.

Choose Ansible when servers already exist and you need repeatable software setup. Tasks like installing PHP 8.4, Redis 8.10, and Certbot on Ubuntu 24.04 are faster to write in Ansible playbooks than as cloud-init scripts or Terraform provisioners. Application teams usually find Ansible YAML easier to read than HCL and provider configuration.

The pattern is Terraform first, Ansible second, application deploy third. Terraform creates droplets, firewalls, and network objects, then outputs IP addresses and hostnames. Ansible reads those values from a generated inventory and installs Nginx, PHP-FPM, Redis, Certbot, and system users. Deployer or GitLab CI handles Laravel releases only after that baseline exists, keeping terraform apply out of daily deploy paths.

Terraform state is the source of truth for cloud object identity, storing resource IDs, attributes, and dependency metadata. Without it, Terraform cannot safely update or destroy what it created. Ansible has no global resource graph; each task checks the current node via module logic and changes only what drifted. Run terraform plan before apply, and re-run Ansible playbooks or use check mode to detect configuration drift on hosts.

Ansible cloud modules can work for small footprints, but they lack Terraform's unified state and clean destroy lifecycle. Without a central plan and state file, orphaned disks and stale DNS records accumulate over time. For multi-resource stacks with dependencies, Terraform's stateful provisioning is the safer default. Reserve Ansible cloud modules for short bootstrap hacks, not long-lived production topology.

Those provisioners run once at creation time, block apply completion, hide logs, and cannot be re-run independently. If you need a PHP module six months later, you should not taint the entire droplet. Long cloud-init or remote-exec scripts tie application configuration to infrastructure lifecycle and are hard to test. Move OS configuration to idempotent Ansible playbooks you can run safely whenever software state changes.

Repeated failures include using Terraform provisioners for all configuration, letting Ansible create cloud resources without state tracking, splitting ownership with no handoff contract, storing secrets in plain Git, and running apply against brownfield servers without importing them first. Define clear boundaries: Terraform outputs IP, hostname, and firewall IDs; Ansible owns everything inside the OS; application deploy owns release directories under the web root.

After terraform apply, pipe Terraform outputs into a static inventory file. A typical CI wrapper runs terraform output -raw app_ip, writes the value to a local file, then builds inventory/hosts.ini with a group such as laravel_app and ansible_user=deploy on the target line. Validate generated inventory JSON when you automate further, especially if dynamic inventory scripts sit between Terraform and Ansible.

No. Do not run git pull or application releases 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, while application deploy may be daily. Mixing provisioning and code deploy increases reprovisioning risk and makes routine releases harder to test and roll back.

In Terraform, drift means live cloud resources differ from what state and configuration expect; terraform plan surfaces unexpected changes or destroys before apply. In Ansible, drift means packages, files, or services on a host differ from playbook definitions, discovered by re-running the playbook or using check mode. Terraform drift often comes from manual console edits; Ansible drift often comes from manual package installs or config edits on servers.

Store cloud API tokens in CI variables or Terraform Cloud, not in plain Git. Store database passwords and application keys in Ansible Vault or your existing secret manager. Terraform state can contain sensitive attributes, so use remote backends with access controls and versioning. Ansible Vault encrypts variable files at rest. Never commit unencrypted secrets for either tool, and keep cloud credentials separate from application secrets.

Import existing DNS and compute into Terraform before the first apply, or restrict Terraform to net-new environments. Blind apply against manually built VPS instances can create duplicates or trigger destructive replaces. Leave Ansible to converge software state over one or two maintenance windows rather than forcing a big-bang rebuild. That incremental path works well for budget-sensitive migrations where uptime and cost matter.

Platform or DevOps teams owning cloud accounts usually own Terraform because it manages VPCs, firewalls, DNS, and state backends. App ops or sysadmins owning server baselines usually own Ansible because it installs runtimes, templates Nginx configs, and maintains OS packages. Document the handoff in an internal runbook so daily application deploys stay with Deployer or GitLab CI and neither team crosses into the other's layer without coordination.

Share this article

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.

Quick Contact Options
Choose how you want to connect me: