Provisioners and When to Avoid Them
Provisioners let a Terraform resource block run a script or command as part of resource creation or destruction — copying a file to a remote machine, running a shell command locally, or executing a script over SSH on a freshly created instance. They exist because Terraform's declarative model, which excels at describing infrastructure's desired end state, doesn't naturally express imperative, one-time setup steps. HashiCorp's own documentation is explicit that provisioners are a measure of last resort: they should only be used when no other Terraform-native mechanism can achieve the same outcome.
Cricket analogy: A provisioner running a script during resource creation is like a groundstaff crew doing last-minute pitch touch-ups right before a match starts — allowed, but the ICC's own guidance says only do it when nothing else in the standard prep process works.
The main provisioner types
local-exec runs a command on the machine executing Terraform itself, useful for local notifications or invoking an external CLI tool. remote-exec runs a command on the newly created remote resource over SSH or WinRM, commonly used to bootstrap software immediately after instance creation. file copies a file or directory from the local machine to the newly created remote resource, typically paired with remote-exec to then run the uploaded script.
Cricket analogy: local-exec is like a coach sending a text notification from the dugout, remote-exec is like radioing instructions directly to a fielder on the ground, and file is like passing a written note to that fielder before radioing the instructions to read it.
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
instance_type = "t3.micro"
key_name = aws_key_pair.deployer.key_name
connection {
type = "ssh"
user = "ec2-user"
private_key = file("~/.ssh/deployer.pem")
host = self.public_ip
}
provisioner "file" {
source = "scripts/bootstrap.sh"
destination = "/tmp/bootstrap.sh"
}
provisioner "remote-exec" {
inline = [
"chmod +x /tmp/bootstrap.sh",
"sudo /tmp/bootstrap.sh",
]
}
provisioner "local-exec" {
command = "echo ${self.public_ip} >> inventory.txt"
}
}Provisioners are not tracked in the Terraform state's desired-configuration model the way resource attributes are: if a remote-exec script fails partway through, Terraform marks the resource as tainted, but it has no way to know or reconcile what the script actually changed on the machine. This makes provisioner-driven setup fundamentally harder to reason about, retry safely, and drift-detect than declarative resource attributes.
Why HashiCorp recommends avoiding them
Provisioners break Terraform's core value proposition: they introduce imperative, order-sensitive, often network-dependent steps into an otherwise declarative and idempotent plan/apply cycle. A remote-exec provisioner can fail because of a transient SSH timeout unrelated to the actual infrastructure change, forcing a taint-and-recreate cycle even though the underlying resource was created successfully. Provisioners also can't be planned in advance the way resource attribute changes can — terraform plan cannot show you what a provisioner's script will do, only that it will run.
Cricket analogy: A remote-exec provisioner failing on a transient SSH timeout is like a run being disallowed because the third umpire's video link glitched for a second, forcing a costly re-take even though the batter's shot was clearly fine — terraform plan can't preview that glitch in advance.
Preferred alternatives
For instance bootstrapping, cloud-init / user_data (on AWS, Azure, GCP) is nearly always preferable: the cloud platform itself guarantees the script runs exactly once at boot, independent of Terraform's own network connectivity to the instance, and the script content can still be defined and templated in Terraform via the templatefile function. For configuration management beyond initial bootstrap, dedicated tools like Ansible, Chef, or Packer (for baking a pre-configured machine image ahead of time) are purpose-built for exactly this problem and integrate more robustly than a provisioner's SSH connection ever can.
Cricket analogy: cloud-init/user_data is like a match referee's pre-written toss protocol that runs automatically at the coin toss regardless of whether the broadcast van's signal is up, while Packer is like pre-preparing a standard pitch off-site rather than fixing it live on match day.
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
instance_type = "t3.micro"
# Preferred over remote-exec: cloud-init runs at boot, managed by AWS itself.
user_data = templatefile("${path.module}/templates/bootstrap.sh.tpl", {
environment = "production"
})
}A useful rule of thumb: if you can express the same outcome as a resource attribute (user_data, custom_data, metadata_startup_script) or by baking it into a machine image ahead of time with Packer, you almost never need a provisioner. Reach for provisioners only for genuinely one-off, Terraform-run-specific actions that have no infrastructure-native equivalent.
- Provisioners (local-exec, remote-exec, file) run imperative scripts during resource creation or destruction.
- HashiCorp explicitly documents provisioners as a last resort, not a default tool for bootstrapping.
- Provisioner failures taint the resource without Terraform understanding what partial changes the script made.
- Provisioners are not visible to terraform plan the way attribute changes are, reducing predictability.
- cloud-init/user_data mechanisms are the preferred, platform-native alternative for instance bootstrapping.
- Dedicated configuration management tools (Ansible, Chef) or pre-baked images (Packer) are preferred for ongoing or complex configuration.
Practice what you learned
1. What does HashiCorp's official documentation say about provisioners?
2. What provisioner type runs a command on the machine executing Terraform, rather than on the created resource?
3. What happens when a remote-exec provisioner fails partway through?
4. What is the preferred, platform-native alternative to a remote-exec provisioner for bootstrapping a new AWS EC2 instance?
5. Why can't terraform plan show what a provisioner's script will actually do?
Was this page helpful?
You May Also Like
Provisioning AWS Resources
A practical walkthrough of configuring the AWS provider and provisioning core compute, networking, and storage resources with Terraform.
Implicit vs Explicit Dependencies
Learn how Terraform automatically infers resource ordering from configuration references, and when you must override that inference with depends_on.
Common Terraform Pitfalls
A field guide to the mistakes Terraform users make most often — from state file mishandling to accidental destroys — and how to avoid each one.
Terraform Testing Frameworks
Survey the tools used to test Terraform code — from the built-in `terraform test` command to Terratest and Kitchen-Terraform — and when each fits into a testing strategy.
Related Reading
Related Study Notes in DevOps
Browse all study notesNginx Study Notes
DevOps · 30 topics
DevOpsAnsible Study Notes
DevOps · 30 topics
DevOpsAdvanced Kubernetes Study Notes
Kubernetes · 30 topics
DevOpsAdvanced Bash Scripting Study Notes
Bash · 30 topics
DevOpsApache Kafka Study Notes
Kafka · 30 topics
DevOpsDocker & Kubernetes Study Notes
YAML · 40 topics