ข้ามไปยังเนื้อหา

Count and For Each

count และ for_each ต่างก็สร้าง resource มากกว่าหนึ่งชุดจาก block เดียว แต่อ้างอิงแต่ละชุดต่างกันโดยพื้นฐาน และความต่างนั้นตัดสินว่าจะเพิ่มหรือลบทีหลังได้อย่างปลอดภัยแค่ไหน

count รับจำนวนเต็มแล้วสร้าง resource ที่แทบเหมือนกันตามจำนวนนั้น นับ index 0 ถึง count - 1 ภายใน resource count.index บอกตำแหน่งของชุดปัจจุบัน ภายนอก resource แต่ละชุดถูกอ้างอิงด้วย index ตัวเลขเดียวกันนั้น

variable "instance_count" {
type = number
description = "Number of Compute Engine instances to launch"
default = 3
}
resource "google_compute_instance" "web" {
count = var.instance_count
name = "web-${count.index}"
machine_type = "e2-medium"
zone = "us-central1-a"
boot_disk {
initialize_params {
image = "debian-cloud/debian-12"
}
}
network_interface {
network = "default"
access_config {}
}
}

เมื่อ instance_count = 3 Terraform สร้าง google_compute_instance.web[0], web[1] และ web[2] แต่ละตัวถูกอ้างอิงจากภายนอกด้วย index ในวงเล็บนั้น — google_compute_instance.web[0] หมายถึงชุดแรกโดยเฉพาะ web[1] คือชุดที่สอง และต่อไปเรื่อย ๆ

จุดอ่อนที่แท้จริงของ count คือ identity ของแต่ละชุดไม่ใช่อะไรมากไปกว่าตำแหน่งใน list สมมติว่า count ไม่ได้ขับเคลื่อนด้วยตัวเลขธรรมดา แต่ด้วย list ของชื่อ:

variable "instance_names" {
type = list(string)
description = "Names of Compute Engine instances to launch"
default = ["web-a", "web-b", "web-c"]
}
resource "google_compute_instance" "web" {
count = length(var.instance_names)
name = var.instance_names[count.index]
machine_type = "e2-medium"
zone = "us-central1-a"
boot_disk {
initialize_params {
image = "debian-cloud/debian-12"
}
}
network_interface {
network = "default"
access_config {}
}
}

ตรงนี้ web[0] คือ "web-a", web[1] คือ "web-b" และ web[2] คือ "web-c" ลอง remove "web-b" ออกจากตรงกลางของ list เหลือ ["web-a", "web-c"] Terraform ไม่เข้าใจว่า "web-b" ถูกลบไปโดยเฉพาะ — สิ่งที่มันเห็นมีแค่ว่า index 1 ตอนนี้ map ไปยังชื่ออื่นแล้ว และ index 2 ไม่มีอยู่แล้ว

พิสูจน์ด้วย count บน local_file (กลไก index-shifting เป๊ะ ไม่ต้องมี GCP project ก็เห็นได้ — ตัว google_compute_instance เองต้องมี project จริงแม้แต่จะ plan ซึ่งคอร์สนี้ไม่มี):

$ terraform plan
# local_file.web[1] must be replaced
-/+ resource "local_file" "web" {
~ content = "web-b" -> "web-c" # forces replacement
# (other computed attributes omitted)
}
# local_file.web[2] will be destroyed
# (because index [2] is out of range for count)
- resource "local_file" "web" {
- content = "web-c" -> null
- filename = "./out-2.txt" -> null
}
Plan: 1 to add, 0 to change, 2 to destroy.

Index 1 (เดิมคือ "web-b") ถูก destroy แล้วสร้างใหม่ด้วย content "web-c" และ index 2 ถูก destroy ตรง ๆ โดยไม่มีอะไรมาแทนที่ ลบหนึ่งรายการจาก list สามรายการ ได้ผลลัพธ์เป็น 1 create + 2 destroy — ทุก resource ที่ index เลื่อนถูกแตะต้องหมด เพียงเพราะมันขยับตำแหน่งใน list

for_each รับ map หรือ set ของ string แล้วสร้าง resource instance หนึ่งตัวต่อหนึ่ง key อ้างอิงด้วย key แทน index ตัวเลข

