Today marks the Day 19 of 30 days of AWS Terraform challenge initiative by Piyush Sachdeva. Today we will deep dive into the Terraform Provisoners concept, what exactly is a provisoners. What are the different types of provisioners and how provisioners will be helpful in writing clean and effective code along with critical world test cases.
Provisoner:
Think of a Provisoner like something that performs a task like executing a script, running a command or doing some operation.
Provisioners are Terraform's way to execute scripts or commands during resource creation or destruction. They enable you to perform actions that go beyond Terraform's declarative resource management.
Key Concepts:
Bootstrapping: Performing initial setup like installing software, configuring services, or preparing an instance for service.
File Transfer: Copying files or directories between the machine running Terraform and the newly created remote resource.
Post-Deployment Cleanup/Operations: Executing final commands or scripts after a resource is created or before it is destroyed.
- Provisioners run during resource lifecycle events (creation or destruction)
- They are a "last resort" - Terraform recommends using native cloud-init, user_data, or configuration management tools when possible
- They execute only once during resource creation (not on updates)
- Failure handling: By default, if a provisioner fails, the resource is marked as "tainted" and will be recreated on next apply
Types of Provisioners:
There are 3 types of provisioners available based on their use cases such as local-exec, remote-exec and file provisioner.
Local-exec:
Local-exec provisioners are used to run basically on the machine where the Terraform host machine is present. It doesn't require any connection like SSH or RDP for execution.
Use cases:
- Trigger webhooks or API calls
- Update local inventory files
- Run local scripts for orchestration
- Send notifications (Slack, email)
- Register resources in external systems
Syntax:
provisioner "local-exec" {
command = "echo ${self.public_ip} >> inventory.txt"
}
In the above block, we are just executing a simple echo command on the Terraform host machine.
Tip: Always remember that provisoners must be inside of the Terraform resource blocks.
Remote-exec:
We will use this remote-exec provisioner when we would like to perform some tasks on remote machines by using SSH connectivity.
Like you have created a EC2 Instance resource using terraform and want to install nginx or apache server on that instance on the go in the same code, then you can use this remote exec provisoner which will wait till the creation of EC2 instance and then executes the installation of nginx.
The connection we will be using for this is SSH for Linux and WinRM for Windows.
Use cases:
- Install packages (nginx, docker, etc.)
- Run initialization commands
- Configure system settings
- Start services or daemons
- Quick bootstrap tasks
Syntax:
provisioner "remote-exec" {
inline = [
"sudo apt-get update",
"sudo apt-get install -y nginx",
"sudo systemctl start nginx"
]
}
We will be using an incline block while executing remote-exec provisoners.
file Provisioner:
We will use this file provisoner when we want to copy a file from one machine to another machine. For example in your local you have a file and you want to copy that file to the remote instance you have created with your Terraform code.
In this case, we can use the file provisioner and yes we will need the SSH conectivity for this provisioner just like remote-exec and also if you want to connect to an instance, then you need a key-pair too.
Use cases:
- Copy configuration files
- Deploy scripts for execution
- Transfer SSL certificates
- Upload application binaries
Syntax:
provisioner "file" {
source = "scripts/setup.sh"
destination = "/tmp/setup.sh"
}
provisioner "remote-exec" {
inline = [
"chmod +x /tmp/setup.sh",
"/tmp/setup.sh"
]
}
In the above block, you could see that we have used file provisioner to copy file from source to destination and then used remote-exec provisioner to setup required permissions for that file and execute that.
Connection Block:
For remote-exec and file provisioners, you need a connection block:
connection {
type = "ssh" # or "winrm" for Windows
user = "ubuntu" # SSH user
private_key = file("~/.ssh/id_rsa") # SSH private key
host = self.public_ip # Target host
timeout = "5m" # Connection timeout
}
Best Practices:
DO:
- Use provisioners as a last resort
- Prefer cloud-init, user_data, or AMI baking (Packer)
- Keep provisioner scripts idempotent
- Handle errors gracefully with on_failure parameter
- Use connection timeouts to avoid hanging
- Test thoroughly in non-production environments
DON'T:
- Use provisioners when native Terraform resources exist
- Rely on provisioners for critical configuration
- Forget that provisioners only run on creation
- Store sensitive data in provisioner commands
- Use complex logic - move to proper config management tools
Code Execution:
main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical (Ubuntu official)
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"
}
}
In the above code, we have created a basic terraform code for creation of an Ec2 instance using AMI data source and with a security group allowing ports 22 and outbound traffic as all.
Local-exec:
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"
}
provisioner "local-exec" {
command = "echo 'Local-exec: created instance ${self.id} with IP ${self.public_ip}'"
}
}
We have initiated a local-exec command which will execute a simple echo command with instance_id and instance_public_ip of the created EC2 instance.
remote-exec:
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"
}
provisioner "remote-exec" {
inline = [
"sudo apt-get update",
"echo 'Hello from remote-exec' | sudo tee /tmp/remote_exec.txt",
]
}
}
We have added a remote-exec provisoner of updating the packages in the remote machine of aws_instance.demo server and then echoing a simple line to the file "/tmp/remote_exec.txt".
After the resource is created, you can ssh into that instance using the key-pair and can check whether the file has been created in that location or not.
file provisoner:
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"
}
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"
]
}
}
Here we are using both remote-exec and file provisioners. The above script copies a script (scripts/welcome.sh) to the instance, then executes it. Good pattern for more complex bootstrapping when script files are preferred.
After execution of that script, you can ssh into that instance and check whether everything is working fine as per mentioned in the provisoners.
Also while doing hands-on exercises for the provisioners, make sure dont always follow terraform destroy followed by terraform apply which will be unnecessary and a waste of time.
Taint:
Instead use taint, when you mark a resource as taint, then Terraform will re-create that instance on the next terraform apply.
so just taint the aws_instance.demo and then do the terraform apply so that instance will be recreated with local-exec, remote-exec and file provisoners
terraform taint aws_instance.demo
Conclusion:
This marks the Day 19 of 30 days of AWS Terraform challenge. We have done a deep dive into the Terraform provisoners, what exactly are provisioners, different types of provisioners and what are the use cases for each of them and when we will use a specific provisioner.

Top comments (0)