AWS with Terraform (Day 19)

Terraform Provisioners Explained: local-exec, remote-exec, and file

Provisioners in Terraform are often misunderstood. Many beginners either overuse them or avoid them entirely without understanding where they fit. In this post, I’ll explain what Terraform provisioners actually do, when they make sense, and how local-exec, remote-exec, and file provisioners behave in real-world scenarios, based on a hands-on EC2 demonstration.

This was part of my Day 19 learning, focused on understanding the imperative escape hatch Terraform provides when declarative resources alone are not enough.


What are Terraform Provisioners?

A provisioner allows Terraform to execute imperative actions during a resource’s lifecycle, usually at creation time or destruction time.

Provisioners are commonly used for:

  • Bootstrapping instances

  • Copying files to servers

  • Running scripts or commands that Terraform resources cannot express directly

Terraform documentation clearly states that provisioners should be used sparingly. Whenever possible, prefer:

  • Cloud-init / user data

  • Native Terraform resources

  • Immutable images (AMIs)

  • Configuration management tools

Provisioners are best treated as a last-mile tool, not a primary design pattern.


Provisioner Types in Terraform

Terraform supports three commonly used provisioners:

  • local-exec

  • remote-exec

  • file

Each one serves a different purpose.


local-exec Provisioner

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

When to use local-exec

  • Generating files locally

  • Calling external CLI tools

  • Logging or notifying systems during apply

  • Triggering scripts in CI/CD pipelines

Key point

local-exec does not require SSH, keys, or network access to the resource. It runs entirely on your local system or CI runner.

Because it runs locally, it is not suitable for server bootstrapping.


remote-exec Provisioner

The remote-exec provisioner runs commands on the remote resource itself, typically over SSH.

Common use cases

  • Installing packages

  • Creating files or directories

  • Running setup commands after instance creation

Requirements

To use remote-exec, you must define a connection block:

  • SSH user

  • Private key

  • Host (usually the instance public IP)

You must also ensure:

  • Security groups allow SSH

  • Private key permissions are correct

  • The instance is reachable when Terraform runs

Because remote-exec depends on SSH connectivity, it is more fragile than cloud-init and should be used carefully.


file Provisioner

The file provisioner copies files from the local machine to the remote instance over SSH.

Typical usage

  • Uploading scripts

  • Copying configuration files

  • Transferring certificates or assets

The file provisioner does not execute anything. It only copies files. In most cases, it is paired with a remote-exec provisioner to run the uploaded script.


Using the self Object

Inside a resource block, Terraform exposes a special object called self.

Useful attributes include:

  • self.id

  • self.public_ip

  • self.private_ip

These attributes are commonly used in provisioners and connection blocks to dynamically reference the resource being created.


Forcing Provisioners to Re-run

Terraform provisioners run only when a resource is created or recreated.

If the resource has no changes, provisioners will not execute again.

To force a re-run during testing:

terraform taint aws_instance.example terraform apply

This marks the resource for recreation, triggering all provisioners again.


Practical Observations

From hands-on testing, a few important lessons stand out:

  • local-exec is excellent for automation around Terraform, not inside servers

  • remote-exec and file depend heavily on network stability and SSH correctness

  • Provisioner failures can taint resources and cause unexpected recreations

  • Sensitive data should never be hardcoded inside provisioner commands

  • Provisioners increase coupling between infrastructure and configuration


Best Practices

  • Prefer cloud-init or user data for instance bootstrapping

  • Use provisioners only when declarative resources are insufficient

  • Keep provisioner logic simple and idempotent

  • Avoid long-running or complex scripts inside provisioners

  • Always destroy test resources to avoid unnecessary costs


Diagram




Final Thoughts

Terraform provisioners are powerful, but they are not the default solution. They exist to bridge the gap between declarative infrastructure and imperative configuration when no better option is available.

Here is the session link:



Comments

Popular posts from this blog

AWS with Terraform (Day 01)

AWS with Terraform (Day 27)

AWS with Terraform (Day 02)