variable "instances" {
type = map(string)
description = "Map of instance key to machine type"
default = {
primary = "e2-medium"
secondary = "e2-small"
batch = "e2-standard-2"
}
}
resource "google_compute_instance" "web" {
for_each = var.instances
name = "web-${each.key}"
machine_type = each.value
zone = "us-central1-a"
boot_disk {
initialize_params {
image = "debian-cloud/debian-12"
}
}
network_interface {
network = "default"
access_config {}
}
}

แต่ละ instance ถูกอ้างอิงจากภายนอกเป็น google_compute_instance.web["primary"], web["secondary"] และ web["batch"] ภายใน resource each.key คือ map key และ each.value คือ value ของมัน

การลบ key ออกไปทั้งหมด (ต่างจากการเปลี่ยนชื่อ) ไม่กระทบข้อควรระวังเรื่อง mock_provider ข้างบนเลย เพราะเกี่ยวข้องแค่กับ state entry ที่หายไปจาก desired set ไม่ใช่การตัดสินใจ in-place-vs-replace พิสูจน์ด้วยตัวอย่างต่อ-key ด้านล่างของบทนี้ (terraform test จริง, google_compute_subnetwork): ลบ key เดียว destroy แค่ resource ของ key นั้นเท่านั้น web["primary"] และ web["batch"] ในที่นี้ถูก key ด้วยชื่อของตัวเอง ไม่ใช่ตำแหน่ง จึงไม่ถูกกระทบเลยเมื่อ key ตรงกลางหายไป

บน set(string) each.key กับ each.value คือ string เดียวกัน แต่บน map ทั้งสองต่างกัน: each.key คือ map key, each.value คือ value ของ key นั้น — ตรงกับตัวแปร instances ข้างบน ที่ each.value ส่ง machine_type ให้แต่ละ instance

พิสูจน์ด้วยเวอร์ชัน local_file ที่เทียบเท่า (content = "instance ${each.key} has type ${each.value}") หลัง apply:

$ cat out-primary.txt
instance primary has type e2-medium
$ cat out-batch.txt
instance batch has type e2-standard-2

each.key ตั้งชื่อ instance ส่วน each.value แบกค่า config เฉพาะตัวของมัน — รูปแบบที่ต้องใช้เมื่อ instance แต่ละตัวของ resource เดียวกันต้องการ argument เฉพาะตัวจริง ๆ ไม่ใช่แค่ชื่อที่ต่างกัน

count = var.enable_x ? 1 : 0 คือรูปแบบมาตรฐานสำหรับ resource ที่เป็นตัวเลือก — 1 ชุดถ้า flag เป็นจริง, 0 ถ้าไม่ใช่ การอ้างอิง resource นั้นจากที่อื่นจะพังทันทีที่ count เป็น 0: google_compute_instance.monitoring[0].id ไม่มี index 0 ให้อ่าน one() แก้ปัญหานี้: มันรับ list ที่มี 0 หรือ 1 element แล้วคืน element เดียวนั้น หรือ null ถ้า list ว่างเปล่า — ไม่ error ไม่ว่ากรณีไหน

variable "enable_monitoring" {
type = bool
default = false
}
resource "google_compute_instance" "monitoring" {
count = var.enable_monitoring ? 1 : 0
name = "monitoring"
machine_type = "e2-medium"
zone = "us-central1-a"
boot_disk {
initialize_params {
image = "debian-cloud/debian-12"
}
}
network_interface {
network = "default"
access_config {}
}
}
output "monitoring_instance_id" {
value = one(google_compute_instance.monitoring[*].id)
}

พิสูจน์ด้วยเวอร์ชัน local_file ที่เทียบเท่า ทั้งสองเงื่อนไข คอนฟิกเดียวกัน:

$ terraform apply -auto-approve # enable_monitoring = false (default)
No changes. Your infrastructure matches the configuration.
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
(monitoring_file_id output: null -- one() on an empty list)
$ terraform apply -auto-approve -var="enable_monitoring=true" # flip it on
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
monitoring_file_id = "3ea3f9802accf8817bacd6f3df46a73b93ccddec"

one([]) คืน null โดยไม่ error, one([x]) คืน x มีแค่กรณีสองรายการขึ้นไปเท่านั้นที่ error จริง ซึ่งเกิดขึ้นไม่ได้ในที่นี้เพราะ count ถูกจำกัดไว้ที่ 0 หรือ 1 เท่านั้น

key ของ for_each ต้องรู้ครบก่อน plan collection ที่มาจาก computed attribute ของ resource อื่น (ID ที่เกิดขึ้นหลังจาก resource นั้นถูกสร้างเท่านั้น) จะ error แม้ค่าเดียวกันจะใช้เป็น argument ธรรมดาได้สบาย ๆ:

resource "random_pet" "names" {
count = 2
}
resource "google_compute_instance" "web" {
for_each = toset(random_pet.names[*].id)
# ...
}

Error จริง (พิสูจน์ด้วยเวอร์ชัน local_file ที่เทียบเท่า — กลไกเป็นของ Terraform core เอง เหมือนกันไม่ว่า resource ประเภทไหนหรือ provider ไหน):

$ terraform plan
╷
│ Error: Invalid for_each argument
│
│ on main.tf line 6, in resource "local_file" "web":
│ 6: for_each = toset(random_pet.names[*].id)
│ ├────────────────
│ │ random_pet.names is tuple with 2 elements
│
│ The "for_each" set includes values derived from resource attributes that
│ cannot be determined until apply, and so Terraform cannot determine the
│ full set of keys that will identify the instances of this resource.
│
│ When working with unknown values in for_each, it's better to use a map
│ value where the keys are defined statically in your configuration and where
│ only the values contain apply-time results.
╵

คำแนะนำของ Terraform เองใน error นั้นคือทางแก้ที่ควรจำ: ใช้ key จากสิ่งที่รู้แน่นอนแบบ static (ชื่อที่คุณเลือกเอง, zone จาก variable) แล้วให้ฝั่ง value เท่านั้นที่แบกผลลัพธ์ตอน apply

null_resource (จาก provider แยกต่างหาก hashicorp/null) เคยเป็นวิธีมาตรฐานในการรัน provisioner หรือเก็บ triggers map โดยไม่มี infrastructure จริงผูกอยู่ terraform_data (built-in ใน Terraform core ตั้งแต่ 1.4 ไม่ต้องมี provider เพิ่ม) มาแทนที่ และเพิ่ม replace_triggered_by: บังคับให้ resource ถูก replace ทุกครั้งที่ค่าที่อ้างอิงเปลี่ยน แม้ไม่มี argument ของตัว resource เองเปลี่ยนเลย

resource "local_file" "config" {
filename = "${path.module}/config.txt"
content = var.config_content
}
resource "terraform_data" "restart_app" {
input = local_file.config.content
lifecycle {
replace_triggered_by = [local_file.config.content]
}
}

Plan จริงหลัง content ของ local_file.config เปลี่ยนเท่านั้น (terraform_data.restart_app ไม่มี argument ของตัวเองที่เปลี่ยนเลย):

$ terraform plan
# terraform_data.restart_app will be replaced due to changes in replace_triggered_by
Plan: 2 to add, 0 to change, 2 to destroy.

การใช้ for_each ตามที่มันถูกใช้จริง: หนึ่ง subnetwork ต่อหนึ่งชื่อ อ้างอิงด้วย key เพื่อให้เพิ่มหรือลบ subnet ไม่รบกวนตัวอื่นเลย พิสูจน์ด้วย terraform test และ mock_provider "google" {} เพราะ plan จริงต้องใช้ GCP project จริงที่คอร์สนี้ไม่มี:

variable "subnets" {
type = map(string)
default = {
web = "10.0.1.0/24"
app = "10.0.2.0/24"
db = "10.0.3.0/24"
}
}
resource "google_compute_network" "main" {
name = "vpc-demo"
auto_create_subnetworks = false
}
resource "google_compute_subnetwork" "this" {
for_each = var.subnets
name = each.key
ip_cidr_range = each.value
region = "us-central1"
network = google_compute_network.main.id
}

รัน terraform test จริง: สร้างทั้งสามตัว แล้วลบ "app" ออกจาก map แล้ว plan อีกครั้ง:

tests/removal.tftest.hcl
mock_provider "google" {}
run "create_three" {
command = apply
}
run "remove_app_key" {
command = plan
variables {
subnets = {
web = "10.0.1.0/24"
db = "10.0.3.0/24"
}
}
}
$ terraform test
# google_compute_subnetwork.this["app"] will be destroyed
Plan: 0 to add, 0 to change, 1 to destroy.
Success! 2 passed, 0 failed.

