Count and For Each
แนวคิดในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “แนวคิดในหนึ่งประโยค”count และ for_each ต่างก็สร้าง resource มากกว่าหนึ่งชุดจาก block เดียว แต่อ้างอิงแต่ละชุดต่างกันโดยพื้นฐาน และความต่างนั้นตัดสินว่าจะเพิ่มหรือลบทีหลังได้อย่างปลอดภัยแค่ไหน
count — meta-argument แบบตัวเลข
หัวข้อที่มีชื่อว่า “count — meta-argument แบบตัวเลข”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] คือชุดที่สอง และต่อไปเรื่อย ๆ
กับดัก index-shifting
หัวข้อที่มีชื่อว่า “กับดัก index-shifting”จุดอ่อนที่แท้จริงของ 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 — อ้างอิงด้วย key ที่คงที่ ไม่ใช่ตำแหน่ง
หัวข้อที่มีชื่อว่า “for_each — อ้างอิงด้วย key ที่คงที่ ไม่ใช่ตำแหน่ง”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 ตรงกลางหายไป
for_each บน map: each.key กับ each.value ต่างกันจริง
หัวข้อที่มีชื่อว่า “for_each บน map: each.key กับ each.value ต่างกันจริง”บน 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.txtinstance primary has type e2-medium$ cat out-batch.txtinstance batch has type e2-standard-2each.key ตั้งชื่อ instance ส่วน each.value แบกค่า config เฉพาะตัวของมัน — รูปแบบที่ต้องใช้เมื่อ instance แต่ละตัวของ resource เดียวกันต้องการ argument เฉพาะตัวจริง ๆ ไม่ใช่แค่ชื่อที่ต่างกัน
Conditional count และ one()
หัวข้อที่มีชื่อว่า “Conditional count และ one()”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 onApply complete! Resources: 1 added, 0 changed, 0 destroyed.Outputs:monitoring_file_id = "3ea3f9802accf8817bacd6f3df46a73b93ccddec"one([]) คืน null โดยไม่ error, one([x]) คืน x มีแค่กรณีสองรายการขึ้นไปเท่านั้นที่ error จริง ซึ่งเกิดขึ้นไม่ได้ในที่นี้เพราะ count ถูกจำกัดไว้ที่ 0 หรือ 1 เท่านั้น
for_each ใช้ key จากค่าที่รู้หลัง apply ไม่ได้
หัวข้อที่มีชื่อว่า “for_each ใช้ key จากค่าที่รู้หลัง apply ไม่ได้”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
terraform_data แทน null_resource และ replace_triggered_by
หัวข้อที่มีชื่อว่า “terraform_data แทน null_resource และ replace_triggered_by”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_byPlan: 2 to add, 0 to change, 2 to destroy.ตัวอย่าง subnetwork ต่อ key จริง
หัวข้อที่มีชื่อว่า “ตัวอย่าง subnetwork ต่อ key จริง”การใช้ 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 อีกครั้ง:
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 destroyedPlan: 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 — แปลง collection ไม่ใช่ resource meta-argument
หัวข้อที่มีชื่อว่า “for expression — แปลง collection ไม่ใช่ resource meta-argument”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 Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”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 ไหน
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ตัวเลือก | ข้อดี | ต้นทุน |
|---|---|---|
count | Syntax ง่ายที่สุดสำหรับจำนวน 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กับforexpression เป็นฟีเจอร์เดียวกันเพราะใช้คำว่า ‘for’ ร่วมกัน” ทั้งสองไม่เกี่ยวข้องกันเลย:for_eachคือ resource meta-argument ที่ทำซ้ำ resource block ส่วนforexpression แปลงข้อมูลของ collection หนึ่งเป็นอีก collection หนึ่ง ไม่สร้างหรือทำลายอะไรเลย - “อ้างอิง
google_compute_instance.monitoring[0]ได้อย่างปลอดภัยแม้countอาจเป็น0” พิสูจน์ข้างบนแล้วว่านี่ error ทันทีที่countประเมินเป็น0one(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ไม่เคยมี
อ่านโค้ดที่ AI สร้างให้
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้างให้”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_bygoogle_compute_instanceblock ที่ generate มาโดยไม่มีnetwork_interfaceblock เลย — นี่ไม่ใช่ตัวเลือก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 เท่านั้น ไม่มีอะไรอื่นถูกแตะ