What are Provisioners in Terraform? | How They Work (2025)

Blog Featured image for a blog with a title - What are Provisioners in Terraform

What are Provisioners in Terraform? | How They Work (2025)

Blog Featured image for a blog with a title - What are Provisioners in Terraform
Categories

Introduction

Provisioners in Terraform help execute scripts or commands on local or remote machines during resource creation or destruction. They are normally utilized to do things such as setting up of servers, executing shell scripts, or starting up any application on the deployment of infrastructure. While provisioners in Terraform can automate post-deployment actions, HashiCorp recommends using them sparingly, favoring configuration management tools like Ansible or cloud-init for ongoing setups. Provisioners are a final intervention when native Terraform providers or resources are unable to reach particular configurations. In this blog, we will discuss what terraform provisioners in detail, along with its types and functioning. We will also explain when to use Terraform provisioner.

Let us first understand what provisioners in Terraform are.

What are Provisioners in Terraform?

Provisioners in Terraform are tools used to execute scripts or commands on resources during creation or destruction. They assist in setting up servers, software installation or post-installation actions that are not supported by native Terraform resources. Provisioners in Terraform support local and remote execution, enabling automation beyond standard infrastructure provisioning. Although they make a viable way to customize system configurations, they are regarded as the last resort, as configuration management solutions, such as Ansible or cloud-init, can be more reliable. Use provisioners in Terraform carefully to avoid dependency or state issues.

Now that you have a good understanding of terraform provisioners, let us move to the next section where we will understand how you can use it.

How to use Terraform provisioners?

Terraform provisioners allow you to execute actions such as copying or running a script with a local or remote machine once a resource has been created or when a resource is about to be destroyed. To invoke one, you simply state a so-called `provisioner` block within a resource.

The main types are:

  • file: Copies files or directories of the machine to which Terraform is being run to the newly created resource.
  • local-exec: Executes a command on the host running the Terraform process.
  • remote-exec: Executes a script on the remote resource after the resource is created.

Note: Provisioners need to be a last resort. When virtual machines are to be configured, there is practically no better option than setting up cloud-init scripts (through `user_data`), as well as configuration management programs designed specifically to carry this out, i.e., dedicated configuration management tools, such as Ansible.

Let us now look at some of the examples to better understand the functioning of Terraform Provisioners

Example 1: Running a Local Command with null_resource

Let’s have a look at it

resource "null_resource" "example" {
  provisioner "local-exec" {
    command = "echo Hello World!"
  }
}
# null_resource.example: Creating...
# null_resource.example: Provisioning with 'local-exec'...
# null_resource.example (local-exec): Executing: ["/bin/sh" "-c" "echo Hello World!"]
# null_resource.example (local-exec): Hello World!
# null_resource.example: Creation complete after 0s [id=someid]

This will only run a shell command that outputs Hello World.

Example 2: Copying a File to an AWS EC2 Instance

Let’s look into a real-life example. This one, we will copy a YAML file in our local system to an AWS instance via the file provisioner within a null resource. The AWS instance does not exist hence we will also script it.

provider "aws" {
  region = "us-east-1"
}

data "aws_ami" "ubuntu" {
  filter {
    name   = "name"
    values = ["ubuntu-*"]
  }
  most_recent = true
}

resource "aws_key_pair" "example" {
  key_name   = "key"
  public_key = file("~/.ssh/id_rsa.pub")
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = local.instance_type
  key_name      = aws_key_pair.example.key_name
}

resource "null_resource" "copy_file_on_vm" {
  depends_on = [
    aws_instance.web
  ]
  connection {
    type        = "ssh"
    user        = "ubuntu"
    private_key = file("~/.ssh/id_rsa")
    host        = aws_instance.web.public_dns
  }
  provisioner "file" {
    source      = "./file.yaml"
    destination = "./file.yaml"
  }
}

# null_resource.copy_file_on_vm: Creating...
# null_resource.copy_file_on_vm: Provisioning with 'file'...
# null_resource.copy_file_on_vm: Creation complete after 2s [id=someid]

As you can see, in the output of the null resource, the file provisioner finished its operation successfully. You can now log in to the vm and see the file copied in the specified directory.

Below, we have discussed the most asked question i.e., when to use terraform provisioners.

When to run the provisioners?

Provisioners in Terraform can be configured to run at specific stages of a resource’s lifecycle, such as during creation or destruction. The default is to run once a resource is successfully created, but you may leave them to run only with the Creation, and with each apply, or pre-destroy a resource.

Provisioners in Terraform are ideal for tasks like bootstrapping servers or cleaning up resources. Nevertheless, they must be minimally utilized, since they may induce dependencies and facilitate less predictable infrastructure.