google_compute_subnetwork.this["app"] ถูก destroy พอดี — web และ db ไม่ปรากฏใน plan เลย เพราะ key ของมันไม่เคยขยับ ต่างจากกรณีเปลี่ยนชื่อที่เตือนไว้ข้างบน การพิสูจน์นี้เกี่ยวข้องแค่กับ key ที่หายไปจาก desired set เท่านั้น ซึ่งเป็นการ reconcile state ธรรมดา ไม่ใช่การตัดสินใจ ForceNew — จึงเชื่อถือได้ตามที่แสดง

for expression เป็น comprehension ทั่วไปที่แปลง collection หนึ่งเป็นอีก collection หนึ่ง — ไม่เกี่ยวอะไรกับการสร้าง resource หลายชุดเลย มันแค่บังเอิญใช้คำว่า “for” ร่วมกับ for_each ซึ่งทำให้คนสับสน

variable "instance_names" {
type = list(string)
description = "Names of Compute Engine instances"
default = ["web-a", "web-b", "web-c"]
}
locals {
# for expression producing a list: transform each name to uppercase
upper_names = [for name in var.instance_names : upper(name)]
# for expression producing a map: build a lookup from name to a derived label
name_to_label = { for name in var.instance_names : name => "label-${name}" }
}

[for name in var.instance_names : upper(name)] เป็น for expression ที่ผลิต list ใหม่ { for name in var.instance_names : name => "label-${name}" } เป็น for expression ที่ผลิต map ใหม่ ทั้งสองไม่ใช่ for_each meta-argument บน resource block — for expression แปลงข้อมูลที่มีอยู่แล้วให้เป็นข้อมูลอื่น ในขณะที่ for_each meta-argument บน resource ตัดสินใจว่าจะสร้าง resource กี่ instance และ key ไหนอ้างอิงแต่ละตัว เป็นเรื่องปกติมาก ๆ ที่จะใช้ for expression สร้าง map แล้วส่งให้ for_each ของ resource — ตัวอย่าง google_compute_subnetwork.this ข้างบนก็สามารถสร้าง map subnets ด้วย for expression จาก input variable ที่แบนกว่านี้ได้เช่นกัน

flowchart TB
  subgraph count_case["count with a list"]
    direction LR
    c0["web[0] = web-a"] --> c1["web[1] = web-b"] --> c2["web[2] = web-c"]
    c2 -.remove web-b.-> shift["web[1] destroyed and recreated, web[2] destroyed"]
  end
  subgraph foreach_case["for_each with a map"]
    direction LR
    f0["web[primary]"] --- f1["web[secondary]"] --- f2["web[batch]"]
    f1 -.remove secondary.-> untouched["only web[secondary] destroyed, primary and batch undisturbed"]
  end
การลบรายการตรงกลางทำให้ index ของ count เลื่อนทุกตัวหลังจากนั้น ส่วน for_each กระทบแค่ key ที่ถูกลบเท่านั้น

count และ for_each ตอบคำถาม “กี่ชุด” เหมือนกัน แต่เข้ารหัส identity ต่างกันโดยสิ้นเชิง: count ได้ identity จาก ตำแหน่งใน list (เปราะบาง — ตำแหน่งเลื่อนทุกครั้งที่รูปร่างของ list เปลี่ยน) ในขณะที่ for_each ได้ identity จาก key ที่คงที่ซึ่งมีความหมายอยู่แล้ว (ชื่อ, zone, map key ที่คุณเลือกเอง) — ทนทานกว่า เพราะลบสิ่งหนึ่งจะไม่ทำให้อีกสิ่งเปลี่ยนชื่อ ทุกการพิสูจน์ข้างบนคือผลจากความต่างข้อเดียวนี้ รวมถึงข้อควรระวังเรื่อง mock_provider: คำถาม rename-in-place-vs-replace ขึ้นอยู่กับสคีมาของ resource เองและ test tooling เคารพมันแค่ไหน แต่ key ที่หายไปจาก for_each set ไม่ขึ้นกับเรื่องนั้นเลย — มันแค่ “key นี้ไม่ต้องการแล้ว” ซึ่งแก้ไขโดย Terraform core เองไม่ว่า provider ไหน

