In today’s blog post, we will learn what are terraform provisioners, why we need provisioners, explore provisioners types with examples, provisioners meta arguments, and finally its use cases. So without any further delay, let us get started.
What are Terraform Provisioners?
Terraform Provisioners are blocks of code defined within a resource block (or a null_resource
block) that execute some actions on a local or remote machine after a resource has been created or before it is destroyed. They allow you to perform bootstrapping, configuration management, or cleanup tasks directly from your Terraform configuration.
Think of them as hooks: they let you run scripts or commands at specific points in the resource lifecycle.
Why Use Provisioners (and their limitations)?
Pros:
- Initial Setup: Perform basic setup on a new server (e.g., install a web server, update packages).
- Bootstrapping: Inject configuration or run a script that kicks off further automation (e.g., registering with a configuration management system).
- Simplicity for Small Tasks: For very simple, one-off configuration tasks, they can be quicker than setting up a full-blown configuration management tool.
- Cleanup: Execute commands before a resource is fully destroyed.
Cons:
- Lack of Idempotency: Provisioners run arbitrary commands. It is your responsibility to ensure these commands are idempotent (running them multiple times yields the same result) – something configuration management tools handle inherently.
- State Management Issues: If a provisioner fails, Terraform might mark the resource as “tainted,” requiring manual intervention. They do not easily integrate into Terraform’s declarative state.
- Limited Error Handling: While there is an
on_failure
option, complex error handling is difficult. - Reduced Visibility: The configuration executed by provisioners is outside Terraform’s declarative state, making it harder to inspect the final state of the configured system.
- Security Concerns: Remote execution requires credentials (SSH keys, WinRM credentials) to be managed, potentially introducing security risks if not handled carefully.
- Single-Run Nature: Provisioners run only when a resource is created or tainted. They would not re-run (exception –
always_run
) if you change an input variable for the provisioner itself (unless the resource is destroyed and recreated, or tainted).
General Rule of Thumb: Use provisioners as a last resort for tasks that can not be handled by declarative resource configuration or a dedicated configuration management tool.
Types of Terraform Provisioners
Terraform offers several built-in provisioners:
file
: Copies files from the local machine to the remote resource.local-exec
: Executes a command on the machine running Terraform.remote-exec
: Executes a command on the remote resource created by Terraform.null_resource
(with provisioners): A special resource type used when you want to run provisioners independent of other resources.
The connection Block
For file
and remote-exec
provisioners, you almost always need a connection
block to specify how Terraform should connect to the remote machine.
connection {
type = "ssh" # or "winrm"
user = "ec2-user" # Username for SSH
private_key = file("~/.ssh/id_rsa") # Path to your private key
host = self.public_ip # The IP address of the instance
timeout = "5m" # Optional: How long to wait for connection
}
Common connection
arguments:
type
:ssh
(Linux) orwinrm
(Windows).user
: Username for the connection.password
: (Avoid if possible, use private keys/secrets management).private_key
: Path to the SSH private key file.host
: The IP address or DNS name of the remote machine.timeout
: How long to wait for the connection to establish before timing out.port
: The port for the connection (default: 22 for SSH, 5985/5986 for WinRM).
Terraform Provisioner Examples:
file Provisioner: Copying Files
The file
provisioner copies files or directories from the machine running Terraform to a remote resource.
resource "aws_instance" "web_server" {
ami = "ami-0abcdef1234567890" # Replace with a valid AMI
instance_type = "t2.micro"
key_name = "my-ssh-key" # Ensure this key exists
provisioner "file" {
source = "scripts/install_nginx.sh" # Path to local script
destination = "/tmp/install_nginx.sh" # Path on remote machine
connection {
type = "ssh"
user = "ec2-user"
private_key = file("~/.ssh/id_rsa")
host = self.public_ip
}
}
tags = {
Name = "Web Server"
}
}
Explanation:
source
: The path to the file or directory on your local machine.destination
: The absolute path on the remote machine where the file/directory should be copied.
remote-exec Provisioner: Running Commands on Remote Machines
The remote-exec
provisioner executes scripts or commands on the remote resource. This is often used after a file
provisioner has copied a script.
resource "aws_instance" "web_server" {
ami = "ami-0abcdef1234567890"
instance_type = "t2.micro"
key_name = "my-ssh-key"
provisioner "file" {
source = "scripts/install_nginx.sh"
destination = "/tmp/install_nginx.sh"
connection { /* ... (same as above) ... */ }
}
provisioner "remote-exec" {
inline = [
"chmod +x /tmp/install_nginx.sh", # Make script executable
"/tmp/install_nginx.sh" # Execute the script
]
connection { /* ... (same as above) ... */ }
}
tags = {
Name = "Web Server"
}
}
Explanation:
inline
: A list of strings, where each string is a command to be executed.script
: (Alternative toinline
) Path to a local script that will be copied to the remote machine and executed. This is useful for longer scripts.
provisioner "remote-exec" {
script = "scripts/configure_webserver.sh" # This script will be copied and executed
connection { /* ... */ }
}
local-exec Provisioner: Running Commands Locally
The local-exec
provisioner executes a command on the machine where terraform apply
is run, not on the created resource.
resource "aws_s3_bucket" "my_app_bucket" {
bucket = "my-unique-app-bucket-12345"
acl = "private"
}
resource "null_resource" "post_bucket_creation" {
# This resource has no infrastructure backing, only provisioners.
# It depends on the S3 bucket to ensure it runs after the bucket is created.
depends_on = [aws_s3_bucket.my_app_bucket]
provisioner "local-exec" {
command = "echo 'S3 bucket ${aws_s3_bucket.my_app_bucket.bucket} created successfully!' >> terraform_logs.txt"
}
provisioner "local-exec" {
# Example: Triggering a notification or another local script
command = "curl -X POST -H 'Content-type: application/json' --data '{\"text\":\"S3 bucket ${aws_s3_bucket.my_app_bucket.bucket} provisioned!\"}' ${var.slack_webhook_url}"
}
}
Explanation:
command
: The command to execute on the local machine.working_directory
: (Optional) The directory from which to run the command.environment
: (Optional) A map of environment variables to set for the command.
Common local-exec
use cases:
- Triggering external scripts or CI/CD pipelines.
- Generating configuration files locally.
- Sending notifications (as shown above).
- Running local tests or validations.
null_resource: Running Provisioners Independently
The null_resource
is a special resource in Terraform that does not map to any cloud or infrastructure object. Its sole purpose is to serve as a container for provisioners. It is very useful when you need to run commands that are logically tied to your Terraform deployment but do not configure a specific infrastructure resource directly.
You often combine null_resource
with the triggers
argument to make it sensitive to changes in other resources or variables, forcing its provisioners to re-run.
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "MyVPC"
}
}
resource "null_resource" "vpc_config_script" {
# This makes the null_resource re-run its provisioners if the VPC ID changes
triggers = {
vpc_id = aws_vpc.main.id
}
provisioner "local-exec" {
command = "echo 'VPC with ID ${self.triggers.vpc_id} created.' >> vpc_creation_log.txt"
}
provisioner "local-exec" {
# Imagine configuring a Firewall rule that depends on the VPC ID externally
command = "python external_firewall_config.py --vpc-id ${self.triggers.vpc_id}"
}
}
Explanation:
triggers
: A map of arbitrary values. If any value in thetriggers
map changes betweenterraform apply
runs, thenull_resource
is “tainted” and its provisioners will re-run. This is how you force re-execution of provisioners tied to thenull_resource
.self.triggers.vpc_id
: This accesses the value from thetriggers
map.
Provisioner Lifecycle and Error Handling
Provisioners run in a specific order and have options for error handling:
- Default Behavior: By default, provisioners run after a resource is created.
on_failure
: Controls what happens if a provisioner fails.continue
(default): Terraform proceeds with theapply
even if the provisioner fails.fail
: Terraform stops theapply
and marks the resource as “tainted,” indicating it is in an unknown state and should likely be destroyed and recreated.
when
: Controls when the provisioner runs.create
(default): Runs after resource creation.destroy
: Runs before a resource is destroyed. (Requiresconnection
for remote resources).
resource "aws_instance" "app_server" {
# ... instance configuration ...
provisioner "remote-exec" {
inline = ["sudo apt-get update && sudo apt-get install -y nginx"]
connection { /* ... */ }
on_failure = fail # If Nginx install fails, taint the instance
}
provisioner "remote-exec" {
when = destroy # This provisioner runs when the instance is being destroyed
inline = ["sudo systemctl stop nginx && sudo rm -rf /var/www/html/*"]
connection { /* ... */ }
}
}
When NOT to Use Terraform Provisioners
As mentioned, provisioners should be used with caution. Here are scenarios where you should strongly consider alternatives:
- Complex Configuration Management: For tasks like managing user accounts, installing multiple software packages, maintaining service configurations, or ensuring continuous desired state, use a dedicated Configuration Management (CM) tool like Ansible, Chef, Puppet, or SaltStack. These tools are designed for idempotency, error handling, and reporting.
- Application Deployment: For deploying application code, use specialized Deployment Tools like Jenkins, GitLab CI/CD, Spinnaker, or specific cloud deployment services (e.g., AWS CodeDeploy, Azure DevOps).
- Initial Server Bootstrapping: For initial setup, Cloud-Init (Linux) or User Data (AWS/Azure) is often a better choice. It is natively supported by cloud providers, runs at boot time, and keeps your Terraform focused purely on infrastructure.
- Long-Lived Configuration: If your server needs ongoing configuration updates or drifts from its desired state, a CM tool is necessary. Provisioners are “fire and forget.” High Security Requirements: Passing sensitive credentials directly in Terraform files or via provisioners can be risky. Integrations with secret managers are usually more secure.
Terraform Provisioners vs. Ansible / User Data / Providers
- Provisioners vs. Ansible (or other CM tools):
- Provisioners: Good for simple, one-off post-provisioning tasks, generally not idempotent by default. Limited state management.
- Ansible: Excellent for ongoing configuration management, ensuring idempotency, managing complex dependencies, and offering robust error handling. Designed for server configuration, not infrastructure provisioning. Ideally, Terraform provisions the server, then Ansible configures it.
- Provisioners vs. User Data / Cloud-Init:
- Provisioners: Run by Terraform after resource creation. More flexible in terms of connection types (SSH/WinRM).
- User Data/Cloud-Init: Native cloud provider feature. Runs during the instance boot process. Simpler for basic bootstrapping. Often preferred for first-boot configurations.
- Provisioner vs. Provider:
- Provider: The core of Terraform. Defines and manages the lifecycle of infrastructure resources (e.g.,
aws_instance
,aws_vpc
). It is about what infrastructure exists. - Provisioner: A feature of a resource. It is about how to perform actions on or related to that infrastructure after it is created.
- Provider: The core of Terraform. Defines and manages the lifecycle of infrastructure resources (e.g.,
Conclusion
Terraform provisioners feature, when used appropriately, can bridge the gap between infrastructure provisioning and initial configuration. However, they are not a substitute for dedicated configuration management or deployment tools.
Mastering provisioners means understanding their capabilities and their limitations. Use them wisely for bootstrapping, cleanup, or simple local tasks, and always consider more robust alternatives for complex, long-lived, or highly critical configuration management.
Author

Experienced Cloud & DevOps Engineer with hands-on experience in AWS, GCP, Terraform, Ansible, ELK, Docker, Git, GitLab, Python, PowerShell, Shell, and theoretical knowledge on Azure, Kubernetes & Jenkins. In my free time, I write blogs on ckdbtech.com