When learning Terraform, most people stop at creating infrastructure.
But what if you want to run scripts, install packages, or trigger actions after a resource is created?
That's where Terraform Provisioners come in.
In this blog, I will walk through:
- What provisioner really are
- When (and when not) to use them
- A hands-on AWS EC2 demo using:
-
local-exec-remote-exec-file + remote-exec
What are Terraform Provisioners?
Provisioners let Terraform execute scripts or commands during resource creation or destruction.
They’re useful for tasks that Terraform can’t model declaratively, such as:
- Installing software
- Running bootstrap scripts
- Registering resources in external systems
- Sending notifications
- Copying files to servers
⚠️ Terraform officially recommends using provisioners as a last resort.
Preferuser_data, cloud-init, Packer, or configuration management tools for serious production setups.
Types of Provisioners
1. local-exec
Runs commands on your local machine.
Use case:
- Trigger webhooks
- Call APIs
- Write to local inventory files
- Send Slack notifications
provisioner "local-exec" {
command = "echo 'Local-exec: created instance ${self.id} with IP ${self.public_ip}'"
}
2. remote-exec
Runs commands on the remote resource via SSH (Linux) or WinRM (Windows).
Use Cases:
- Install packages (nginx, docker, node, etc.)
- Configure OS settings
- Start services
- Quick bootstrap scripts
provisioner "remote-exec" {
inline = [
"sudo apt-get update",
"echo 'Hello from remote-exec' | sudo tee /tmp/remote_exec.txt",
]
}
3. file
Copies files from your machine to the remote resource.
Use Cases:
- Copy setup scripts
- Upload config files
- Transfer certificates
- Deploy small binaries
provisioner "file" {
source = "${path.module}/scripts/welcome.sh"
destination = "/tmp/welcome.sh"
}
provisioner "remote-exec" {
inline = [
"sudo chmod +x /tmp/welcome.sh",
"sudo /tmp/welcome.sh"
]
}
Architecture
Laptop / CI
│
▼
Terraform
│
▼
AWS EC2 (Ubuntu)
│
├── local-exec → runs locally
├── remote-exec → runs via SSH on EC2
└── file → copies scripts to EC2
-> Prerequisites
Before running the demo:
- AWS credentials configured
- Terraform v1.0+ installed
- AWS CLI installed
- SSH client installed
- An EC2 key pair created
aws ec2 create-key-pair --key-name terraform-demo-key \
--query 'KeyMaterial' --output text > terraform-demo-key.pem
chmod 400 terraform-demo-key.pem
Main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}
# EC2 instance used for provisioner demos.
# Each provisioner block is included below but wrapped in block comments (/* ... */).
# For the demo, uncomment one provisioner block at a time, then `terraform apply`.
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical (Ubuntu official) - Current owner ID
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
resource "aws_security_group" "ssh" {
name = "tf-prov-demo-ssh"
description = "Allow SSH inbound"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "demo" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
key_name = var.key_name
vpc_security_group_ids = [aws_security_group.ssh.id]
tags = {
Name = "terraform-provisioner-demo"
}
connection {
type = "ssh"
user = var.ssh_user
private_key = file(var.private_key_path)
host = self.public_ip
timeout = "5m"
}
/*
------------------------------------------------------------------
Provisioner 1: local-exec
- Runs on the machine where you run Terraform (your laptop/CI agent).
- Useful for local tasks, logging, calling local scripts, etc.
- To demo: uncomment this block, then run `terraform apply`.
------------------------------------------------------------------
*/
# provisioner "local-exec" {
# command = "echo 'Local-exec: created instance ${self.id} with IP ${self.public_ip}'"
# }
/*
------------------------------------------------------------------
Provisioner 2: remote-exec
- Runs commands on the remote instance over SSH.
- Requires SSH access (security group + key pair + reachable IP).
- To demo: uncomment this block, ensure `var.private_key_path` is correct, then run `terraform apply`.
------------------------------------------------------------------
*/
provisioner "remote-exec" {
inline = [
"sudo apt-get update",
"echo 'Hello from remote-exec' | sudo tee /tmp/remote_exec.txt",
]
}
/*
------------------------------------------------------------------
Provisioner 3: file + remote-exec
- Copies a script (scripts/welcome.sh) to the instance, then executes it.
- Good pattern for more complex bootstrapping when script files are preferred.
- To demo: uncomment both the file provisioner and the remote-exec block below.
------------------------------------------------------------------
*/
# provisioner "file" {
# source = "${path.module}/scripts/welcome.sh"
# destination = "/tmp/welcome.sh"
# }
# provisioner "remote-exec" {
# inline = [
# "sudo chmod +x /tmp/welcome.sh",
# "sudo /tmp/welcome.sh"
# ]
# }
}
Demo 1: local-exec
provisioner "local-exec" {
command = "echo 'Created ${self.id} with IP ${self.public_ip}'"
}
What happens:
- EC2 is created
- Terraform prints a message on your computer
- Nothing changes inside the EC2
Demo 2: remote-exec
provisioner "remote-exec" {
inline = [
"sudo apt-get update",
"echo 'Hello from remote-exec' | sudo tee /tmp/remote_exec.txt"
]
}
What happens:
- Terraform waits for SSH
- Connects to EC2
- Runs the commands remotely
- Creates
/tmp/remote_exec.txt
Verify:
ssh -i terraform-demo-key.pem ubuntu@<PUBLIC_IP>
cat /tmp/remote_exec.txt
Expected output:
Hello from remote-exec
Demo 3: file + remote-exec
provisioner "file" {
source = "scripts/welcome.sh"
destination = "/tmp/welcome.sh"
}
provisioner "remote-exec" {
inline = [
"sudo chmod +x /tmp/welcome.sh",
"sudo /tmp/welcome.sh"
]
}
welcome.sh
#!/bin/bash
echo "Welcome to the Provisioner Demo" | sudo tee /tmp/welcome_msg.txt
uname -a | sudo tee -a /tmp/welcome_msg.txt
What happens:
- Script is copied to EC2
- Script is executed
- Output file is created at
/tmp/welcome_msg.txt
Resources
>> Connect With Me
If you enjoyed this post or want to follow my #30DaysOfAWSTerraformChallenge journey, feel free to connect with me here:
💼 LinkedIn: Amit Kushwaha
🐙 GitHub: Amit Kushwaha
📝 Hashnode / Amit Kushwaha
🐦 Twitter/X: Amit Kushwaha
Found this helpful? Drop a ❤️ and follow for more AWS and Terraform tutorials!
Questions? Drop them in the comments below! 👇

Top comments (0)