ตัวเลือกข้อดีต้นทุน
countSyntax ง่ายที่สุดสำหรับจำนวน resource ที่แทบเหมือนกันคงที่จริง ๆ และลำดับไม่สำคัญลบรายการตรงกลางทำให้เกิด destroy-and-recreate ไล่ตามทุก index หลังจากนั้น — พิสูจน์ข้างบนด้วย plan diff จริงของ local_file
for_each (set)ลบ key เดียว destroy แค่ resource เดียว — พิสูจน์ข้างบนด้วย subnetworkต้องแปลง list เป็น set (หรือหา stable key เอง) ถ้าเริ่มจากข้อมูลที่มีลำดับ
for_each (map)each.key/each.value แยกกันทำให้แต่ละ instance มี config เฉพาะตัวได้จริง — พิสูจน์ข้างบนด้วย machine type ต่อ instanceทุก key ต้องรู้ก่อน plan — พิสูจน์ข้างบน key ที่มาจาก computed attribute ของ resource อื่นคือ error จริง
terraform_data + replace_triggered_byบังคับ replace อย่างตั้งใจจากสัญญาณภายนอก โดยไม่ต้องใส่ argument ปลอมเพื่อลักลอบสร้าง dependency — พิสูจน์ข้างบนด้วย plan จริงการ replace เป็นแบบทั้งหมด (destroy + recreate) ไม่ใช่ update บางส่วน
  • “ลบรายการจาก list ที่ใช้ count กระทบแค่ resource ของรายการนั้น” พิสูจน์ข้างบนแล้วว่าไม่ใช่ มันไล่ลาม ทุก index หลังจากรายการที่ลบจะเลื่อน และ Terraform อ่านการเลื่อนนั้นเป็น destroy-and-recreate สำหรับทุก index ที่เลื่อน
  • “for_each กับ for expression เป็นฟีเจอร์เดียวกันเพราะใช้คำว่า ‘for’ ร่วมกัน” ทั้งสองไม่เกี่ยวข้องกันเลย: for_each คือ resource meta-argument ที่ทำซ้ำ resource block ส่วน for expression แปลงข้อมูลของ collection หนึ่งเป็นอีก collection หนึ่ง ไม่สร้างหรือทำลายอะไรเลย
  • “อ้างอิง google_compute_instance.monitoring[0] ได้อย่างปลอดภัยแม้ count อาจเป็น 0” พิสูจน์ข้างบนแล้วว่านี่ error ทันทีที่ count ประเมินเป็น 0 one(google_compute_instance.monitoring[*].id) คือรูปแบบที่ปลอดภัย
  • “for_each ใช้ key จากค่า computed อะไรก็ได้ เหมือนที่ argument ธรรมดาอ้างอิงได้” พิสูจน์ข้างบนแล้วว่า key ของ for_each ต้องรู้ครบก่อน plan — key ที่มาจาก apply-time attribute ของ resource อื่นคือ error จริงที่มีชื่อเฉพาะ
  • “terraform test กับ mock_provider ทำนายพฤติกรรม replace-vs-update ของ provider จริงได้ถูกต้องเสมอ” ไม่ได้รับการยืนยันข้างบน — ที่จริงขัดแย้งกันด้วยซ้ำ: google_compute_instance.name มี ForceNew: true ในสคีมาจริง แต่การรัน mock_provider "google" {} เปล่า ๆ ที่เปลี่ยนแค่ name กลับ plan เป็น update in-place ใช้ local_file (หรือ override_resource ที่กำหนดรูปแบบ computed/planned ชัดเจน) สำหรับการพิสูจน์กลไก และอย่าเชื่อการจัดประเภท replace/update ของ mock_provider สำหรับ resource เฉพาะของ provider โดยไม่เช็คสคีมาด้วย
  • “null_resource ยังเป็นวิธีสมัยใหม่ในการเก็บ trigger โดยไม่มี infrastructure จริง” terraform_data (built-in ใน Terraform core ตั้งแต่ 1.4) มาแทนที่แล้ว ไม่ต้องมี provider แยก และยังเพิ่ม replace_triggered_by ซึ่ง null_resource ไม่เคยมี
  • count = length(var.some_list) แล้ว index เข้า var.some_list[count.index] สำหรับอะไรก็ตามที่อาจถูกลบรายการตรงกลางในอนาคต — พิสูจน์ข้างบนแล้วว่านี่คือรูปแบบที่ทำให้เกิด destroy-and-recreate cascade พอดี
  • resource ที่ generate มาอ้างอิง some_resource.name[0] แบบไม่มีเงื่อนไข ทั้งที่ count บน some_resource.name เป็นแบบมีเงื่อนไขจริง (var.enable ? 1 : 0) — one(some_resource.name[*].attr) คือทางแก้ พิสูจน์ข้างบนแล้ว
  • for_each ใช้ key จาก list index ที่แปลงเป็น string ทั้งที่ collection ข้างใต้มี key ที่มีความหมายอยู่แล้ว (ชื่อ, zone) — นี่เอาความเปราะบางของ count กลับมาพร้อม syntax ที่มากขึ้น
  • for_each บนค่าที่มาจาก computed attribute ของ resource อื่น (ID) โดยไม่เช็คว่า attribute นั้นรู้ก่อน apply หรือไม่ — พิสูจน์ข้างบนแล้วว่านี่คือ error จริงตอน plan ทันที
  • null_resource ยังโผล่ในโค้ดที่ generate สด ๆ สำหรับ resource ที่มีหน้าที่แค่ trigger — terraform_data คือตัวแทนสมัยใหม่ที่ไม่ต้องมี provider และเป็นตัวเดียวในสองตัวที่รองรับ replace_triggered_by
  • google_compute_instance block ที่ generate มาโดยไม่มี network_interface block เลย — นี่ไม่ใช่ตัวเลือก terraform validate จะปฏิเสธทันทีเพราะ network_interface เป็น block ที่จำเป็น ไม่ใช่ computed
  • โค้ดที่เชื่อ plan diff ของ terraform test แบบ mock_provider เพื่อพิสูจน์พฤติกรรม ForceNew ของ resource จริงของ provider — พิสูจน์ข้างบนแล้วว่าอาจผิดได้ ควรตรวจสอบกับสคีมา/เอกสารของ provider เองสำหรับสิ่งที่มีผลจริงต่อการทำงาน

