Count and For Each
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”count กับ for_each ต่างสร้าง resource หลาย copy จาก config block เดียว แต่ทั้งสองอ้างอิง copy เหล่านั้นต่างกัน — ด้วย numeric index กับด้วย stable key — และความต่างตรงนี้เองที่กำหนดว่าจะแก้ collection ด้านหลังได้ปลอดภัยแค่ไหนในภายหลัง
count: numeric index ต่อหนึ่ง copy
หัวข้อที่มีชื่อว่า “count: numeric index ต่อหนึ่ง copy”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 ไม่เปลี่ยนรูปร่างตรงกลาง
จุดอ่อนจริงของ count: ลบ item กลาง list
หัวข้อที่มีชื่อว่า “จุดอ่อนจริงของ count: ลบ item กลาง 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 ที่ลบยิ่งอยู่ต้น ๆ
for_each: stable key ต่อหนึ่ง instance
หัวข้อที่มีชื่อว่า “for_each: stable key ต่อหนึ่ง instance”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 ไม่สำคัญจริง ๆ เพราะทำให้ “ลบหนึ่งอย่าง” แปลว่า “ทำลายหนึ่งอย่าง” พอดี โดยที่อย่างอื่นไม่ถูกแตะเลย
for expression: แปลง collection ไม่ใช่ทำซ้ำ resource
หัวข้อที่มีชื่อว่า “for expression: แปลง collection ไม่ใช่ทำซ้ำ resource”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