Creation-time provisioners

By default, all provisioners are creation-time provisioners. This implies that as you are provisioning the resources linked with the provisioners, the provisioners will run themselves and perform their duties. You don’t need to specify ‘when’ option to apply creation-time provisioners. However, to run a provisioner when a resource is destroyed, you must use when = destroy argument.

resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"

  # This provisioner runs ONCE during resource creation by default.
  provisioner "local-exec" {
    command = "echo ${self.public_ip} > ip_address.txt"
  }
}

Destroy-time provisioners

In the example below, we create separate text files that contain event-specific messages for both create and destroy events.

resource "aws_instance" "my_vm" {
 ami           = var.ami //Amazon Linux AMI
 instance_type = var.instance_type
 
 provisioner "local-exec" {
   command = "echo 'Creation is successful.' >> creation.txt"
 }
 
 provisioner "local-exec" {
   when = destroy
   command = "echo 'Destruction is successful.' >> destruction.txt"
 }
 
 tags = {
   Name = var.name_tag,
 }
}

This should generate two text files in respective order of operations – creation.txt and destruction.txt – in the project directory.

Prerequisites for the Connection Block

Prior to going on to other sections, we need to mention the connection block. The file provisioner and remote-exec provisioners – they both work with the target resource being made in the future.

In order to make Terraform to be able to SSH into our Linux-powered EC2 instance, we only require two things:

  • AWS key pair
  • Security group to open up the HTTP access

Access the AWS console, and manually generate a key pair and save the file containing the private key locally – to the host where Terraform is installed. Generate the key pair and name it tfsn. The private key file downloaded to your machine will be named tfsn.cer.

Terraform provisioners SSH to the EC2 with this information. We will also SSH into the EC2 instance ourselves using this key pair to validate all that we did.

The Terraform configuration adds the configuration of a newly created security group that allows the HTTP protocol of internet traffic to reach it using a browser, and SSH access is required by the provisioners. This we would require to validate, under which we argue about the remote-exec provisioner.

Below is an example configuration of the security group in Terraform:

resource "aws_security_group" "http_access" {
 name        = "http_access"
 description = "Allow HTTP inbound traffic"
 
 ingress {
   description = "HTTP Access"
   from_port   = 80
   to_port     = 80
   protocol    = "tcp"
   cidr_blocks = ["0.0.0.0/0"]
 }
 
 ingress {
   description = "SSH Access"
   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"]
 }
 
 tags = {
   Name = "http_access"
 }
}

Terraform file provisioner

A file provisioner is a mechanism of copying some files or artifacts from the host machine to target resources that will be created in the future. It is a quite convenient method of delivering some script files, configuration files, artifacts such as .jar files, binaries, etc., to the target resource at the time when the latter was created and booted up for the first time.

For it to work, you have to give Terraform the server’s address. You do this in a connection block.

The Plan:

  • We will build a small server.
  • We will copy a script file to it.

First, create a simple script file on your computer. Call it my-script.sh.

#!/bin/bash 
echo "Hello from the server!"

Now, here is the Terraform code.

# This part builds a small server on AWS.
resource "aws_instance" "app_server" {
  ami           = "ami-0c55b159cbfafe1f0" # An Amazon Linux 2 AMI
  instance_type = "t2.micro"

  # This part tells Terraform how to log in.
  connection {
    type        = "ssh"
    user        = "ec2-user"
    private_key = file("~/.ssh/id_rsa") # Assumes your key is here
    host        = self.public_ip
  }

  # And this part copies your file.
  provisioner "file" {
    source      = "my-script.sh"             # The file on your computer
    destination = "/home/ec2-user/my-script.sh" # Where it goes on the new server
  }

  tags = {
    Name = "SimpleServer"
  }
}

When you run this code, it’s simple. A new server gets made. Your file, my-script.sh, shows up in the user’s home folder on that server.

Terraform local-exec provisioner

The local-exec provisioner runs within the Terraform host- where we apply/run the Terraform configuration. It is used to execute any shell command. It may be used to set or read environment variables, details about the resource created, call any process or application, etc.

The local-exec provisioner executes scripts on the same machine that is running Terraform. This is perfect for running local scripts, whether they are part of your project or already exist on your computer.

Example: Saving a Server’s IP to a File

This example creates a small AWS server. After the server is ready, the local-exec provisioner runs a command on your computer to save the new server’s public IP address into a file named ip_address.txt.

# Creates a small server on AWS
resource "aws_instance" "web_server" {
  ami           = "ami-0c55b159cbfafe1f0" # An Amazon Linux 2 AMI
  instance_type = "t2.micro"

  # This block runs a command on your local machine
  provisioner "local-exec" {
    command = "echo ${self.public_ip} > ip_address.txt"
  }

  tags = {
    Name = "LocalExecExample"
  }
}

