Skip to main content

Command Palette

Search for a command to run...

Terraform Provisioners: A Last Resort, Not a First Choice

Published
4 min read
Terraform Provisioners: A Last Resort, Not a First Choice

As part of my ongoing Terraform learning journey, today I explored a topic that often confuses beginners and is frequently misused in real projects — Terraform Provisioners.

At first glance, provisioners look like a convenient way to “configure” resources after creation. But once I understood how and when they run, it became clear why Terraform itself advises us to be very careful with them.

This learning day was less about writing code and more about understanding Terraform philosophy and best practices.

What Are Terraform Provisioners?

Terraform is built to manage infrastructure declaratively — you describe the desired state, and Terraform makes it happen. Provisioners are different.

They allow Terraform to run commands or scripts at specific points in a resource’s lifecycle, mainly:

  • when a resource is created

  • or when it is destroyed

Because provisioners introduce imperative behavior into a declarative tool, Terraform clearly labels them as a last-option mechanism, not a default solution.

Why Provisioners Are Considered a “Last Resort”

Terraform is designed to manage infrastructure state, not configuration steps.

Provisioners:

  • Are imperative (command-based)

  • Are not tracked well in Terraform state

  • Can fail silently or partially

  • Do not rerun automatically on every change

Because of this, HashiCorp clearly states:

Use provisioners only when no better alternative exists.

Better alternatives usually include:

  • user_data / cloud-init

  • Configuration management tools (Ansible, Chef, Puppet)

  • Immutable images (AMI baking with Packer).

Types of Provisioners I Worked With

Terraform supports multiple provisioners, but the most commonly used ones fall into three categories.


1. local-exec – Runs on Your Machine

The local-exec provisioner runs where Terraform itself is executed, not on the cloud resource.

When it makes sense

  • Calling APIs or webhooks

  • Writing values to local files

  • Sending notifications

  • Updating external systems

Example

provisioner "local-exec" {
   command = "echo 'Local-exec: created instance ${self.id} with IP ${self.public_ip}'"
}

This does not run on the EC2 instance.
It runs locally where Terraform is executed.

Key takeaway

This provisioner never touches the EC2 instance or VM. It’s purely local.


2. remote-exec – Runs on the Resource

The remote-exec provisioner connects to the created resource and executes commands directly on it.

Typical uses

  • Installing basic packages

  • Running simple setup commands

  • Starting services like Nginx

Example

provisioner "remote-exec" {
  inline = [
    "sudo apt-get update",
    "sudo apt-get install -y nginx",
    "sudo systemctl start nginx"
  ]
}

This is useful for quick demos or labs, but risky for production.

Important limitation

It requires a working SSH or WinRM connection, and failures can easily break your Terraform run.


3. file – Copies Files to the Resource

The file provisioner is used to transfer files from your local system to the remote machine.

Common scenarios

  • Uploading scripts before execution

  • Copying configuration files

  • Moving certificates or binaries

Example

provisioner "file" {
     source      = "${path.module}/main.tf"
     destination = "/tmp/welcome.tf"
   }

It is usually paired with remote-exec to run the copied files.

Connection Block (Very Important)

For remote-exec and file provisioners, a connection block is mandatory.

 connection {
    type        = "ssh"
    user        = "ubuntu"
    private_key = file("${path.module}/yourpemfile.pem")
    host        = self.public_ip
  }

Without this, Terraform won’t know how to connect to the resource.

When Do Provisioners Actually Run?

One of the most important lessons from today:

Provisioners run only during resource creation.

They do not run:

  • during terraform plan

  • during normal updates

  • when provisioner code changes

  • on every terraform apply

Options to re-run:

terraform taint aws_instance.demo

or

terraform apply -replace=aws_instance.demo

Failure Behavior

By default, if a provisioner fails:

  • Resource creation is considered failed

  • Resource is marked as tainted

  • Next apply will destroy and recreate it

You can override this behavior:

provisioner "remote-exec" {
  inline = ["some-command"]
  on_failure = continue
}

Destroy-Time Provisioners

Provisioners can also run during resource destruction.

provisioner "local-exec" {
  when    = destroy
  command = "echo 'Cleaning up ${self.id}'"
}

Useful for cleanup tasks, logging, or deregistration.

Full Working Example (What I Implemented)

Here’s the complete Terraform code I used to understand provisioners practically.

resource "aws_instance" "example" {
  ami           = data.aws_ami.example.id
  instance_type = "t2.micro"
  key_name      = "your-key-name"
  vpc_security_group_ids = [aws_security_group.example.id]
  tags = {
    Name = "HelloWorld"
  }
  connection {
    type        = "ssh"
    user        = "ubuntu"
    private_key = file("${path.module}/yourpemfile.pem")
    host        = self.public_ip
  }
  provisioner "remote-exec" {
    inline = [
      "sudo apt-get update",
      "sudo apt install nginx -y",
      "sudo systemctl start nginx"
    ]
  }
}

resource "aws_security_group" "example" {
  name = "sg"
  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"]
  }
}

Once the instance was created, Nginx was automatically installed and started using remote-exec.

📺 Video That Helped Me Understand this concept:

Final Thoughts

Today’s learning changed how I look at Terraform.

Provisioners are not “bad,” but they are not Terraform’s strength. They exist for edge cases — not as a replacement for proper configuration management or automation tools.

Understanding provisioners helped me better understand Terraform’s design mindset, which is just as important as writing code.