Provisioners: local-exec, remote-exec and Why They Are a Last Resort
Scenario
After creating an S3 bucket, you need to run a local script that registers the bucket in your team's internal inventory system. This action cannot be expressed as a Terraform resource — it's a custom API call. You also want to understand remote-exec (running commands on a remote instance) and why HashiCorp calls provisioners a "last resort."
- Add a local-exec provisioner to run a command on your machine after a resource is created.
- Use self to reference the parent resource's attributes.
- Add on_failure = continue to handle non-critical provisioner failures.
- Add a destroy-time provisioner that runs when the resource is destroyed.
- Understand why remote-exec and file provisioners exist but should be avoided in favor of cloud-native alternatives.
Additional Context
Provisioners run once at create time (or destroy time). They do NOT re-run on updates. They are not visible in terraform plan output. If a provisioner fails, the resource is marked as tainted — meaning it will be destroyed and recreated on the next apply.
Better alternatives: Use user_data / cloud-init for VM bootstrapping, Packer for building pre-configured images, and Ansible/Puppet/Chef for ongoing configuration management. These are purpose-built tools that are idempotent, visible, and don't have SSH connectivity requirements.
main.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}
provider "aws" { region = "us-east-1" }
data "aws_caller_identity" "current" {}
resource "aws_s3_bucket" "app" {
bucket = "provisioner-demo-${data.aws_caller_identity.current.account_id}"
tags = { ManagedBy = "terraform" }
# ─── CREATE-TIME local-exec ───
# Runs on YOUR machine (where terraform executes), not in the cloud.
# 'self' refers to this resource — use it to access attributes.
provisioner "local-exec" {
command = "echo 'Bucket created: ${self.id}' >> /tmp/terraform-inventory.log"
}
# ─── local-exec with environment variables ───
provisioner "local-exec" {
command = "echo Bucket=$BUCKET_NAME Region=$AWS_REGION >> /tmp/terraform-inventory.log"
environment = {
BUCKET_NAME = self.id
AWS_REGION = "us-east-1"
}
}
# ─── local-exec with on_failure = continue ───
# If this fails, Terraform warns but does NOT taint the resource.
# Use this for non-critical operations like notifications.
provisioner "local-exec" {
command = "echo 'Sending notification...' && false" # simulated failure
on_failure = continue # warn but keep going
}
# ─── DESTROY-TIME provisioner ───
# Runs BEFORE the resource is destroyed. Useful for deregistration.
provisioner "local-exec" {
when = destroy
command = "echo 'Bucket ${self.id} is being destroyed' >> /tmp/terraform-inventory.log"
}
}
# ─── remote-exec EXAMPLE (commented — requires SSH access) ───
# Shown for reference only. In practice, use user_data or Packer instead.
#
# resource "aws_instance" "web" {
# ami = "ami-0c55b159cbfafe1f0"
# instance_type = "t3.micro"
# key_name = "my-key"
#
# connection {
# type = "ssh"
# user = "ec2-user"
# private_key = file("~/.ssh/my-key.pem")
# host = self.public_ip
# }
#
# provisioner "remote-exec" {
# inline = [
# "sudo yum update -y",
# "sudo yum install -y httpd",
# "sudo systemctl start httpd",
# ]
# }
# }outputs.tf
output "bucket_name" { value = aws_s3_bucket.app.id }
output "inventory_log" {
value = "Check /tmp/terraform-inventory.log for provisioner output"
}Workflow Commands
terraform init && terraform apply
# Check the local log created by the provisioner
cat /tmp/terraform-inventory.log
# Destroy — triggers the destroy-time provisioner
terraform destroy
cat /tmp/terraform-inventory.log # now includes the destroy entryBucket created: provisioner-demo-123456789012 Bucket=provisioner-demo-123456789012 Region=us-east-1 Bucket provisioner-demo-123456789012 is being destroyed

