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

Count and For Each

count กับ for_each ต่างสร้าง resource หลาย copy จาก config block เดียว แต่ทั้งสองอ้างอิง copy เหล่านั้นต่างกัน — ด้วย numeric index กับด้วย stable key — และความต่างตรงนี้เองที่กำหนดว่าจะแก้ collection ด้านหลังได้ปลอดภัยแค่ไหนในภายหลัง

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

variable "instance_names" {
type = list(string)
default = ["primary", "secondary", "worker"]
}
resource "aws_instance" "web" {
count = length(var.instance_names)
ami = var.ami_id
instance_type = "t3.micro"
tags = {
Name = var.instance_names[count.index]
}
}

โค้ดนี้สร้าง aws_instance.web[0] tag ว่า "primary", aws_instance.web[1] tag ว่า "secondary" และ aws_instance.web[2] tag ว่า "worker" ใช้งานได้ดีตราบใดที่ list ไม่เปลี่ยนรูปร่างตรงกลาง

ปัญหาเกิดขึ้นเฉพาะตอนที่ลบ item ออกจากกลาง var.instance_names สมมติลบ "secondary" ออก เหลือ ["primary", "worker"] ในทางความคิด ควรมีแค่เรื่องเดียวเกิดขึ้น คือทำลาย instance ที่ tag เป็น "secondary" แต่ Terraform ไม่ได้ track “instance ที่ tag เป็น secondary” แต่ track index 0, index 1, index 2 พอ list สั้นลง index 1 ตอนนี้กลายเป็นอ้างถึง "worker" แทน "secondary" และ index 2 ก็ไม่มีอยู่แล้วเลย plan ของ Terraform สะท้อน index ไม่ใช่ intent:

  • aws_instance.web[1] ถูกทำลาย (เดิมคือ "secondary") แล้วสร้างใหม่ด้วยค่าใหม่ที่ index นั้น ("worker")
  • aws_instance.web[2] ("worker" ตัวเดิม) ถูกทำลาย โดยไม่มีอะไรมาแทนที่

การลบหนึ่งอย่างในทางความคิด กลายเป็น cascade ของ destroy-and-recreate สำหรับทุก index หลังจากตัวที่ถูกลบ เพียงเพราะ scheme การอ้างอิงเป็นแบบ positional นี่คือ foot-gun ของ count ที่พบบ่อยที่สุด และยิ่งแย่ลงเมื่อ list ยิ่งยาวและ item ที่ลบยิ่งอยู่ต้น ๆ

พิสูจน์ด้วย count บน local_file (ไม่ต้องใช้ cloud เลยก็เห็นกลไกได้ชัด) — เริ่มจาก ["primary", "secondary", "worker"] แล้วลบ "secondary" ออก เหลือ ["primary", "worker"]:

$ terraform plan
# local_file.web[1] must be replaced
-/+ resource "local_file" "web" {
~ content = "secondary" -> "worker" # 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 = "worker" -> null
- filename = "./out-2.txt" -> null
}
Plan: 1 to add, 0 to change, 2 to destroy.

อ่านตามตัวอักษรเลย: index 1 (เดิมคือ "secondary") ถูกทำลายแล้วสร้างใหม่ด้วย content "worker" — resource ที่ในทางความคิดควรหมายถึง “instance ของ worker” ถูกทำลายแล้วสร้างใหม่โดยไม่มีเหตุผลอะไรนอกจากมันขยับไปอยู่ช่อง array อีกช่อง index 2 ("worker" ตัวเดิม ตอนนี้ไม่มีอะไรอยู่ที่ตำแหน่งนั้นแล้ว) ถูกทำลายไปเลยโดยไม่มีอะไรมาแทนที่ ลบ item เดียวจาก list สามตัวได้ผลลัพธ์เป็น 1 create + 2 destroy ไม่ใช่ 1 destroy อย่างที่ควรจะเป็น

for_each รับ map หรือ set ของ string มาแล้วสร้าง resource instance หนึ่งตัวต่อหนึ่ง key อ้างอิงด้วย key นั้นแทนที่จะเป็นตำแหน่ง — aws_instance.web["primary"] แทนที่จะเป็น aws_instance.web[0]

