Skip to main content

Provisioners Overview

Provisioner = รัน script/command ตอน resource ถูกสร้างหรือทำลาย — HashiCorp แนะนำเป็น last resort

Provisioner คืออะไร?

provisioner block ภายใน resource → รัน command:

  • Creation time — หลัง resource สร้างเสร็จ
  • Destroy time — ก่อน resource ถูกลบ

ทำไม HashiCorp บอกเป็น Last Resort?

Official docs:

"Provisioners should only be used as a last resort. For most common situations there are better alternatives."

ปัญหาของ provisioner:

  • Imperative — ขัดกับ Terraform's declarative model
  • Hard to debug — error เกิดกลาง apply
  • State tracking ไม่ได้ — Terraform ไม่รู้ว่ารันสำเร็จไหม
  • Network requirements — ต้อง SSH/WinRM ได้
  • Re-run logic — destroy provisioner รันแค่ตอน destroy

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

Cloud-init / User Data

แทน remote-exec ใช้ user data:

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

user_data = <<-EOF
#!/bin/bash
yum install -y httpd
systemctl start httpd
EOF
}

✅ ข้อดี:

  • รันใน boot phase (no SSH needed)
  • มี logs ใน /var/log/cloud-init.log
  • Re-run เองถ้า fail

Pre-baked AMI (Packer)

สร้าง AMI ที่ติดตั้ง software ไว้แล้ว:

# Build with Packer
packer build web-server.pkr.hcl

# ใช้ใน Terraform
data "aws_ami" "web" {
most_recent = true
owners = ["self"]

filter {
name = "name"
values = ["web-server-*"]
}
}

resource "aws_instance" "web" {
ami = data.aws_ami.web.id
# ไม่ต้อง provisioner — config built-in AMI แล้ว
}

Configuration Management

ใช้ Ansible/Chef/Puppet แยกจาก Terraform

# 1. Terraform สร้าง EC2
terraform apply

# 2. Ansible ตั้งค่า
ansible-playbook -i inventory site.yml

AWS Systems Manager

รัน command ผ่าน SSM (no SSH):

resource "aws_ssm_document" "config" {
name = "configure-web-server"
document_type = "Command"
content = jsonencode({
schemaVersion = "2.2"
description = "Configure web server"
mainSteps = [{
action = "aws:runShellScript"
name = "configure"
inputs = {
runCommand = ["yum install -y httpd", "systemctl start httpd"]
}
}]
})
}

เมื่อไหร่ใช้ Provisioner ก็ได้

Use Case 1: Local Setup (local-exec)

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

depends_on = [aws_eks_cluster.main]
}

Use Case 2: Trigger External Process

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

provisioner "local-exec" {
command = <<-EOF
curl -X POST $SLACK_WEBHOOK \
-d '{"text": "Deployed!"}'
EOF
}
}

Use Case 3: Bootstrap ที่ user_data ทำไม่ได้

ส่วนน้อยมาก — ส่วนใหญ่ user_data + cloud-init เพียงพอ

Provisioner Types

Provisionerหน้าที่
fileCopy file จาก local → remote
local-execRun command บนเครื่องที่ apply
remote-execRun command บน remote resource

ดูเพิ่ม:

Creation Time vs Destroy Time

Creation Time (default)

รันหลังสร้าง resource:

resource "aws_instance" "web" {
# ...
provisioner "local-exec" {
command = "echo 'Created ${self.id}'"
when = create # default
}
}

Destroy Time

รันก่อนลบ resource:

resource "aws_instance" "web" {
# ...
provisioner "local-exec" {
when = destroy
command = "echo 'Destroying ${self.id}' >> destroy.log"
}
}

ทั้งคู่

resource "aws_instance" "web" {
# ...
provisioner "local-exec" {
command = "echo 'Created'"
# default: when = create
}

provisioner "local-exec" {
when = destroy
command = "echo 'Destroying'"
}
}
Destroy Provisioner Limitations

Destroy provisioner ที่ reference variables/resources อื่นถูก deprecated ใน Terraform 0.12+

ต้อง self-contained:

# ❌ Error — reference var
provisioner "local-exec" {
when = destroy
command = "echo ${var.name}" # ห้าม
}

# ✅ ใช้ self.* ได้
provisioner "local-exec" {
when = destroy
command = "echo ${self.id}" # OK
}

on_failure

ตั้งว่าจะทำยังไงถ้า provisioner fail:

provisioner "local-exec" {
command = "./flaky-script.sh"
on_failure = continue # ไม่ fail apply
# default: fail
}
Valueความหมาย
fail (default)Fail entire apply
continueLog error แต่ไป resource ถัดไป

null_resource Pattern

ใช้ provisioner โดยไม่ผูกกับ resource จริง:

resource "null_resource" "deploy" {
triggers = {
cluster_name = aws_eks_cluster.main.name
deployed_at = timestamp()
}

provisioner "local-exec" {
command = "kubectl apply -f manifests/"
}

depends_on = [aws_eks_cluster.main]
}

triggers = เปลี่ยนเมื่อค่าใน triggers เปลี่ยน → re-run

Connection Block (สำหรับ remote provisioner)

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

connection {
type = "ssh"
user = "ec2-user"
private_key = file("~/.ssh/id_rsa")
host = self.public_ip
}

provisioner "remote-exec" {
inline = ["echo 'Hello from ${self.id}'"]
}
}

ตัวอย่าง: Anti-Pattern vs Better

❌ Anti-Pattern

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

connection {
type = "ssh"
user = "ec2-user"
host = self.public_ip
}

provisioner "remote-exec" {
inline = [
"yum install -y httpd",
"systemctl start httpd",
"echo 'Hello' > /var/www/html/index.html"
]
}
}

✅ Better (user_data)

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

user_data = <<-EOF
#!/bin/bash
yum install -y httpd
systemctl start httpd
echo 'Hello' > /var/www/html/index.html
EOF
}

สรุป

  • Provisioners = last resort — ใช้ทางเลือกอื่นก่อน
  • ทางเลือก: user_data, cloud-init, Packer AMI, Ansible, SSM
  • 3 types: file, local-exec, remote-exec
  • Creation time (default) vs destroy time (when = destroy)
  • Use null_resource เพื่อ provisioner ที่ไม่ผูกกับ resource จริง
  • Set on_failure ถ้าต้องการ continue on error

ต่อไป → file provisioner