🐛 หา bug ในโค้ดนี้

variable "instance_names" {
type = list(string)
default = ["web-a", "web-b", "web-c"]
}
resource "google_compute_instance" "web" {
count = length(var.instance_names)
name = var.instance_names[count.index]
machine_type = "e2-medium"
zone = "us-central1-a"
boot_disk {
initialize_params {
image = "debian-cloud/debian-12"
}
}
network_interface {
network = "default"
access_config {}
}
}
# Later: ops wants to remove "web-b" from the deploy
# variable "instance_names" { default = ["web-a", "web-c"] }
เฉลย

การลบ "web-b" ออกจากตรงกลางของ list นี้ ไม่ได้ แค่ destroy instance ของ web-b เท่านั้น พิสูจน์ข้างบนด้วยรูปแบบเดียวกันเป๊ะ (local_file แทน google_compute_instance กลไกหลักเหมือนกัน): index 1 ("web-b") ถูก destroy แล้วสร้างใหม่เป็น "web-c" และ index 2 ("web-c" ตัวเดิม) ถูก destroy ตรง ๆ โดยไม่มีอะไรมาแทนที่ —

Plan: 1 to add, 0 to change, 2 to destroy.

— ทั้งที่ในทางความคิดควรเป็นแค่ destroy เดียว ทางแก้คือ for_each บน toset(var.instance_names) (หรือ map ถ้าต้องการ config เฉพาะตัวเกินกว่าชื่อ): ลบ "web-b" แล้วจะ plan แค่ 1 to destroy เท่านั้น ไม่มีอะไรอื่นถูกแตะ

เกิดอะไรขึ้นจริง ๆ เมื่อลบรายการหนึ่งออกจากตรงกลางของ list ที่ขับเคลื่อน resource แบบ count
ทำไม for_each ถึงไม่มีปัญหา index-shifting เหมือน count
ความต่างระหว่าง for expression กับ for_each meta-argument ของ resource คืออะไร
resource instance ที่สร้างด้วย for_each บน map ถูกอ้างอิงจากภายนอกอย่างไร
รัน terraform plan กับโค้ดนี้จะเกิดอะไรขึ้น
resource "random_pet" "names" {
  count = 2
}

resource "google_compute_instance" "web" {
  for_each = toset(random_pet.names[*].id)
}
resource หนึ่งมี count = var.enable_x ? 1 : 0 และอีก resource อ้างอิง id ของมันด้วย one(that_resource[*].id) one() จะคืนค่าอะไรเมื่อ count ประเมินเป็น 0
terraform test ที่ใช้ mock_provider "google" {} (ไม่มี override_resource) แสดงว่า google_compute_instance.name เปลี่ยนเป็น update in-place ทั้งที่สคีมาจริงของ provider ระบุ name เป็น ForceNew นี่บอกอะไร
terraform_data เพิ่มอะไรเหนือกว่า null_resource แบบเก่า นอกจากไม่ต้องมี provider แยก