variable "instance_names" {
type = set(string)
default = ["primary", "secondary", "worker"]
}
resource "aws_instance" "web" {
for_each = var.instance_names
ami = var.ami_id
instance_type = "t3.micro"
tags = {
Name = each.key
}
}

ทีนี้ลบ "secondary" ออกจาก set มีแค่ aws_instance.web["secondary"] เท่านั้นที่ถูก plan ให้ทำลาย aws_instance.web["primary"] กับ aws_instance.web["worker"] ไม่ถูกแตะเลย เพราะ key ของแต่ละตัวไม่เคยขยับ Terraform เลยไม่มีเหตุผลต้องไปยุ่ง นี่คือเหตุผลที่ for_each เป็นตัวเลือก default ที่ดีกว่าสำหรับเกือบทุกกรณี ยกเว้นกรณีง่ายที่สุดที่ count คงที่และลำดับกับ identity ไม่สำคัญจริง ๆ เพราะทำให้ “ลบหนึ่งอย่าง” แปลว่า “ทำลายหนึ่งอย่าง” พอดี โดยที่อย่างอื่นไม่ถูกแตะเลย

พิสูจน์ด้วย scenario เดียวกันเป๊ะกับข้างบน แต่ใช้ for_each บน set(string) แทน count บน list(string):

$ terraform plan
# local_file.web["secondary"] will be destroyed
Plan: 0 to add, 0 to change, 1 to destroy.

ลบหนึ่ง item ได้ destroy หนึ่งตัวพอดี ไม่มี cascade เพราะ local_file.web["primary"] กับ local_file.web["worker"] ถูกอ้างอิงด้วย key ของตัวเอง ที่ไม่เคยขยับเลย

for_each บน set(string) (ข้างบน) ทำให้ each.key กับ each.value เหมือนกันเป๊ะ — key กับ value คือ string เดียวกัน แต่บน map(string, ...) ทั้งสองต่างกันจริง ๆ: each.key คือ map key ส่วน each.value คือค่าของ key นั้น

variable "instances" {
type = map(string)
default = {
primary = "t3.micro"
secondary = "t3.small"
}
}
resource "aws_instance" "web" {
for_each = var.instances
ami = var.ami_id
instance_type = each.value
tags = {
Name = each.key
}
}

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

$ cat out-primary.txt
instance primary has type t3.micro
$ cat out-secondary.txt
instance secondary has type t3.small

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

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

variable "enable_monitoring" {
type = bool
default = false
}
resource "aws_instance" "monitoring" {
count = var.enable_monitoring ? 1 : 0
ami = var.ami_id
instance_type = "t3.micro"
}
output "monitoring_instance_id" {
value = one(aws_instance.monitoring[*].id)
}

พิสูจน์ด้วยเวอร์ชัน local_file ที่เทียบเท่ากัน ทั้งสอง branch, config เดียวกัน:

$ 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() บน list ว่าง)
$ terraform apply -auto-approve -var="enable_monitoring=true" # เปิด flag
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
monitoring_file_id = "3ea3f9802accf8817bacd6f3df46a73b93ccddec"

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

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

resource "random_pet" "names" {
count = 2
}
resource "aws_instance" "web" {
for_each = toset(random_pet.names[*].id)
ami = var.ami_id
instance_type = "t3.micro"
}

error จริง (พิสูจน์ด้วยเวอร์ชัน local_file ที่เทียบเท่ากัน — กลไกเหมือนกันทุกประการไม่ว่า resource type ไหน):

$ 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 (ชื่อที่คุณตั้งเอง, index, string ของ AZ จาก variable) แล้วให้แค่ฝั่ง value แบก apply-time result เท่านั้น

null_resource (จาก provider แยก hashicorp/null) เคยเป็นวิธีมาตรฐานในการรัน provisioner หรือเก็บ map triggers โดยไม่มี 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.

