Provisioners in OpenTofu enable you to execute scripts or commands either on remote resources or locally on the machine running the OpenTofu binary. They are useful for bootstrapping instances or performing cleanup tasks but should be used sparingly.
Provisioner Types
Provisioner Type Execution Location Default Timing remote-exec Remote instance Create-time local-exec Local machine Create-time
Remote-Exec Provisioner
Use remote-exec to run shell commands on a newly created EC2 instance. Place the provisioner block inside your resource:
resource "aws_instance" "webserver" {
ami = "ami-0edad43b6fa892279"
instance_type = "t2.micro"
provisioner "remote-exec" {
inline = [
"sudo apt update" ,
"sudo apt install nginx -y" ,
"sudo systemctl enable nginx" ,
"sudo systemctl start nginx" ,
]
}
connection {
type = "ssh"
host = self . public_ip
user = "ubuntu"
private_key = file ( "/root/.ssh/web" )
}
key_name = aws_key_pair . web . id
vpc_security_group_ids = [ aws_security_group . ssh-access . id ]
}
A security group allowing SSH (22) or WinRM (5986 for Windows).
An SSH key pair created via aws_key_pair or your preferred key management.
Correct user name for your AMI (e.g., ubuntu, ec2-user, admin).
Example Resources
resource "aws_key_pair" "web" {
key_name = "web-key"
public_key = file ( "/root/.ssh/web.pub" )
}
resource "aws_security_group" "ssh-access" {
name = "allow-ssh"
description = "Allow SSH inbound"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [ "0.0.0.0/0" ]
}
}
When you run tofu apply, you’ll see:
$ tofu apply
aws_key_pair.web: Creating...
aws_security_group.ssh-access: Creating...
aws_instance.webserver: Creating...
aws_instance.webserver: Provisioning with 'remote-exec'...
aws_instance.webserver (remote-exec): Connecting to remote host via SSH...
aws_instance.webserver (remote-exec): Host: 3.96.136.157
aws_instance.webserver (remote-exec): User: ubuntu
aws_instance.webserver (remote-exec): Connected!
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
Local-Exec Provisioner
The local-exec provisioner runs commands on your workstation or CI/CD runner where OpenTofu is executed:
resource "aws_instance" "webserver" {
ami = "ami-0edad43b6fa892279"
instance_type = "t2.micro"
provisioner "local-exec" {
command = "echo ${ aws_instance . webserver . public_ip } >> /tmp/ips.txt"
}
}
After tofu apply, verify the file:
$ cat /tmp/ips.txt
54.214.68.27
Create-Time and Destroy-Time Hooks
By default, provisioners run after creation. You can also run them before destruction:
resource "aws_instance" "webserver" {
ami = "ami-0edad43b6fa892279"
instance_type = "t2.micro"
provisioner "local-exec" {
when = "create"
command = "echo Instance ${ aws_instance . webserver . public_ip } Created! > /tmp/instance_state.txt"
}
provisioner "local-exec" {
when = "destroy"
command = "echo Instance ${ aws_instance . webserver . public_ip } Destroyed! >> /tmp/instance_state.txt"
}
}
Handling Provisioner Failures
By default, a failed provisioner aborts the apply and marks the resource as tainted:
resource "aws_instance" "webserver" {
# ...
provisioner "local-exec" {
on_failure = "fail"
command = "echo Instance ${ aws_instance . webserver . public_ip } > /temp/instance_state.txt"
}
}
$ tofu apply
Error: Error running command 'echo 35.183.14.192 > /temp/instance_state.txt': exit status 1.
Output: The system cannot find the path specified.
To continue despite errors, set on_failure = "continue":
provisioner "local-exec" {
on_failure = "continue"
command = "echo Instance ${ aws_instance . webserver . public_ip } > /temp/instance_state.txt"
}
Overusing on_failure = "continue" can hide critical bootstrap errors. Use it only when failures are non-fatal.
Best Practices
Use provisioners only as a last resort .
Prefer native options when available:
AWS: user_data
Azure: custom_data
GCP: metadata.startup-script
Example using AWS user_data:
resource "aws_instance" "webserver" {
ami = "ami-0edad43b6fa892279"
instance_type = "t2.micro"
tags = {
Name = "webserver"
Description = "NGINX WebServer on Ubuntu"
}
user_data = <<- EOF
#!/bin/bash
sudo apt update
sudo apt install nginx -y
sudo systemctl enable nginx
sudo systemctl start nginx
EOF
}
Using user_data or cloud-init reduces complexity and maintains idempotency compared to provisioners.
Links and References