How to Use Terraform Modules - Build Reusable Infrastructure
Create and use Terraform modules for reusable infrastructure code. Covers module structure, inputs, outputs, versioning, and the Terraform Registry.
Terraform
Learn when to use Terraform provisioners and when to avoid them. Covers local-exec, remote-exec, file provisioner, null_resource, and better alternatives.
Provisioners run scripts on local or remote machines during resource creation/destruction. Use them as a last resort — prefer user_data, configuration management tools, or cloud-init. When you must use them, local-exec is safer than remote-exec.
Runs a command on the machine running Terraform:
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
provisioner "local-exec" {
command = "echo ${self.private_ip} >> inventory.txt"
}
}Runs commands on the remote resource via SSH or WinRM:
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
key_name = aws_key_pair.deploy.key_name
connection {
type = "ssh"
user = "ubuntu"
private_key = file("~/.ssh/deploy.pem")
host = self.public_ip
}
provisioner "remote-exec" {
inline = [
"sudo apt-get update",
"sudo apt-get install -y nginx",
"sudo systemctl enable nginx",
]
}
}Copies files or directories to the remote machine:
provisioner "file" {
source = "configs/nginx.conf"
destination = "/tmp/nginx.conf"
}
provisioner "remote-exec" {
inline = ["sudo cp /tmp/nginx.conf /etc/nginx/nginx.conf"]
}| Instead of... | Use... |
|---|---|
remote-exec for package install | user_data / cloud-init |
remote-exec for configuration | Ansible, Chef, Puppet |
local-exec for API calls | Terraform provider or http data source |
file + remote-exec for app deploy | CI/CD pipeline |
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
user_data = <<-EOF
#!/bin/bash
apt-get update
apt-get install -y nginx
systemctl enable nginx
systemctl start nginx
EOF
user_data_replace_on_change = true
}resource "null_resource" "deploy" {
triggers = {
app_version = var.app_version
}
provisioner "local-exec" {
command = "./deploy.sh ${var.app_version} ${aws_instance.web.public_ip}"
}
}resource "terraform_data" "deploy" {
triggers_replace = [var.app_version]
provisioner "local-exec" {
command = "curl -X POST https://deploy.example.com/trigger"
}
}| Behavior | Default | Override |
|---|---|---|
| When it runs | On creation | when = destroy |
| On failure | Marks resource tainted | on_failure = continue |
| Retry | None | Not built-in |
provisioner "local-exec" {
when = destroy
on_failure = continue
command = "cleanup.sh ${self.id}"
}connection { timeout = "10m" } — instance may still be bootingProvisioners are a last resort in Terraform. Prefer user_data for bootstrapping, configuration management tools for ongoing configuration, and CI/CD for deployments. When you must use provisioners, local-exec is simpler and doesn't require SSH access.
Create and use Terraform modules for reusable infrastructure code. Covers module structure, inputs, outputs, versioning, and the Terraform Registry.
Destroy individual Terraform resources without affecting the rest. Covers -target flag, state rm, replace, and lifecycle prevent_destroy.
Learn the best way to organize Terraform projects. Covers file structure, modules, environments, and naming conventions for scalable infrastructure code.
Master Terraform lifecycle meta-arguments. Covers prevent_destroy, create_before_destroy, ignore_changes, and replace_triggered_by with examples.