replace_triggered_by คือกลไกที่ pattern “restart สิ่งนี้เมื่อ config ของมันเปลี่ยน” ต้องการพอดี — ถ้าไม่มีมัน terraform_data.restart_app จะไม่มีเหตุผลถูกแตะเลย เพราะ argument ของตัวเองไม่ได้อ้างถึงอะไรที่เปลี่ยนในแบบที่จะ force replacement ตามปกติ

เอา for_each มาใช้ในแบบที่ใช้งานจริง ๆ: subnet หนึ่งตัวต่อหนึ่ง availability zone key ด้วยชื่อ AZ เพื่อให้เพิ่มหรือลบ AZ ไม่กระทบตัวอื่นเลย:

variable "az_names" {
type = set(string)
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "private" {
for_each = { for idx, az in tolist(var.az_names) : az => idx }
vpc_id = aws_vpc.main.id
availability_zone = each.key
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, each.value)
tags = {
Name = "private-${each.key}"
}
}

terraform plan จริงกับ aws provider จริง (mock credentials ไม่แตะ network เลย):

$ terraform plan
# aws_vpc.main will be created
+ resource "aws_vpc" "main" {
+ cidr_block = "10.0.0.0/16"
...
}
Plan: 4 to add, 0 to change, 0 to destroy.

resource สี่ตัว: VPC หนึ่งตัวบวก subnet สามตัว ตัวละหนึ่ง AZ ใน var.az_names expression for idx, az in tolist(var.az_names) : az => idx แปลง set ให้เป็น map (ชื่อ AZ → index) ซึ่งคือสิ่งที่ทำให้ each.value ป้อนเข้า argument ตัวที่สองของ cidrsubnet ได้ ในขณะที่ each.key ยังเป็นชื่อ AZ ที่ stable และอ่านง่าย ใช้ทั้งอ้างอิงและใส่ใน tag Name

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

locals {
upper_names = [for name in var.instance_names : upper(name)]
name_to_id = { for k, v in aws_instance.web : k => v.id }
}

[for name in var.instance_names : upper(name)] รับ list มาแล้วผลิต list ใหม่ หนึ่ง element ที่แปลงแล้วต่อหนึ่ง element ที่รับเข้ามา {for k, v in aws_instance.web : k => v.id} รับ map ของ resource instance ที่ for_each สร้างไว้ มาผลิตเป็น plain map จากชื่อไปเป็น instance ID ทั้งสองบรรทัดนี้ไม่ได้สร้างหรือทำลาย resource สักตัวเดียว เป็น data transformation ล้วน ๆ ที่เอาไปใช้ใน local, output หรือ default ของ variable ได้ คำว่า “for” ที่ซ้ำกันทำให้คนใหม่ ๆ สับสนบ่อยมาก for_each meta-argument ทำซ้ำ resource block หนึ่งครั้งต่อหนึ่ง element ของ collection ส่วน for expression แปลง content ของ collection ให้เป็น collection ใหม่ ทั้งสองคือ feature คนละตัวที่บังเอิญสะกดคล้ายกันเท่านั้น

flowchart TB
  subgraph count["count: index-based"]
    c0["index 0 primary"] --> c1["index 1 destroyed then recreated as worker"]
    c1 --> c2["index 2 destroyed, no replacement"]
  end
  subgraph foreach["for_each: key-based"]
    f0["key primary untouched"]
    f1["key secondary destroyed"]
    f2["key worker untouched"]
  end
การลบรายการตรงกลางทำให้ index ของ count เลื่อนหมด แต่ for_each ลบแค่ key เดียว

count กับ for_each ตอบคำถาม “กี่ copy” เหมือนกัน แต่ encode identity ต่างกันโดยสิ้นเชิง: count ได้ identity มาจาก ตำแหน่งใน list (เปราะบาง — ตำแหน่งขยับทุกครั้งที่รูปร่าง list เปลี่ยน) ส่วน for_each ได้ identity มาจาก stable key ที่มีความหมายอยู่แล้ว (ชื่อ, AZ, map key ที่คุณเลือกเอง) — ทนทานกว่า เพราะการลบสิ่งหนึ่งไม่มีทางไป rename อีกสิ่งหนึ่ง proof ทุกอันข้างบนคือผลลัพธ์ของความต่างข้อเดียวนี้ — index-shift cascade, for_each unknown-value error (Terraform ต้องรู้ทุก key ล่วงหน้า เพราะ key คือ identity เป๊ะ ๆ), และ replace_triggered_by (กลไก trigger การ replace ที่รักษา identity แบบตั้งใจ ตามเงื่อนไขของคุณเอง ไม่ใช่เกิดขึ้นโดยบังเอิญผ่าน index ที่ขยับ)

