Provisioners Overview
Provisioner = รัน script/command ตอน resource ถูกสร้างหรือทำลาย — HashiCorp แนะนำเป็น last resort
Provisioner คืออะไร?
provisioner block ภายใน resource → รัน command:
- Creation time — หลัง resource สร้างเสร็จ
- Destroy time — ก่อน resource ถูกลบ
ทำไม HashiCorp บอกเป็น Last Resort?
"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 | หน้าที่ |
|---|---|
file | Copy file จาก local → remote |
local-exec | Run command บนเครื่องที่ apply |
remote-exec | Run 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 ที่ 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 |
continue | Log 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