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

Count and For Each

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

count รับจำนวนเต็มแล้วสร้าง resource ที่แทบจะเหมือนกันตามจำนวนนั้น โดย index ตั้งแต่ 0 ถึง count - 1 ภายใน resource count.index บอกตำแหน่งของชุดปัจจุบัน ส่วนภายนอก แต่ละชุดถูก address ด้วย numeric 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] แต่ละอันถูก address จากภายนอกด้วย 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" ทีนี้ลบ "web-b" ออกจากกลางๆ list เหลือ ["web-a", "web-c"] Terraform ไม่เข้าใจว่า "web-b" ถูกลบไปโดยเฉพาะ เห็นแค่ว่า index 1 ตอนนี้ map ไปหาชื่อที่ต่างจากเดิม และ index 2 ไม่มีอยู่แล้ว plan ที่ Terraform สร้างออกมาคือ destroy web[2] ("web-c" ตัวเก่าไม่มี index แล้ว) แล้ว destroy กับ recreate web[1] ด้วยชื่อใหม่ "web-c" ทั้งที่ในความเป็นจริงมีแค่ "web-b" ที่ควรถูกแตะต้อง ทุก resource ที่ index เลื่อนจะโดน destroy แล้ว recreate พร้อม downtime, IP address ที่เปลี่ยน, และ disk ที่หายไปตามมา แค่เพราะขยับตำแหน่งใน list

for_each รับ map หรือ set ของ string แล้วสร้าง resource instance หนึ่งชุดต่อหนึ่ง key โดย address ด้วย key นั้นแทนที่จะเป็น numeric 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 ถูก address จากภายนอกเป็น google_compute_instance.web["primary"], web["secondary"], และ web["batch"] ภายใน resource each.key คือ key ของ map และ each.value คือค่าที่ผูกกับ key นั้น ทีนี้ลบ "secondary" ออกจาก map ทั้งหมด Terraform destroy แค่ google_compute_instance.web["secondary"] เท่านั้นและไม่แตะอะไรอื่นเลย — web["primary"] กับ web["batch"] ถูก key ด้วยชื่อของตัวเอง ไม่ใช่ตำแหน่ง เลยไม่ถูกกระทบเลยแม้ key กลางๆ จะหายไป

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 อะไร address แต่ละอัน การใช้ for expression สร้าง map แล้วส่งต่อให้ for_each ของ resource เป็นเรื่องปกติมากและเจอบ่อย

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
count shifts every index after a mid-list removal; for_each only touches the removed key
เกิดอะไรขึ้นกันแน่เมื่อลบ item กลาง list ที่ขับเคลื่อน resource แบบ count
ทำไม for_each ถึงหลีกเลี่ยงปัญหา index เลื่อนที่ count มี
for expression ต่างจาก for_each resource meta-argument อย่างไร
resource instance แต่ละตัวถูก address อย่างไรเมื่อใช้ for_each กับ map