TFLAB19-Provisioners: local-exec, remote-exec and Why They Are a Last Resort

Provisioners: local-exec, remote-exec and Why They Are a Last Resort

🔧 Terraform Core ⭐ Intermediate local-exec remote-exec file provisioner on_failure self destroy-time

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."

Your Objectives
  • 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 entry
✓ /tmp/terraform-inventory.log After Apply + Destroy
Bucket created: provisioner-demo-123456789012
Bucket=provisioner-demo-123456789012 Region=us-east-1
Bucket provisioner-demo-123456789012 is being destroyed