ตัวเลือกได้อะไรเสียอะไร
countsyntax ง่ายที่สุดสำหรับจำนวน resource ที่เกือบเหมือนกันแบบตายตัวจริง ๆ ที่ลำดับไม่สำคัญลบ item กลางลิสต์ทำให้เกิด cascade destroy-and-recreate ข้าม index ทุกตัวหลังจากนั้น — พิสูจน์ข้างบนด้วย plan diff จริง
for_each (set)ลบ key หนึ่งตัวทำลายแค่ resource ตัวนั้น — พิสูจน์ข้างบนต้องแปลง list เป็น set (หรือหาทางให้มี stable key) ถ้าเริ่มจากข้อมูลที่มีลำดับ
for_each (map)each.key/each.value แยกกันให้แต่ละ instance มี config ต่างกันได้จริง — พิสูจน์ข้างบนทุก key ต้องรู้ก่อน plan — พิสูจน์ข้างบนว่า key ที่มาจาก computed attribute ของ resource อื่นคือ error จริง เข้มงวด
terraform_data + replace_triggered_byบังคับ replacement แบบตั้งใจตาม external signal โดยไม่ต้องเพิ่ม argument ปลอมเพื่อลักลอบสร้าง dependency — พิสูจน์ข้างบนด้วย plan จริงreplacement เป็นแบบเต็ม (destroy + recreate) ไม่ใช่ partial update — เหมาะกับ “restart” ไม่เหมาะกับอะไรที่ควร update in place
  • “ลบ item ออกจาก list ที่ใช้กับ count กระทบแค่ resource ของ item นั้น” พิสูจน์ข้างบน: มัน cascade index ทุกตัวหลังจากตัวที่ถูกลบขยับ และ Terraform อ่านการขยับนั้นเป็น destroy-and-recreate ของแต่ละ index ที่ขยับ ไม่ใช่การลบสะอาด ๆ ครั้งเดียว
  • “for_each กับ for expression เป็น feature เดียวกันโดยพื้นฐาน เพราะมีคำว่า ‘for’ เหมือนกัน” ไม่เกี่ยวกันเลย: for_each คือ resource meta-argument ที่ทำซ้ำ resource block ส่วน for expression แปลงข้อมูลของ collection หนึ่งไปเป็นอีก collection และไม่สร้างหรือทำลายอะไรเลย
  • “อ้างอิง aws_instance.monitoring[0] ได้อย่างปลอดภัยแม้ count อาจเป็น 0” พิสูจน์ข้างบน: มัน error ทันทีที่ count evaluate เป็น 0 เพราะไม่มี index 0 one(aws_instance.monitoring[*].id) คือ form ที่ปลอดภัย — null เมื่อว่าง, element เดียวเมื่อมีตัวเดียว และมีแค่ 2+ element เท่านั้นที่ error จริง
  • “for_each ใช้ key จาก computed value อะไรก็ได้ เหมือนที่ argument ปกติอ้างอิงได้” พิสูจน์ข้างบน: key ของ for_each ต้องรู้ครบก่อน plan key ที่มาจาก apply-time attribute ของ resource อื่นคือ error จริง มีชื่อชัดเจน พร้อม error text ของ Terraform เองแนะนำทางแก้ (static key, apply-time value เฉพาะฝั่ง value)
  • “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] สำหรับอะไรก็ตามที่อาจมี item ถูกลบออกจากกลางในภายหลัง — พิสูจน์ข้างบนว่านี่คือรูปแบบเป๊ะ ๆ ที่ทำให้เกิด destroy-and-recreate cascade
  • resource ที่ generate มาอ้างอิง some_resource.name[0] แบบไม่มีเงื่อนไข ทั้งที่ count บน some_resource.name จริง ๆ เป็น conditional (var.enable ? 1 : 0) — พังทันทีที่ flag เป็น false; one(some_resource.name[*].attr) คือทางแก้ พิสูจน์ข้างบน
  • for_each ที่ key มาจาก list index ที่แปลงเป็น string (for_each = toset([for i in range(3) : tostring(i)])) ทั้งที่ collection ด้านหลังมี key ที่มีความหมายจริง ๆ อยู่แล้ว (ชื่อ, AZ) — นี่คือการนำความเปราะบางของ count กลับมาด้วย syntax ที่ซับซ้อนขึ้น ไม่ใช่ประโยชน์จริงของ for_each
  • for_each บนค่าที่มาจาก computed attribute ของ resource อื่น (ID, ARN) โดยไม่เช็คก่อนว่า attribute นั้นรู้ก่อน apply หรือเปล่า — พิสูจน์ข้างบนว่านี่คือ error จริงทันทีตอน plan ไม่ใช่อะไรที่ fail แบบเงียบ ๆ
  • null_resource ที่ยังโผล่มาในโค้ดที่ generate ใหม่สำหรับ resource ที่มีไว้แค่ trigger — terraform_data คือตัวแทนปัจจุบันที่ไม่ต้องมี provider และเป็นตัวเดียวในสองตัวที่รองรับ replace_triggered_by
  • ค่า replace_triggered_by ที่ชี้ไปที่ argument ซึ่งเป็นส่วนหนึ่งของ config ของ resource เองอยู่แล้ว (ซ้ำซ้อน — argument เปลี่ยนก็ trigger update หรือ replacement ตาม schema ของ argument นั้นอยู่แล้ว) แทนที่จะเป็น external signal ที่ resource ไม่มีทางอื่นให้ react

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

