Skip to main content

local-exec Provisioner

รัน command บน เครื่องที่รัน Terraform (ไม่ใช่ remote resource)

Syntax

resource "aws_instance" "web" {
# ...

provisioner "local-exec" {
command = "echo Created: ${self.id} > log.txt"
}
}

Arguments

Argumentคำอธิบาย
commandCommand ที่จะรัน (string)
working_dirDirectory ที่รัน command
interpreterShell ที่รัน (default: /bin/sh -c หรือ cmd /C)
environmentEnv vars ที่ส่งให้ command
whencreate (default) หรือ destroy
on_failurefail (default) หรือ continue
quietซ่อน output (default false)

ตัวอย่าง: Basic

resource "aws_instance" "web" {
ami = "ami-12345"
instance_type = "t2.micro"

provisioner "local-exec" {
command = "echo 'Instance ${self.id} created at $(date)' >> creation.log"
}
}

ตัวอย่าง: ใช้ self.* Reference

provisioner "local-exec" {
command = "echo 'IP: ${self.public_ip}, ID: ${self.id}' >> instances.log"
}

self = reference ตัว resource เอง (ใช้ได้ใน provisioner เท่านั้น)

ตัวอย่าง: Working Directory

provisioner "local-exec" {
command = "./deploy.sh"
working_dir = "${path.module}/scripts"
}

ตัวอย่าง: Custom Interpreter

# Bash (default จะ /bin/sh)
provisioner "local-exec" {
command = "echo 'Hello'"
interpreter = ["bash", "-c"]
}

# Python
provisioner "local-exec" {
command = "import os; print(os.environ['INSTANCE_ID'])"
interpreter = ["python3", "-c"]
environment = {
INSTANCE_ID = self.id
}
}

# PowerShell (Windows)
provisioner "local-exec" {
command = "Write-Host 'Created'"
interpreter = ["pwsh", "-Command"]
}

ตัวอย่าง: Environment Variables

provisioner "local-exec" {
command = "echo $MY_VAR"

environment = {
MY_VAR = "Hello from Terraform"
INSTANCE_ID = self.id
AWS_REGION = "ap-southeast-1"
}
}

ตัวอย่าง: Destroy Time

resource "aws_instance" "web" {
# ...

# Run when created
provisioner "local-exec" {
command = "echo 'Created' >> log.txt"
}

# Run before destroyed
provisioner "local-exec" {
when = destroy
command = "echo 'Destroying ${self.id}' >> log.txt"
}
}

ตัวอย่าง: null_resource Pattern

ใช้ provisioner โดยไม่ต้องมี real resource:

resource "null_resource" "kubeconfig" {
triggers = {
cluster_name = aws_eks_cluster.main.name
}

provisioner "local-exec" {
command = "aws eks update-kubeconfig --name ${aws_eks_cluster.main.name} --region ${data.aws_region.current.name}"
}
}

triggers = ทำ provisioner re-run เมื่อค่าเปลี่ยน

Common Use Cases

Use Case 1: Run Ansible Playbook

resource "aws_instance" "web" {
# ...
}

resource "null_resource" "ansible" {
triggers = {
instance_id = aws_instance.web.id
}

provisioner "local-exec" {
command = <<-EOF
ansible-playbook \
-i '${aws_instance.web.public_ip},' \
--user ec2-user \
--private-key ~/.ssh/id_rsa \
playbook.yml
EOF
}
}

Use Case 2: Update kubeconfig

resource "null_resource" "kubeconfig" {
triggers = {
endpoint = aws_eks_cluster.main.endpoint
}

provisioner "local-exec" {
command = "aws eks update-kubeconfig --name ${aws_eks_cluster.main.name}"
}

depends_on = [aws_eks_cluster.main]
}

Use Case 3: Notify Team

resource "null_resource" "notify" {
triggers = {
deployed_at = timestamp()
}

provisioner "local-exec" {
command = <<-EOF
curl -X POST $SLACK_WEBHOOK \
-H 'Content-type: application/json' \
--data '{"text":"Deployed prod at $(date)"}'
EOF
}
}

Use Case 4: Generate Local Files

resource "null_resource" "config" {
triggers = {
api_url = aws_alb.main.dns_name
}

provisioner "local-exec" {
command = <<-EOF
cat > .env <<ENV
API_URL=https://${aws_alb.main.dns_name}
DB_HOST=${aws_db_instance.main.address}
ENV
EOF
}
}

