Stage 4 · Provision
Terraform Fundamentals
Provisioners & Local-exec
When to use them, alternatives, and the null_resource pattern — the last resort for infrastructure setup.
What Are Provisioners?
Provisioners execute actions on a resource after it is created or before it is destroyed. They are escape hatches for actions Terraform cannot express declaratively — running a script, copying a file, or bootstrapping software.
The Terraform documentation explicitly states that provisioners should be a last resort. They make Terraform graphs fragile, are difficult to test, and fail silently. Prefer cloud-init, Packer, or configuration management tools instead.
Remote-exec Provisioner
The remote-exec provisioner connects to a remote resource over SSH or WinRM and runs commands. It waits for the connection before executing scripts.
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
key_name = aws_key_pair.deployer.key_name
provisioner "remote-exec" {
connection {
type = "ssh"
user = "ec2-user"
private_key = file("~/.ssh/id_rsa")
host = self.public_ip
}
inline = [
"sudo yum update -y",
"sudo yum install -y nginx",
"sudo systemctl start nginx",
]
}
}The connection block configures how Terraform reaches the resource. The inline list runs commands sequentially. If any command fails, Terraform marks the resource as tainted and recreates it on the next apply.
Local-exec Provisioner
The local-exec provisioner runs a command on the machine running Terraform. Use it for tasks that must happen locally — generating config files, triggering webhooks, or calling external APIs.
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
provisioner "local-exec" {
command = "ansible-playbook -i ${self.public_ip}, bootstrap.yml"
environment = {
ANSIBLE_HOST_KEY_CHECKING = "False"
}
}
}The local-exec provisioner runs on the Terraform machine. The command can use self references to the resource being provisioned. The environment block sets variables for the command.
File Provisioner
The file provisioner copies files from the local machine to the remote resource. It is commonly used to upload configuration files, scripts, or certificates.
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
provisioner "file" {
source = "scripts/bootstrap.sh"
destination = "/tmp/bootstrap.sh"
connection {
type = "ssh"
user = "ec2-user"
private_key = file("~/.ssh/id_rsa")
host = self.public_ip
}
}
}The file provisioner uploads local files to the remote resource. source is the local path. destination is the remote path. Use this to transfer config files before running remote-exec to execute them.
The null_resource Pattern
The null_resource is a placeholder that triggers provisioners based on triggers. It is the most common way to run provisioners that depend on infrastructure changes but do not need to modify a specific resource.
resource "null_resource" "configure" {
triggers = {
vpc_id = aws_vpc.main.id
subnets = join(",", aws_subnet.private[*].id)
revision = var.config_revision
}
provisioner "local-exec" {
command = "python3 configure.py --vpc ${aws_vpc.main.id}"
}
depends_on = [aws_subnet.private]
}The triggers block determines when the provisioner runs. If any trigger value changes, the provisioner executes on the next apply. The revision trigger lets you force re-execution by changing a variable.
When to Use Provisioners
- Bootstrapping a virtual machine when no cloud-init or UserData option exists.
- Running a local script after infrastructure changes (webhook, notification).
- Copying files to a resource that does not support user_data.
- Running database migrations that cannot be expressed declaratively.
- Triggering a downstream system that has no provider.
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
user_data = <<-EOF
#!/bin/bash
yum update -y
yum install -y nginx
systemctl start nginx
EOF
}User data runs once at instance creation. It is idempotent, does not require SSH keys, and is visible in the AWS console. This is almost always better than remote-exec for bootstrapping.
Terraform builds a dependency graph from references. Provisioners create implicit dependencies that can cause ordering issues. If a provisioner fails, the entire apply may fail or leave resources in an inconsistent state.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.