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. 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"
}
}
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.