(แต่ใช้ local_file resource ดีกว่า)

Use Case 5: Run Database Migration

resource "null_resource" "migrate" {
triggers = {
db_endpoint = aws_db_instance.main.endpoint
}

provisioner "local-exec" {
command = "psql -h ${aws_db_instance.main.address} -U admin -d mydb -f migrations.sql"

environment = {
PGPASSWORD = random_password.db.result
}
}

depends_on = [aws_db_instance.main]
}

ทางเลือกที่ดีกว่า

Use Case: Update kubeconfig

ดีกว่า: ใช้ Helm/kubectl provider ตรงๆ:

provider "kubernetes" {
host = aws_eks_cluster.main.endpoint
cluster_ca_certificate = base64decode(aws_eks_cluster.main.certificate_authority[0].data)
token = data.aws_eks_cluster_auth.main.token
}

resource "kubernetes_namespace" "app" {
metadata { name = "myapp" }
}

Use Case: Write to local file

ดีกว่า: local_file resource:

resource "local_file" "kubeconfig" {
content = templatefile("kubeconfig.tpl", { ... })
filename = "${path.module}/kubeconfig"
}

Use Case: HTTP request

ดีกว่า: http data source:

data "http" "myip" {
url = "https://api.ipify.org"
}

Quirks & Gotchas

1. Command runs in $TERRAFORM_DIR

provisioner "local-exec" {
command = "ls"
}
# → list files ของ Terraform directory

ถ้าจะรันใน folder อื่น → ใช้ working_dir

2. Quotes Hell

HCL string + shell escaping = ปวดหัว

# ❌ จะ break
command = "echo "hello""

# ✅ ใช้ heredoc
command = <<-EOT
echo "hello"
EOT

3. Cross-Platform Issues

provisioner "local-exec" {
command = "rm -rf /tmp/cache"
# ❌ Windows ไม่มี rm
}

แก้: ใช้ Terraform built-in หรือ check OS:

provisioner "local-exec" {
command = "Remove-Item -Recurse -Force C:\\temp\\cache"
interpreter = ["pwsh", "-Command"]
}

4. Doesn't Re-run Automatically

ถ้า command ตัวเองเปลี่ยน — Terraform ไม่รู้ → ต้องใช้ triggers

resource "null_resource" "x" {
triggers = {
script_hash = filemd5("script.sh") # ← detect change
}

provisioner "local-exec" {
command = "./script.sh"
}
}

Output Capture

local-exec output แสดงใน apply log:

$ terraform apply

null_resource.kubeconfig: Provisioning with 'local-exec'...
null_resource.kubeconfig (local-exec): Executing: ["/bin/sh" "-c" "..."]
null_resource.kubeconfig (local-exec): Updated context arn:aws:eks:...

ซ่อน output:

provisioner "local-exec" {
command = "echo secret"
quiet = true
}

ตัวอย่าง: รวม

resource "aws_eks_cluster" "main" {
# ...
}

resource "null_resource" "kubeconfig" {
triggers = {
cluster_name = aws_eks_cluster.main.name
endpoint = aws_eks_cluster.main.endpoint
}

provisioner "local-exec" {
command = "aws eks update-kubeconfig --name ${aws_eks_cluster.main.name} --region ap-southeast-1"
}

provisioner "local-exec" {
when = destroy
command = "kubectl config delete-context ${self.triggers.cluster_name}"
}
}

resource "null_resource" "install_addons" {
triggers = {
cluster = null_resource.kubeconfig.id
}

provisioner "local-exec" {
command = <<-EOF
kubectl apply -f https://raw.githubusercontent.com/aws/eks-charts/main/stable/aws-load-balancer-controller/crds/crds.yaml
helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
--namespace kube-system \
--set clusterName=${aws_eks_cluster.main.name}
EOF
}
}

สรุป

  • local-exec = รัน command บนเครื่องที่ apply
  • ใช้ self.<attr> reference resource ตัวเอง
  • ใช้ null_resource + triggers เป็น pattern หลัก
  • ทางเลือก: Kubernetes/Helm provider, local_file, http data source
  • ระวัง quotes + cross-platform + triggers ที่ถูก

ต่อไป → remote-exec provisioner