When I started learning Terraform, I wondered:
Terraform can create infrastructure… but how do we run scripts, install software, or copy files after a VM is created?
That is where Terraform Provisioners come into the picture.
In this hands-on mini project I implemented:
- Local-Exec Provisioner
- Remote-Exec Provisioner
- File Provisioner
and understood their real purpose, limitations, and practical usage.
Table of Contents
- Project Goal
- Architecture Overview
- Step 1 – Create Core Azure Infrastructure
- Step 2 – Create VM and Verify SSH
- Step 3 – Local-Exec Provisioner
- Step 4 – Remote-Exec Provisioner
- Debug Steps and Errors Faced
- Step 5 – File Provisioner
- Understanding Provisioners
- Important Reality
- Final Learning Outcome
Project Goal
Build an Azure Linux VM using Terraform and:
- Run a command on my local PC during deployment
- Install Nginx inside the VM automatically
- Copy a configuration file from my laptop to the VM
Architecture Overview
The infrastructure consists of:
- Resource Group
- Virtual Network and Subnet
- Network Security Group (SSH + HTTP)
- Public IP
- Network Interface
- Linux Virtual Machine
Step 1 – Create Core Azure Infrastructure
Resource Group
resource "azurerm_resource_group" "rg" {
name = "rgminipro878933"
location = "Central US"
}
Virtual Network & Subnet
resource "azurerm_virtual_network" "vnet" {
name = "vnetminipro7678678"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
}
Network Security Group
Inbound rules were added to allow:
- Port 22 → SSH
- Port 80 → HTTP
Step 2 – Create VM and Verify SSH
Generate SSH Keys
ssh-keygen -t rsa -b 4096
Create Linux VM
The VM was created using azurerm_linux_virtual_machine with SSH key authentication.
Test Connection
ssh -i key1 azureuser@<public-ip>
✔ SSH login successful.
Step 3 – Local-Exec Provisioner
What Local-Exec Means
Local-exec runs a command on:
The machine where Terraform is executed
NOT inside the Azure VM.
Implementation
provisioner "local-exec" {
command = "echo Deployment started at ${timestamp()} > deployment.log"
}
Result
A file deployment.log was created on my laptop — proof that the command executed locally.
Real-World Uses
- Trigger Ansible after Terraform
- Call REST API or webhook
- Notify Slack/Email
- Generate inventory files
- Write audit logs
Step 4 – Remote-Exec Provisioner
Purpose
Run commands inside the VM after creation.
Goal
Install Nginx and deploy a simple webpage automatically.
Implementation
provisioner "remote-exec" {
inline = [
"while [ ! -f /var/lib/cloud/instance/boot-finished ]; do sleep 2; done",
"sudo apt-get update -y",
"sudo apt-get install -y nginx",
"echo '<h1>Terraform Provisioner Demo Working!</h1>' | sudo tee /var/www/html/index.html",
"sudo systemctl restart nginx"
]
}
Result
Opening:
👉 http://<public-ip>/
displayed the custom webpage ✔
Debug Lesson
Initially nginx was not installed because:
- VM was not fully ready
- apt was locked by cloud-init
Adding a wait for:
/var/lib/cloud/instance/boot-finished
fixed the issue.
Debug Steps and Errors Faced
While implementing this project, I faced several real-world issues. These are the exact steps that helped me troubleshoot.
SSH Key Permission Issue on Windows
Azure SSH login failed initially because Windows was treating the private key as insecure.
Fix: Restrict key permissions in PowerShell
icacls <key file path> /inheritance:r
icacls <key file path> /grant:r "$($env:USERNAME):(R)"
icacls <key file path> /remove "Authenticated Users" "BUILTIN\Users" "Everyone"
After this, SSH worked correctly:
ssh -i <key file path> azureuser@<public ip>
Important: The key must be stored on an NTFS formatted drive (not FAT/external USB) for permissions to work.
Web Page Not Loading After Remote-Exec
Even though Terraform apply was successful, the browser showed:
ERR_CONNECTION_REFUSED
Debug Steps Inside VM
- SSH into the VM
ssh -i key1 azureuser@<public-ip>
- Check if nginx is installed
which nginx
sudo systemctl status nginx
- Test locally inside VM
curl http://localhost
Root Cause
- Remote-exec ran before the VM was fully ready
- cloud-init was still configuring the system
- apt was locked at the time of execution
Fix Implemented
Added wait for cloud-init before installing nginx:
while [ ! -f /var/lib/cloud/instance/boot-finished ]; do sleep 2; done
After this change, the webpage loaded correctly.
Lesson Learned
Terraform showing “Apply complete” does not always mean:
- Software is installed
- Services are running
- VM is fully ready
Provisioners need proper waiting and validation logic.
Step 5 – File Provisioner
Purpose
Copy files from local machine → VM.
Implementation
provisioner "file" {
source = "configs/sample.conf"
destination = "/home/azureuser/sample.conf"
}
Verification in VM
ls -l /home/azureuser
cat sample.conf
✔ File successfully transferred.
Understanding Provisioners
Local-Exec
- Runs on local computer
- Used for logs, notifications, triggers
Remote-Exec
- Runs inside the VM
- Installs software, configures OS
File Provisioner
- Copies files to remote system
Important Reality
Terraform provisioners are:
- ❌ Not guaranteed
- ❌ Not idempotent
- ❌ Not recommended for production
Better Alternatives
- cloud-init
- Custom VM images
- Ansible
- Azure VM Extensions
Final Learning Outcome
This mini project helped me understand:
- How Terraform builds infrastructure
- Difference between the 3 provisioners
- Debugging real deployment issues
- Basic Linux + Azure networking
It connected multiple skills:
Terraform + Azure + Linux + Automation

Leave a Reply