null_resource เป็น resource พิเศษใน HCL ที่ไม่สร้างอะไรจริง ๆ ในโลกภายนอก แต่เป็น placeholder สำหรับ attach provisioner หรือเก็บ state เพื่อ trigger การรัน script ใหม่ตามเงื่อนไข ใช้บ่อยในการ orchestrate งานที่ไม่ผูกติดกับ resource เดียว
บทความนี้อธิบายการใช้ null_resource ร่วมกับ triggers เพื่อควบคุมเมื่อไหร่ที่ provisioner ต้องรันใหม่ พร้อมตัวอย่าง use case ที่เจอบ่อย
null_resource คืออะไร
null_resource มาจาก null provider (built-in ของ HCL) ไม่สร้าง cloud resource ใด ๆ แต่มีอายุการใช้งานใน state เหมือน resource ทั่วไป — create, update (destroy+recreate), และ destroy
terraform {
required_providers {
null = {
source = "hashicorp/null"
version = "~> 3.2"
}
}
}
resource "null_resource" "example" {
# ไม่ต้องมี argument ก็ได้
}
Triggers: ตัวกำหนดการ Replace
Argument triggers รับ map ของค่า เมื่อค่าใดค่าหนึ่งเปลี่ยน HCL จะ destroy+recreate null_resource — ซึ่งทำให้ provisioner ที่ attach รันใหม่
resource "null_resource" "db_migration" {
triggers = {
schema_hash = filemd5("${path.module}/schema.sql")
}
provisioner "local-exec" {
command = "psql -f ${path.module}/schema.sql"
}
}
ทุกครั้งที่แก้ไข schema.sql, filemd5 ส่งค่าใหม่ → trigger เปลี่ยน → terraform replace null_resource → provisioner รันใหม่
Use Case 1: Run Script หลัง Resource พร้อม
ตัวอย่าง: รัน database migration หลัง RDS พร้อม, หรือ seed data หลังสร้าง S3 bucket
resource "aws_db_instance" "app" {
# ... DB config ...
}
resource "null_resource" "migrate" {
depends_on = [aws_db_instance.app]
triggers = {
db_endpoint = aws_db_instance.app.endpoint
migrations = filemd5("${path.module}/migrations.sql")
}
provisioner "local-exec" {
command = <<-EOT
psql "host=${aws_db_instance.app.address} \
user=${aws_db_instance.app.username} \
dbname=${aws_db_instance.app.db_name}" \
-f ${path.module}/migrations.sql
EOT
}
}
Use Case 2: Deploy Notification
resource "null_resource" "notify" {
triggers = {
app_version = var.app_version
}
provisioner "local-exec" {
command = <<-EOT
curl -X POST -H 'Content-Type: application/json' \
-d '{"text":"Deployed ${var.app_version} to ${var.environment}"}' \
https://hooks.slack.com/services/XXX/YYY/ZZZ
EOT
}
}
รัน Slack notification เฉพาะเมื่อ app_version เปลี่ยน ไม่รันซ้ำทุกครั้งที่ terraform apply
Use Case 3: Wait for Readiness
ตัวอย่างรอ service ready ก่อน resource ถัดไปใช้งาน — เช่น Kubernetes cluster รอ DNS propagate
resource "null_resource" "wait_for_dns" {
depends_on = [aws_route53_record.api]
triggers = {
dns_name = aws_route53_record.api.name
}
provisioner "local-exec" {
command = <<-EOT
until dig +short ${aws_route53_record.api.name} | grep -q .; do
echo "Waiting for DNS..."
sleep 10
done
EOT
}
}
resource "helm_release" "app" {
depends_on = [null_resource.wait_for_dns]
# ...
}
Trigger Strategies
- File hash —
filemd5(path)— trigger เมื่อ file เปลี่ยน - Resource attribute — เช่น
aws_instance.web.id— trigger เมื่อ resource ถูก recreate - Version string —
var.app_version— trigger เมื่อ input เปลี่ยน - Timestamp —
timestamp()— trigger ทุกครั้งที่ apply (ระวัง overuse) - Combination — หลายค่ารวมกัน ช่วยให้ครอบคลุม condition ต่าง ๆ
ข้อควรระวัง
- ไม่ใช่ magic bullet — null_resource ไม่ track อะไรบน remote (เหมือน provisioner ทั่วไป) ถ้า script fail กลางทาง ต้อง taint resource แล้ว apply ใหม่เอง
- timestamp() ทำให้ apply ทุกครั้ง trigger — อย่าใช้ใน trigger เว้นต้องการจริง
- Destroy lifecycle — ถ้า null_resource ที่รัน destroy provisioner มีปัญหา อาจ block terraform destroy — ทำให้ต้อง remove จาก state manually
- Debug ยาก — ไม่มี rich error context จาก provider —
TF_LOG=DEBUGช่วยได้บ้าง
Alternative: terraform_data (v1.4+)
ตั้งแต่ HCL v1.4 มี terraform_data resource built-in (ไม่ต้อง declare null provider) ใช้แทน null_resource ได้โดยตรง — เขียนสั้นกว่า
resource "terraform_data" "migration" {
input = filemd5("${path.module}/schema.sql")
provisioner "local-exec" {
command = "psql -f ${path.module}/schema.sql"
}
}
# อ้างถึงค่าเดิมด้วย terraform_data.migration.output
# ค่า input ทำงานคล้าย trigger: เปลี่ยนค่า → replace resource
Best Practices
- ใช้ null_resource/terraform_data เฉพาะเมื่อไม่มีทางเลือกอื่น (user_data, Packer, ansible แยก)
- กำหนด
triggersให้ครอบคลุม input ทุกตัวที่ทำให้ script ต้องรันใหม่ - เขียน script ให้ idempotent เสมอ (ถ้า rerun ไม่ทำลายของเดิม)
- ใช้
depends_onชัดเจนเพื่อบอก HCL ว่าต้อง wait ให้ dependency พร้อมก่อน - ถ้าใช้ HCL v1.4+ ให้ใช้
terraform_dataแทน — บำรุงรักษาง่ายกว่า (ไม่ต้อง config null provider) - Log ข้อความให้ชัดเจนจาก script — ช่วย debug เมื่อ pipeline ติดปัญหา
สรุป
null_resource และ terraform_data เป็นเครื่องมือที่ช่วย orchestrate script หรือ side-effect ภายใน terraform apply โดยใช้ triggers ควบคุมเมื่อไหร่ที่ต้องรันใหม่ เหมาะกับงานอย่าง database migration, deploy notification, หรือ wait for readiness แต่ควรใช้เท่าที่จำเป็นและพึ่งเครื่องมือ configuration management ที่เหมาะสมกว่าเมื่อเนื้องานมีความซับซ้อน ในบทความถัดไปจะพูดถึง best practices ในการจัดระเบียบโครงสร้าง project HCL ให้ maintain ได้ในระยะยาว