variable "instance_names" {
type = list(string)
default = ["primary", "secondary", "worker"]
}
resource "aws_instance" "web" {
count = length(var.instance_names)
ami = var.ami_id
instance_type = "t3.micro"
tags = {
Name = var.instance_names[count.index]
}
}
# Later: ops wants to remove "secondary" from the deploy
# variable "instance_names" { default = ["primary", "worker"] }
เฉลย

การลบ "secondary" ออกจากกลาง list นี้ไม่ได้แค่ทำลาย instance ของ “secondary” พิสูจน์ข้างบนด้วยรูปร่างเดียวกันเป๊ะ (local_file แทน aws_instance กลไกเหมือนกัน): index 1 ("secondary") ถูกทำลายแล้วสร้างใหม่เป็น "worker" และ index 2 ("worker" ตัวเดิม) ถูกทำลายไปเลยโดยไม่มีอะไรมาแทนที่ —

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

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

เกิดอะไรขึ้นเมื่อลบ item ออกจากกลาง list ที่ใช้กับ count บน resource
ทำไม for_each ถึงไม่มีปัญหา destroy-and-recreate แบบ cascade เหมือน count
for expression ต่างจาก for_each meta-argument อย่างไรทั้งที่ชื่อคล้ายกัน
ควรเลือก meta-argument ไหนสำหรับ logging sidecar resource สามตัวที่เกือบเหมือนกันแบบตายตัว โดยลำดับและ identity ที่คงที่ไม่สำคัญเลย
รัน terraform plan กับโค้ดนี้จะเกิดอะไรขึ้น
resource "random_pet" "names" {
  count = 2
}

resource "local_file" "web" {
  for_each = toset(random_pet.names[*].id)
  filename = "out-${each.key}.txt"
}
resource ตัวหนึ่งมี count = var.enable_x ? 1 : 0 และ resource อีกตัวอ้างอิง id ด้วย one(that_resource[*].id) one() จะ return อะไรเมื่อ count evaluate เป็น 0
terraform_data เพิ่มอะไรเหนือ null_resource รุ่นเก่า นอกจากไม่ต้องมี provider แยก