TerraformPilot

Terraform

How to Use Terraform Provisioners - When and Why

Learn when to use Terraform provisioners and when to avoid them. Covers local-exec, remote-exec, file provisioner, null_resource, and better alternatives.

LLuca Berton2 min read

Quick Answer

#

Provisioners run scripts on local or remote machines during resource creation/destruction. Use them as a last resort — prefer user_data, configuration management tools, or cloud-init. When you must use them, local-exec is safer than remote-exec.

The Three Provisioners

#

local-exec

#

Runs a command on the machine running Terraform:

resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"
 
  provisioner "local-exec" {
    command = "echo ${self.private_ip} >> inventory.txt"
  }
}

remote-exec

#

Runs commands on the remote resource via SSH or WinRM:

resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"
  key_name      = aws_key_pair.deploy.key_name
 
  connection {
    type        = "ssh"
    user        = "ubuntu"
    private_key = file("~/.ssh/deploy.pem")
    host        = self.public_ip
  }
 
  provisioner "remote-exec" {
    inline = [
      "sudo apt-get update",
      "sudo apt-get install -y nginx",
      "sudo systemctl enable nginx",
    ]
  }
}

file

#

Copies files or directories to the remote machine:

provisioner "file" {
  source      = "configs/nginx.conf"
  destination = "/tmp/nginx.conf"
}
 
provisioner "remote-exec" {
  inline = ["sudo cp /tmp/nginx.conf /etc/nginx/nginx.conf"]
}

When to Use (and When NOT to)

#

✅ Use Provisioners When

#
  • Running a one-time setup script that cloud-init can't handle
  • Triggering an external system after resource creation (webhook, API call)
  • Generating local files from Terraform outputs (inventory files, configs)
  • No better alternative exists

❌ Avoid Provisioners When

#
Instead of...Use...
remote-exec for package installuser_data / cloud-init
remote-exec for configurationAnsible, Chef, Puppet
local-exec for API callsTerraform provider or http data source
file + remote-exec for app deployCI/CD pipeline

Better Alternatives

# #
resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"
 
  user_data = <<-EOF
    #!/bin/bash
    apt-get update
    apt-get install -y nginx
    systemctl enable nginx
    systemctl start nginx
  EOF
 
  user_data_replace_on_change = true
}

null_resource for Triggers

#
resource "null_resource" "deploy" {
  triggers = {
    app_version = var.app_version
  }
 
  provisioner "local-exec" {
    command = "./deploy.sh ${var.app_version} ${aws_instance.web.public_ip}"
  }
}

terraform_data (Terraform 1.4+)

#
resource "terraform_data" "deploy" {
  triggers_replace = [var.app_version]
 
  provisioner "local-exec" {
    command = "curl -X POST https://deploy.example.com/trigger"
  }
}

Provisioner Behavior

#
BehaviorDefaultOverride
When it runsOn creationwhen = destroy
On failureMarks resource taintedon_failure = continue
RetryNoneNot built-in
provisioner "local-exec" {
  when       = destroy
  on_failure = continue
  command    = "cleanup.sh ${self.id}"
}

Troubleshooting

#
  • Connection refused: Check security groups, SSH key, and username
  • Timeout: Increase connection { timeout = "10m" } — instance may still be booting
  • Permission denied: Wrong SSH user for the AMI, or wrong key
  • Tainted resource: Failed provisioner marks resource for recreation — fix the script and re-apply
#

Conclusion

#

Provisioners are a last resort in Terraform. Prefer user_data for bootstrapping, configuration management tools for ongoing configuration, and CI/CD for deployments. When you must use provisioners, local-exec is simpler and doesn't require SSH access.

#Terraform#DevOps#Infrastructure as Code

Share this article