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. 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. That's 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