Terraform Provisioners Guide: Local-Exec & Remote-Exec
Terraform Provisioners are one of the most misunderstood features in Terraform. They allow you to run scripts or commands during infrastructure deployment, but they should be used carefully.
This Terraform Provisioners Guide explains how provisioners work, when to use local-exec and remote-exec, how destroy-time provisioners behave, and why HashiCorp recommends treating provisioners as a last resort whenever possible.
If you're new to Terraform, consider starting with our guides on Why Terraform, Installing Terraform, and Getting Started with Terraform on AWS.
What Are Terraform Provisioners and When Should You Use Them?
Terraform Provisioners allow you to execute commands or scripts as part of the resource lifecycle.
Provisioners typically run at one of two stages:
- Create-time provisioning (after a resource is created)
- Destroy-time provisioning (before a resource is destroyed)
Provisioners can execute commands:
- On the machine running Terraform (
local-exec) - On the provisioned resource itself (
remote-exec)
While provisioners are powerful, the official Terraform documentation recommends using provider-native features whenever possible because provisioners operate outside Terraform's declarative workflow.
You can learn more about Terraform's resource lifecycle in our guide on Resource Dependencies in Terraform.
Pro Tip
Provisioners should be viewed as a fallback mechanism rather than a primary configuration tool. Features such as AWS
user_data, Azurecustom_data, cloud-init, or image-based deployments are usually more reliable and easier to maintain.
For official guidance, refer to the Terraform Provisioners Documentation.
How Remote-Exec Provisioners Work
The remote-exec provisioner allows Terraform to connect to a server after creation and execute commands remotely.
This is commonly used for:
- Installing software
- Running bootstrap scripts
- Configuring services
- Performing initial server setup
Running Commands on a Remote Server
The example below installs and starts NGINX on an Ubuntu EC2 instance.
provisioner "remote-exec" {
inline = [
"sudo apt update",
"sudo apt install nginx -y",
"sudo systemctl enable nginx",
"sudo systemctl start nginx"
]
}
After Terraform creates the instance, these commands run on the target machine.
If you're following along with AWS examples, see our complete guide on AWS EC2 with Terraform.
Configuring SSH Connections
Terraform needs a connection method before it can execute remote commands.
For Linux servers, SSH is the most common option.
connection {
type = "ssh"
host = self.public_ip
user = "ubuntu"
private_key = file("~/.ssh/web.pem")
}
This configuration instructs Terraform to:
- Connect using SSH
- Use the instance's public IP
- Authenticate with a private key
- Run commands as the specified user
Without a valid connection block, remote-exec cannot establish communication with the resource.
A Real-World Example
Many teams use remote-exec to install:
- Monitoring agents
- Security agents
- Internal bootstrap scripts
- Log forwarding tools
However, as environments grow, engineers often move this logic into:
- Golden machine images
- Cloud-init scripts
- Configuration management tools
- Container images
These approaches reduce dependency on SSH connectivity and improve deployment reliability.
How Local-Exec Provisioners Work
Unlike remote-exec, the local-exec provisioner runs commands on the machine where Terraform is executed.
The command never runs on the provisioned resource.
Running Commands on Your Local Machine
Common use cases include:
- Creating deployment logs
- Triggering notifications
- Calling external APIs
- Running local automation scripts
Example:
provisioner "local-exec" {
command = "echo ${self.public_ip} > ~/instance_ip.txt"
}
After the resource is created, Terraform writes the public IP address to a local file.
Another common use case is integrating Terraform with external deployment pipelines or operational tooling.
What Happens When a Provisioner Fails?
Provisioners fail whenever the executed command returns a non-zero exit status.
By default, Terraform treats this as an error and stops the deployment.
In many cases, the affected resource may be marked as tainted and recreated during the next apply operation.
For a deeper understanding of tainted resources, see our guide on Terraform Taint Guide.
Using the on_failure Argument
You can instruct Terraform to continue execution even if a provisioner fails.
provisioner "local-exec" {
command = "./custom-script.sh"
on_failure = "continue"
}
This setting tells Terraform to proceed even when the provisioner encounters an error.
Common Causes of Provisioner Failures
In production environments, failures are often caused by connectivity issues rather than script errors.
Common examples include:
- Incorrect SSH credentials
- Closed security group ports
- Network routing problems
- Unreachable hosts
- Long server initialization times
When troubleshooting, always verify connectivity before debugging the script itself.
You can also review our guide on Debugging in Terraform for additional troubleshooting techniques.
Using Destroy-Time Provisioners for Cleanup Tasks
Terraform supports provisioners that execute immediately before a resource is destroyed.
These are called destroy-time provisioners.
Typical use cases include:
- Sending notifications
- Deregistering resources
- Archiving data
- Triggering cleanup workflows
Example:
provisioner "local-exec" {
when = destroy
command = "echo Deleting ${self.public_ip} >> ~/destroy_log.txt"
}
Before Terraform destroys the resource, it executes the specified command.
Destroy-time provisioners should be used carefully because failures during cleanup operations can complicate infrastructure removal workflows.
Terraform Provisioner Best Practices
Provisioners can solve legitimate problems, but experienced Terraform practitioners generally avoid relying on them whenever alternatives exist.
Use Provider-Native Features First
Whenever possible, prefer built-in provider functionality.
Examples include:
- AWS
user_data - Azure
custom_data - Cloud-init
- Managed bootstrap mechanisms
These options integrate naturally with Terraform's declarative model.
Keep Provisioners Simple
Avoid embedding large shell scripts directly inside Terraform configurations.
Complex automation is usually better handled by:
- Ansible
- Chef
- Puppet
- Image-building pipelines
This keeps Terraform configurations easier to maintain and debug.
Secure Connection Credentials
If you're using remote-exec, protect sensitive information.
Best practices include:
- Using SSH keys instead of passwords
- Storing secrets securely
- Limiting network access
- Following least-privilege principles
For AWS deployments, review our guides on AWS IAM with Terraform and AWS IAM Policies with Terraform.
Understand Provisioner Behavior
Terraform provisioners have unique execution rules that can surprise new users.
Before implementing them in production, review our detailed guide on Terraform Provisioner Behaviour.
Consider Provisioner Limitations
Before adopting provisioners, it's worth understanding their operational trade-offs.
Our guide on Considerations with Terraform Provisioners covers common pitfalls, maintenance challenges, and recommended alternatives.
Better Alternatives to Terraform Provisioners
In many environments, provisioners can be replaced with more reliable approaches.
Popular alternatives include:
- Cloud-init
- AWS User Data
- Azure Custom Data
- Packer-built machine images
- Configuration management tools
- Kubernetes bootstrap mechanisms
These approaches generally provide:
- Better repeatability
- Improved scalability
- Easier troubleshooting
- Cleaner Terraform configurations
For infrastructure automation patterns, you may also find our articles on Modules in Terraform, Creating and Using Modules in Terraform, and Using Modules from Terraform Registry helpful.
The official provider ecosystem can be explored through the Terraform Registry.
Key Takeaways
Terraform Provisioners can help bridge gaps when provider-native functionality isn't available.
They are useful for:
- Running bootstrap scripts
- Performing cleanup tasks
- Triggering external workflows
- Executing temporary automation
That said, provisioners are rarely the first choice in mature Terraform environments.
Whenever possible, prefer provider-supported features, cloud-init, image-based deployments, or dedicated configuration management tools. These approaches typically produce more predictable and maintainable infrastructure.
Frequently Asked Questions (FAQs)
Should I always use provisioners in Terraform?
No. Provisioners should only be used when provider-native functionality cannot satisfy the requirement. AWS user_data, Azure custom_data, and cloud-init are usually better options.
What's the difference between local-exec and remote-exec?
local-exec runs commands on the machine executing Terraform.
remote-exec connects to the provisioned resource and executes commands on that resource.
Can I use provisioners during destroy operations?
Yes. Terraform supports destroy-time provisioners using the when = destroy argument. These run immediately before the associated resource is removed.
What happens if a provisioner fails?
Terraform treats provisioner failures as errors by default. Depending on the situation, the deployment may stop and the resource may be marked as tainted.
You can override this behavior with:
on_failure = "continue"
Are Terraform provisioners secure?
Provisioners themselves are not inherently insecure, but they often require credentials, SSH access, or scripts containing sensitive information.
Follow security best practices such as:
- Using SSH keys
- Limiting network exposure
- Avoiding hardcoded secrets
- Applying least-privilege access controls
Does HashiCorp recommend using provisioners?
HashiCorp supports provisioners but recommends using them only as a last resort. Provider-native features and external configuration tools are generally preferred because they align better with Terraform's declarative model.
8 free, 100% client-side tools for developers — no signup, no data uploads.
Explore all tools