By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.
local-exec
remote-exec
(A Hyper-Practical, Zero-Fluff Guide for Engineers Who Need to Ship)
You’re a cloud engineer, and your team just inherited a Terraform codebase that deploys a fleet of EC2 instances. The previous team used remote-exec to run a bash script that installs Docker, configures a user, and pulls a private container image—every single time terraform apply runs. Now, your CI/CD pipeline is failing because the script occasionally times out, and you’re getting paged at 3 AM because a production instance didn’t bootstrap correctly.
terraform apply
This is what local-exec and remote-exec provisioners do:- They let you run arbitrary commands on your local machine (local-exec) or on a newly created resource (remote-exec) after Terraform provisions it.- They’re not idempotent—they run every time, even if nothing changed.- They’re not declarative—they’re imperative scripts bolted onto Terraform, which breaks the IaC paradigm.
Why this matters in production:- Reliability: If the script fails, Terraform marks the resource as "tainted" and destroys/recreates it on the next run (hello, downtime).- Security: Hardcoded secrets in scripts, SSH keys in plaintext, or sudo commands with no audit trail.- Cost: Unnecessary runs waste compute time (e.g., re-installing Docker on every apply).- Debugging: When a remote-exec fails, you’re left with a half-configured instance and no logs (unless you explicitly captured them).
sudo
apply
Real-world scenario:You’re deploying a Kubernetes cluster with Terraform, and the last step is to install Helm charts. The "easy" way is to use remote-exec to run helm install on the control plane. The right way is to use a null_resource with triggers (or better, a dedicated tool like Argo CD). This guide will show you why—and how to avoid the pitfalls.
helm install
connection
hcl connection { type = "ssh" user = "ubuntu" private_key = file("~/.ssh/id_rsa") host = self.public_ip }
when = destroy
hcl provisioner "local-exec" { when = destroy command = "aws elb deregister-instances --load-balancer-name my-lb --instances ${self.id}" }
on_failure = continue
hcl provisioner "remote-exec" { on_failure = continue # ❌ Never do this in production! inline = ["sudo apt update && sudo apt install -y nginx"] }
null_resource
hcl resource "null_resource" "example" { triggers = { instance_id = aws_instance.web.id } provisioner "local-exec" { command = "echo 'Instance ${aws_instance.web.id} created!'" } }
triggers
hcl triggers = { script_hash = filebase64sha256("install.sh") # Re-run if the script changes }
user_data
hcl resource "aws_instance" "web" { user_data = file("user_data.sh") }
>= 1.0
~/.ssh/id_rsa
Deploy an EC2 instance and use remote-exec to install Nginx. Then, break it intentionally to see why this is a bad idea.
Create main.tf:
main.tf
terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } } provider "aws" { region = "us-east-1" } resource "aws_instance" "web" { ami = "ami-0c55b159cbfafe1f0" # Ubuntu 20.04 LTS instance_type = "t3.micro" key_name = "your-key-pair-name" # Replace with your SSH key name # Allow SSH and HTTP vpc_security_group_ids = [aws_security_group.web.id] # Connection block for remote-exec connection { type = "ssh" user = "ubuntu" private_key = file("~/.ssh/id_rsa") host = self.public_ip } # ⚠️ Anti-pattern: remote-exec for bootstrapping provisioner "remote-exec" { inline = [ "sudo apt update -y", "sudo apt install -y nginx", "sudo systemctl start nginx" ] } } resource "aws_security_group" "web" { name = "allow_web_traffic" description = "Allow HTTP and SSH" ingress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } ingress { from_port = 22 to_port = 22 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] # ⚠️ Restrict this in production! } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } } output "public_ip" { value = aws_instance.web.public_ip }
terraform init terraform apply -auto-approve
Expected output:
aws_instance.web: Creating...aws_instance.web: Still creating... [10s elapsed] aws_instance.web: Provisioning with 'remote-exec'...aws_instance.web (remote-exec): Connecting to remote host via SSH...aws_instance.web (remote-exec): Host: 54.210.123.45 aws_instance.web (remote-exec): User: ubuntu aws_instance.web (remote-exec): Password: false aws_instance.web (remote-exec): Private key: true aws_instance.web (remote-exec): Certificate: false aws_instance.web (remote-exec): SSH Agent: false aws_instance.web (remote-exec): Checking Host Key: false aws_instance.web (remote-exec): Connected! aws_instance.web (remote-exec): Hit:1 http://us-east-1.ec2.archive.ubuntu.com/ubuntu focal InRelease aws_instance.web (remote-exec): Get:2 http://us-east-1.ec2.archive.ubuntu.com/ubuntu focal-updates InRelease [114 kB] aws_instance.web: Still creating... [20s elapsed] aws_instance.web (remote-exec): Setting up nginx (1.18.0-0ubuntu1.4) ...aws_instance.web (remote-exec): Processing triggers for systemd (245.4-4ubuntu3.22) ...aws_instance.web: Creation complete after 1m10s [id=i-0123456789abcdef0] Apply complete! Resources: 2 added, 0 changed, 0 destroyed. Outputs: public_ip = "54.210.123.45"
curl http://$(terraform output -raw public_ip)
<!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> ...</html>
hcl provisioner "remote-exec" { inline = [ "sudo apt update -y", "sudo apt install -y nginxx", # ❌ Typo: "nginxx" instead of "nginx" "sudo systemctl start nginx" ] }
bash terraform apply -auto-approve
aws_instance.web (remote-exec): E: Unable to locate package nginxx aws_instance.web (remote-exec): sudo: systemctl: command not found ╷ │ Error: remote-exec provisioner error │ │ with aws_instance.web, │ on main.tf line 25, in resource "aws_instance" "web": │ 25: provisioner "remote-exec" { │ │ timeout - last error: SSH command error: Process exited with status 100 ╵
bash terraform state show aws_instance.web
status = "tainted" # ⚠️ Terraform marked it as tainted!
hcl resource "aws_instance" "web" { ami = "ami-0c55b159cbfafe1f0" instance_type = "t3.micro" key_name = "your-key-pair-name" user_data = file("user_data.sh") # ✅ Better: cloud-init script vpc_security_group_ids = [aws_security_group.web.id] }
user_data.sh
bash #!/bin/bash sudo apt update -y sudo apt install -y nginx sudo systemctl start nginx
bash curl http://$(terraform output -raw public_ip)
/var/log/cloud-init-output.log
aws_secretsmanager_secret_version
vault_generic_secret
TF_VAR_*
hcl resource "null_resource" "example" { triggers = { script_hash = filebase64sha256("script.sh") } provisioner "local-exec" { command = "./script.sh" } }
hcl resource "null_resource" "db_migration" { triggers = { db_endpoint = aws_db_instance.default.endpoint } provisioner "local-exec" { command = "python migrate.py --db ${aws_db_instance.default.endpoint}" } }
hcl provisioner "remote-exec" { inline = [ "sudo apt update -y 2>&1 | tee /var/log/bootstrap.log", "sudo apt install -y nginx 2>&1 | tee -a /var/log/bootstrap.log" ] }
bash #!/bin/bash exec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1 sudo apt update -y sudo apt install -y nginx
triggers = { script_hash = filebase64sha256("script.sh") }
set -e
❌ remote-exec
"What happens if a remote-exec script fails?"
❌ Terraform continues without error.
"How do you run a script only when a resource is destroyed?"
provisioner "local-exec" { when = destroy ... }
❌ There’s no way to do this.
"What’s the most reliable way to bootstrap an EC2 instance?"
"You need to install a monitoring agent on 100 EC2 instances. What’s the most reliable approach?"- ❌ Use remote-exec in Terraform.- ✅ Use user_data with a cloud-init script.- ✅ Use AWS Systems Manager (SSM) to run the agent install.- ✅ Use a configuration management tool like Ansible.
Why?- remote-exec is not scalable (Terraform would SSH into 100 instances sequentially).- user_data is idempotent and runs at boot.- SSM/Ansible are designed for this and provide better logging.
Join 4M+ learners. Unlock unlimited quizzes, wrong-answer tracking, flashcards + reminders, study guides, and 1-on-1 challenges.