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

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 ที่ลบยิ่งอยู่ต้น ๆ

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 เป็นคนละแนวคิดที่บังเอิญมีชื่อคล้ายกับ 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
Removing a middle item shifts every count index, but only removes one for_each key
เกิดอะไรขึ้นเมื่อลบ item ออกจากกลาง list ที่ใช้กับ count บน resource
ทำไม for_each ถึงไม่มีปัญหา destroy-and-recreate แบบ cascade เหมือน count
for expression ต่างจาก for_each meta-argument อย่างไรทั้งที่ชื่อคล้ายกัน
ควรเลือก meta-argument ไหนสำหรับ logging sidecar resource สามตัวที่เกือบเหมือนกันแบบตายตัว โดยลำดับและ identity ที่คงที่ไม่สำคัญเลย