What are provisioners, and why does HashiCorp consider them a last resort?
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"
}
}
filecopies a file to the new resource over SSH/WinRM.remote-execruns commands on the newly created resource.local-execruns a command on the machine running Terraform itself.
Why they're a "last resort"
- 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.
- Not part of the plan.
terraform plancan't preview what a provisioner will do — it's opaque imperative script execution bolted onto an otherwise declarative diff. - Coupled to reachability at apply time.
remote-exec/filerequire SSH/WinRM connectivity to the new resource at the exact momentapplyruns, which introduces network/credential dependencies that have nothing to do with whether the infrastructure itself was created correctly. - Not idempotent by default — re-running
applydoesn'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.