In Terraform, what is the difference between the local-exec and remote-exec provisioners, and what does each one need in order to run?
answer
- where the command executes, not when
- one is the Terraform host
- the other needs SSH or WinRM
- connection block, self.public_ip
- file uploads, remote-exec runs
basics
~20 slocal-exec runs a command on the machine running Terraform itself. remote-exec runs commands on the resource that was just created, over SSH or WinRM, so it additionally needs a connection block with a reachable host and working credentials.
solid answer
~50 sBoth are provisioner blocks nested inside a resource, and both run after that resource is created. The difference is *where the command executes*. `local-exec` runs on whatever machine is running Terraform — your laptop, a CI runner, a Terraform Cloud worker — so it can only reach things that machine can reach, and it takes arguments like `command`, `working_dir`, `interpreter` and `environment`. `remote-exec` executes `inline` commands or a `script`/`scripts` file *on the new resource*, so it requires a `connection` block declaring `type = "ssh"` or `"winrm"`, a `host` (usually `self.public_ip`), a `user`, and a key or password. The related `file` provisioner also uses `connection` — it copies a `source` file or inline `content` up to a `destination` path on the remote machine. In practice you often pair `file` and `remote-exec`: upload a script, then run it.
code
hcl · 27 linesresource "aws_instance" "web" {
ami = var.ami_id
instance_type = "t3.micro"
connection {
type = "ssh"
user = "ubuntu"
private_key = file("~/.ssh/id_rsa")
host = self.public_ip
}
provisioner "file" {
source = "${path.module}/bootstrap.sh"
destination = "/tmp/bootstrap.sh"
}
provisioner "remote-exec" {
inline = [
"chmod +x /tmp/bootstrap.sh",
"sudo /tmp/bootstrap.sh",
]
}
provisioner "local-exec" {
command = "echo ${self.public_ip} >> hosts.txt"
}
}go deeper
Be able to say in one line that local-exec runs on the Terraform host and remote-exec runs on the created resource over SSH or WinRM, and that remote-exec needs a connection block.
Explain the arguments each takes — command/working_dir/interpreter/environment versus inline/script/scripts — and why self exists inside provisioner and connection blocks instead of a normal resource reference.
Show the three preconditions remote-exec silently depends on: a network route from wherever Terraform runs, an open port, and credentials valid on the booted image. Say what you use instead in production.
Own the position that shell-out steps in the resource graph are an operational liability, and set a team standard for where bootstrapping lives — image build, cloud-init, or a config-management handoff.
## What a provisioner is A provisioner is a block nested inside a `resource` block that runs an imperative step as part of that resource's create (or destroy) operation. It is the one place in Terraform where you leave the declarative model: instead of describing a desired end state and letting the provider converge on it, you are saying "after this thing exists, go run these commands". Terraform ships three built-in provisioners — `local-exec`, `remote-exec`, and `file` — and HashiCorp's own documentation frames all of them as a last resort. ## local-exec: runs where Terraform runs `local-exec` invokes a command on the machine executing `terraform apply`. That is the critical detail candidates get wrong: it has nothing to do with the resource being created except *timing*. If Terraform runs on a CI runner in a different network, the command runs on the runner. ```hcl resource "aws_instance" "web" { ami = var.ami_id instance_type = "t3.micro" provisioner "local-exec" { command = "echo ${self.private_ip} >> inventory.txt" working_dir = path.module environment = { REGION = var.region } } } ``` Useful arguments are `command` (required), `working_dir`, `interpreter` (to run through something other than the default shell), and `environment` (a map of extra environment variables). Inside a provisioner you may reference `self`, which is the resource instance the provisioner is attached to — `self.private_ip`, `self.public_ip`, `self.id`, and so on. You cannot reference `self` outside a provisioner or `connection` block. ## remote-exec: runs on the resource `remote-exec` opens a session *to the created resource* and executes commands there. It accepts exactly one of three arguments: `inline` (a list of command strings), `script` (a single local script file, uploaded and then executed), or `scripts` (a list of them). ```hcl provisioner "remote-exec" { inline = [ "sudo apt-get update", "sudo apt-get install -y nginx", ] } ``` Because it has to reach the machine, it requires a `connection` block. ## The connection block `connection` describes how Terraform talks to the remote host. It can be declared once at the resource level — where it applies to every provisioner in that resource — or inside an individual provisioner block, which overrides the resource-level one. ```hcl connection { type = "ssh" user = "ubuntu" private_key = file("~/.ssh/id_rsa") host = self.public_ip } ``` Common SSH arguments are `type`, `user`, `password`, `host`, `port`, `private_key`, `agent`, `host_key`, `timeout`, and `script_path`. For jumping through a jump box there are `bastion_host`, `bastion_user`, `bastion_private_key`, and `bastion_port`. For Windows, `type = "winrm"` unlocks `https`, `insecure`, `use_ntlm` and `cacert`. Three things must all be true for `remote-exec` to work: the machine running Terraform must have a network route to `host`, the port must be open (security group, firewall, NACL), and the credentials must be valid for a user that already exists on the freshly booted image. ## The file provisioner `file` copies data to the remote resource and therefore also needs a `connection`. It takes `destination` plus exactly one of `source` (a local path) or `content` (an inline string, handy with `templatefile`). ```hcl provisioner "file" { content = templatefile("${path.module}/app.conf.tftpl", { port = 8080 }) destination = "/tmp/app.conf" } ``` Provisioners inside one resource run in the order they are written, so the common pattern is `file` to upload, then `remote-exec` to run `sudo mv` and a service restart. ## Attaching provisioners to nothing in particular If the command is not tied to a specific resource, you can attach the provisioner to `terraform_data` (built in since Terraform 1.4) or, historically, to `null_resource` from the `hashicorp/null` provider. ## When each is actually used `local-exec` shows up for glue: kicking off an external CLI, writing a file for another tool, tagging a build. `remote-exec` shows up for bootstrapping a VM — and that is precisely the use HashiCorp advises against, because passing a cloud-init/`user_data` script or baking a prebuilt image gets the same result without Terraform needing a network path and credentials to the box. Knowing the difference is table stakes; knowing why to avoid `remote-exec` is the follow-up an interviewer actually cares about.
- Inside a provisioner, what does the self object refer to, and why do you need it?`self` is the resource instance the provisioner is attached to — you write `self.public_ip` or `self.id`. It exists because referencing the resource by its own address (`aws_instance.web.public_ip`) inside its own block would be a self-referential cycle in the dependency graph. `self` is only valid inside `provisioner` and `connection` blocks.
- If a resource has both a file and a remote-exec provisioner, in what order do they run?In the order they appear in the configuration. Provisioners within a single resource are executed sequentially, top to bottom, which is why the upload-then-execute pattern works: put the `file` block first, the `remote-exec` that consumes the uploaded path second.
saying these in an interview costs you the question
- Thinking local-exec runs on the created instance
- Assuming remote-exec works without a connection block
- Believing provisioners re-run on every apply
- Thinking the file provisioner copies between two local paths