DEV Community

Cover image for ->> Day-19 Terraform Provisioners in Action: A Hands-On Demo (local-exec, remote-exec & file)
Amit Kushwaha
Amit Kushwaha

Posted on

->> Day-19 Terraform Provisioners in Action: A Hands-On Demo (local-exec, remote-exec & file)

When learning Terraform, most people stop at creating infrastructure.

But what if you want to run scripts, install packages, or trigger actions after a resource is created?

That's where Terraform Provisioners come in.

In this blog, I will walk through:

  • What provisioner really are
  • When (and when not) to use them
  • A hands-on AWS EC2 demo using: - local-exec - remote-exec - file + remote-exec


What are Terraform Provisioners?

Provisioners let Terraform execute scripts or commands during resource creation or destruction.

They’re useful for tasks that Terraform can’t model declaratively, such as:

  • Installing software
  • Running bootstrap scripts
  • Registering resources in external systems
  • Sending notifications
  • Copying files to servers

⚠️ Terraform officially recommends using provisioners as a last resort.
Prefer user_data, cloud-init, Packer, or configuration management tools for serious production setups.


Types of Provisioners

1. local-exec

Runs commands on your local machine.

Use case:

  • Trigger webhooks
  • Call APIs
  • Write to local inventory files
  • Send Slack notifications
   provisioner "local-exec" {
   command = "echo 'Local-exec: created instance ${self.id} with IP ${self.public_ip}'"
   }
Enter fullscreen mode Exit fullscreen mode

2. remote-exec

Runs commands on the remote resource via SSH (Linux) or WinRM (Windows).

Use Cases:

  • Install packages (nginx, docker, node, etc.)
  • Configure OS settings
  • Start services
  • Quick bootstrap scripts
  provisioner "remote-exec" {
    inline = [
      "sudo apt-get update",
      "echo 'Hello from remote-exec' | sudo tee /tmp/remote_exec.txt",
    ]
  }
Enter fullscreen mode Exit fullscreen mode

3. file

Copies files from your machine to the remote resource.

Use Cases:

  • Copy setup scripts
  • Upload config files
  • Transfer certificates
  • Deploy small binaries
  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"
    ]
  }
Enter fullscreen mode Exit fullscreen mode

Architecture

Laptop / CI
     │
     ▼
Terraform
     │
     ▼
AWS EC2 (Ubuntu)
     │
     ├── local-exec   → runs locally
     ├── remote-exec  → runs via SSH on EC2
     └── file         → copies scripts to EC2
Enter fullscreen mode Exit fullscreen mode

-> Prerequisites

Before running the demo:

  • AWS credentials configured
  • Terraform v1.0+ installed
  • AWS CLI installed
  • SSH client installed
  • An EC2 key pair created
aws ec2 create-key-pair --key-name terraform-demo-key \
  --query 'KeyMaterial' --output text > terraform-demo-key.pem

chmod 400 terraform-demo-key.pem
Enter fullscreen mode Exit fullscreen mode

Main.tf

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"
    }
  }
}

# EC2 instance used for provisioner demos.
# Each provisioner block is included below but wrapped in block comments (/* ... */).
# For the demo, uncomment one provisioner block at a time, then `terraform apply`.

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"] # Canonical (Ubuntu official) - Current owner ID

  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"
  }

  connection {
    type        = "ssh"
    user        = var.ssh_user
    private_key = file(var.private_key_path)
    host        = self.public_ip
    timeout     = "5m"
  }

  /*
  ------------------------------------------------------------------
  Provisioner 1: local-exec
  - Runs on the machine where you run Terraform (your laptop/CI agent).
  - Useful for local tasks, logging, calling local scripts, etc.
  - To demo: uncomment this block, then run `terraform apply`.
  ------------------------------------------------------------------
  */

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


  /*
  ------------------------------------------------------------------
  Provisioner 2: remote-exec
  - Runs commands on the remote instance over SSH.
  - Requires SSH access (security group + key pair + reachable IP).
  - To demo: uncomment this block, ensure `var.private_key_path` is correct, then run `terraform apply`.
  ------------------------------------------------------------------
  */

  provisioner "remote-exec" {
    inline = [
      "sudo apt-get update",
      "echo 'Hello from remote-exec' | sudo tee /tmp/remote_exec.txt",
    ]
  }


  /*
  ------------------------------------------------------------------
  Provisioner 3: file + remote-exec
  - Copies a script (scripts/welcome.sh) to the instance, then executes it.
  - Good pattern for more complex bootstrapping when script files are preferred.
  - To demo: uncomment both the file provisioner and the remote-exec block below.
  ------------------------------------------------------------------
  */

  # 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"
  #   ]
  # }

}
Enter fullscreen mode Exit fullscreen mode

Demo 1: local-exec

provisioner "local-exec" {
  command = "echo 'Created ${self.id} with IP ${self.public_ip}'"
}
Enter fullscreen mode Exit fullscreen mode

What happens:

  • EC2 is created
  • Terraform prints a message on your computer
  • Nothing changes inside the EC2

Demo 2: remote-exec

provisioner "remote-exec" {
  inline = [
    "sudo apt-get update",
    "echo 'Hello from remote-exec' | sudo tee /tmp/remote_exec.txt"
  ]
}
Enter fullscreen mode Exit fullscreen mode

What happens:

  • Terraform waits for SSH
  • Connects to EC2
  • Runs the commands remotely
  • Creates /tmp/remote_exec.txt

Verify:

ssh -i terraform-demo-key.pem ubuntu@<PUBLIC_IP>

cat /tmp/remote_exec.txt
Enter fullscreen mode Exit fullscreen mode

Expected output:

Hello from remote-exec
Enter fullscreen mode Exit fullscreen mode

Demo 3: file + remote-exec

provisioner "file" {
  source      = "scripts/welcome.sh"
  destination = "/tmp/welcome.sh"
}

provisioner "remote-exec" {
  inline = [
    "sudo chmod +x /tmp/welcome.sh",
    "sudo /tmp/welcome.sh"
  ]
}
Enter fullscreen mode Exit fullscreen mode

welcome.sh

#!/bin/bash
echo "Welcome to the Provisioner Demo" | sudo tee /tmp/welcome_msg.txt
uname -a | sudo tee -a /tmp/welcome_msg.txt
Enter fullscreen mode Exit fullscreen mode

What happens:

  • Script is copied to EC2
  • Script is executed
  • Output file is created at /tmp/welcome_msg.txt

Resources

>> Connect With Me

If you enjoyed this post or want to follow my #30DaysOfAWSTerraformChallenge journey, feel free to connect with me here:

💼 LinkedIn: Amit Kushwaha

🐙 GitHub: Amit Kushwaha

📝 Hashnode / Amit Kushwaha

🐦 Twitter/X: Amit Kushwaha

Found this helpful? Drop a ❤️ and follow for more AWS and Terraform tutorials!

Questions? Drop them in the comments below! 👇

Top comments (0)