What are provisioners, and why does HashiCorp consider them a last resort?

5 minintermediateterraformprovisionersbest-practices

Quick Answer

Provisioners (`local-exec`, `remote-exec`, `file`) run scripts or copy files as part of resource creation/destruction — e.g., SSH-ing into a new VM to run a setup script. HashiCorp recommends them only as a last resort because they fall outside Terraform's declarative model: their success/failure isn't tracked in state the way a resource's is, they can't be planned/previewed, and they tightly couple provisioning to network reachability/credentials at apply time. Prefer provider-native mechanisms instead — cloud-init/user-data, a custom machine image (Packer), or a dedicated configuration-management tool — and reserve provisioners for genuine gaps with no better option.

Detailed Answer

Provisioners let a resource run an action (typically a script) as part of its creation or destruction, but they sit awkwardly outside Terraform's core declarative model — which is exactly why HashiCorp's own documentation recommends using them only when nothing else will do.

The three main provisioner types

resource "aws_instance" "web" {
  ami           = var.ami_id
  instance_type = "t3.micro"

  provisioner "file" {
    source      = "app.conf"
    destination = "/etc/app/app.conf"
  }

  provisioner "remote-exec" {
    inline = ["sudo systemctl restart app"]
  }

  provisioner "local-exec" {
    command = "echo ${self.private_ip} >> inventory.txt"
  }
}
  • file copies a file to the new resource over SSH/WinRM.
  • remote-exec runs commands on the newly created resource.
  • local-exec runs a command on the machine running Terraform itself.

Why they're a "last resort"

  1. Outside the state model. A provisioner's success/failure isn't tracked as a first-class part of resource state the way create/update/destroy is — if a provisioner fails partway through, the resource may be left in a state Terraform considers "created" but that's actually broken, and there's no clean automatic retry/rollback.
  2. Not part of the plan. terraform plan can't preview what a provisioner will do — it's opaque imperative script execution bolted onto an otherwise declarative diff.
  3. Coupled to reachability at apply time. remote-exec/file require SSH/WinRM connectivity to the new resource at the exact moment apply runs, which introduces network/credential dependencies that have nothing to do with whether the infrastructure itself was created correctly.
  4. Not idempotent by default — re-running apply doesn't naturally re-run provisioners unless the resource itself is replaced, unlike a proper configuration-management tool designed around repeated convergence.

What to use instead

  • Cloud-init / user-data for initial instance bootstrapping — declarative, provider-native, and doesn't require live SSH access during apply.
  • A custom machine image (built with Packer) that already has everything baked in, so there's no post-create configuration step at all.
  • A dedicated configuration-management tool (Ansible, Chef) run as a separate, purpose-built step after provisioning.

Provisioners remain a legitimate escape hatch for genuine gaps (odd one-off bootstrapping with no provider-native equivalent), but reaching for them by default is a common anti-pattern interviewers will probe for.

Related Resources