Terraform remote-exec provisioner

Remote-exec provisioners are more or less the same as local-exec provisioners, but instead the commands are run on target-EC2 instance, not on Terraform host. This is achieved through the same connection block used by the file delivered. We apply remote-exec provisioner to execute one or more instructions.

This example creates a small AWS server. After the server is ready, the local-exec provisioner runs a command on your computer to save the new server’s public IP address into a file named ip_address.txt

provider "aws" {
  region = "us-east-1"
}

# Find the latest Ubuntu AMI
data "aws_ami" "ubuntu" {
  most_recent = true
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }
  owners = ["099720109477"] # Canonical's AWS account ID
}

# Create an AWS key pair from your local public key
resource "aws_key_pair" "deployer" {
  key_name   = "my-app-key"
  public_key = file("~/.ssh/id_rsa.pub")
}

# Create the EC2 instance
resource "aws_instance" "web_server" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"
  key_name      = aws_key_pair.deployer.key_name

  # This block tells Terraform how to connect to the instance via SSH
  connection {
    type        = "ssh"
    user        = "ubuntu"
    private_key = file("~/.ssh/id_rsa")
    host        = self.public_ip
  }

  # This block runs commands on the remote server after it's created
  provisioner "remote-exec" {
    inline = [
      "sudo apt-get update",
      "sudo apt-get install -y nginx",
      "sudo systemctl start nginx"
    ]
  }

  tags = {
    Name = "RemoteExecExample"
  }
}

Why Provisioners in Terraform Should be a Last Resort?

Provisioners are great. However, there are some limitations that we need to take into account prior to using them. When making any form of configuration management by means of provisioners, the provisioners should only be used as a last resort in gaining any form of configuration management tasks that are achievable by the provisioners.

Enabling us to do anything later on in the future, target resource is a lot of power and responsibility on the part of provisioners. It provides massive space on what can be carried out on the OS level and the application level. There is no tracking or accountability for these actions.

When the provisioner tasks become faulty and otherwise in executable form on a few machines, finding an alternative and implementing the alternative would multiply overhead in locating those machines and implementing the workaround as well.

Understanding why a certain provisioner failed to operate on a specific set of machines can be extremely challenging since a number of aspects could be unique to each resource.

Frequently Asked Questions

Q1. What does Provisioners Do?

In Terraform, you can use a Provisioner to help automate your trigger-down tasks, such as running scripts, transferring some files, or installing and configuring some software on a machine/device as part of the resource creation or destruction process, therefore enabling Terraform to go beyond infrastructure as code.

Q2. What is the difference between provisioner local-exec and remote exec?

A Terraform Provisioner local-exec runs commands on the machine executing Terraform. For comparison, a Terraform Provisioner with remote-exec will execute a command directly on the target resource, which could be a remote server or instance.

Q3. What is remote provisioner use for?

To execute commands or scripts on the remote resources, including servers or virtual machines, it is possible to use a Terraform Provisioner with remote-exec that allows using tasks like software installation, configuration, and bootstrapping of an infrastructure after creating it.

Q4. What is a Provisioner Node?

A Provisioner in Terraform node is a resource placeholder that triggers tasks like running scripts or transferring files, helping execute configurations during infrastructure creation or destruction when native resources lack functionality.

Conclusion

Provisioners in Terraform let you run scripts, copy files, and automate your configuration when creating or deleting a resource. They enable us to have flexibility with logic, which could not be done with Terraform native resources, such as bootstrapping a server or acting after the deployment.

But Terraform Provisioners need to be avoided when there are no other options, as there can be state dependencies, unpredictability and complexity in troubleshooting. Either Ansible, Chef, or Puppet are used to control infrastructure in a repeatable and scalable manner. You can enroll in an ansible and terraform course that covers these best practices in depth, teaching how to effectively combine these tools with Terraform rather than overusing provisioners. This ensures cleaner, more maintainable infrastructure automation.

Leveraging a provisioner in Terraform should be restricted and effectively as a last resort, when no native provider, or desired configuration management solution, can meet your automation or customization requirements.

Get in touch

Blog
Looking For Networking Training- PyNetLabs

Popular Courses

Automation

(FRESHERS / EXPERIENCED)

Network Expert

Automation

(FRESHERS / EXPERIENCED)

Network Expert

Automation

(FRESHERS / EXPERIENCED)

Network Expert

Automation

(FRESHERS / EXPERIENCED)

Network Expert

Automation

(FRESHERS / EXPERIENCED)

Network Expert

Leave a Reply

Your email address will not be published. Required fields are marked *