100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
HCL

Provisioners and When to Avoid Them

Understand Terraform's provisioner blocks for running scripts on resource creation, why HashiCorp considers them a last resort, and what to use instead.

Multi-Cloud & ProvisioningIntermediate8 min readJul 9, 2026
Analogies

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.

hcl
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.

hcl
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

Was this page helpful?

Topics covered

#HCL#TerraformInfrastructureAsCodeStudyNotes#DevOps#ProvisionersAndWhenToAvoidThem#Provisioners#Avoid#Them#Main#StudyNotes#SkillVeris