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

Count and For Each

count และ for_each เป็น meta-argument ที่สร้าง resource block เดียวกันได้หลาย instance โดย count อ้างอิงแต่ละชุดด้วย index ตัวเลข ส่วน for_each อ้างอิงด้วย key ที่คงที่ ในขณะที่ for expression เป็นคนละ syntax ที่ไม่เกี่ยวข้องกันเลย ใช้แปลง collection หนึ่งให้เป็นอีก collection หนึ่งเท่านั้น

count: สำเนาที่แทบเหมือนกันทุกประการ อ้างอิงด้วย index

หัวข้อที่มีชื่อว่า “count: สำเนาที่แทบเหมือนกันทุกประการ อ้างอิงด้วย index”

count รับจำนวนเต็มแล้วสร้าง resource ตามจำนวนนั้น นับจาก 0 ถึง count - 1 ภายใน resource block count.index เก็บหมายเลขของ instance ปัจจุบัน ซึ่งมักใช้เลือก element จาก list ที่ขนานกันอยู่ ภายนอก resource แต่ละ instance ถูกอ้างอิงด้วย [index] — azurerm_linux_virtual_machine.web[0], azurerm_linux_virtual_machine.web[1] และต่อไป

variable "vm_names" {
type = list(string)
description = "Names for the web tier virtual machines, in order"
default = ["web-a", "web-b", "web-c"]
}
resource "azurerm_linux_virtual_machine" "web" {
count = length(var.vm_names)
name = "${var.environment}-${var.vm_names[count.index]}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
size = "Standard_D2s_v3"
admin_username = "azureuser"
network_interface_ids = [
azurerm_network_interface.web[count.index].id,
]
admin_ssh_key {
username = "azureuser"
public_key = file("~/.ssh/id_rsa.pub")
}
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
}

เมื่อ var.vm_names เป็นค่า default บล็อกนี้สร้าง virtual machine สามตัว: azurerm_linux_virtual_machine.web[0] ชื่อ web-a, web[1] ชื่อ web-b และ web[2] ชื่อ web-c count.index คือสิ่งที่ทำให้แต่ละชุดต่างกันเล็กน้อย — ชื่อที่ต่างกันในตัวอย่างนี้ และในคอนฟิกจริงมักเป็น network interface, disk หรือ IP address ที่ต่างกันด้วย

จุดอ่อนของ count: ลบรายการตรงกลางแล้ว index ที่เหลือทั้งหมดเลื่อน

หัวข้อที่มีชื่อว่า “จุดอ่อนของ count: ลบรายการตรงกลางแล้ว index ที่เหลือทั้งหมดเลื่อน”

count อ้างอิง instance ด้วยตำแหน่งใน list ล้วน ๆ และนั่นคือจุดที่มันอันตราย ลบรายการหนึ่งออกจากตรงกลางของ var.vm_names แล้วทุก element หลังจากนั้นจะเลื่อนตำแหน่งลงหนึ่งขั้น — Terraform ไม่มีทางรู้ว่า “index 1” ตอนนี้หมายถึงชื่ออื่นแล้ว มันเห็นแค่ว่าค่าที่ index 1 เปลี่ยน และค่าที่ index 2 หายไป

Terminal window
# Before: vm_names = ["web-a", "web-b", "web-c"]
# azurerm_linux_virtual_machine.web[0] -> web-a
# azurerm_linux_virtual_machine.web[1] -> web-b
# azurerm_linux_virtual_machine.web[2] -> web-c
# Remove "web-b" from the middle: vm_names = ["web-a", "web-c"]
# azurerm_linux_virtual_machine.web[0] -> web-a (unchanged, untouched)
# azurerm_linux_virtual_machine.web[1] -> web-c (was web-b; name changed -> destroy and recreate)
# azurerm_linux_virtual_machine.web[2] (index no longer exists -> destroy)

ในทางความคิด ควรมีแค่ virtual machine เดียวที่ถูกลบ แต่สิ่งที่ Terraform วางแผนจริงคือ destroy-and-recreate ของ web[1] (เพราะ attribute name ที่ index นั้นเปลี่ยน และ name บังคับให้ replace) และ destroy ตรง ๆ ของ web[2] (เพราะ list สั้นลงและ index นั้นไม่มีอยู่แล้ว) — สองการเปลี่ยนแปลงที่รบกวนระบบ เพื่อลบแค่หนึ่งรายการ แถมเครื่อง web-c ที่ยังอยู่ก็ถูกสร้างใหม่ทั้งที่ไม่มีอะไรควรเปลี่ยนเลย นี่คือกับดักที่พบบ่อยที่สุดของ count และยิ่งแย่ลงถ้ารายการที่ลบอยู่ลึกเข้าไปใน list เพราะทุกอย่างหลังจากนั้นเลื่อนหมด

พิสูจน์ด้วย count บน local_file (กลไกเดียวกันเป๊ะ ไม่ต้องมี Azure credentials ก็เห็นได้) — เริ่มจาก ["web-a", "web-b", "web-c"] แล้วลบ "web-b" ออก เหลือ ["web-a", "web-c"]:

$ 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

for_each รับ map หรือ set ของ string แล้วสร้าง resource instance หนึ่งตัวต่อหนึ่ง key อ้างอิงด้วย key แทนตำแหน่งตัวเลข — azurerm_linux_virtual_machine.web["primary"] แทน web[0] each.key และ each.value ใช้ได้ภายใน resource block และสำหรับ set(string) key กับ value คือ string เดียวกัน

variable "web_server_roles" {
type = set(string)
description = "Stable role names for the web tier virtual machines"
default = ["primary", "secondary", "dr"]
}
resource "azurerm_linux_virtual_machine" "web" {
for_each = var.web_server_roles
name = "${var.environment}-web-${each.key}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
size = "Standard_D2s_v3"
admin_username = "azureuser"
network_interface_ids = [
azurerm_network_interface.web[each.key].id,
]
admin_ssh_key {
username = "azureuser"
public_key = file("~/.ssh/id_rsa.pub")
}
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
}

ลอง remove "secondary" ออกจาก var.web_server_roles Terraform จะ destroy แค่ azurerm_linux_virtual_machine.web["secondary"] เท่านั้น ไม่มีอะไรอื่น — web["primary"] และ web["dr"] ถูกอ้างอิงด้วย key ของตัวเอง ซึ่งไม่เคยขยับ Terraform เลยไม่มีเหตุผลจะไปแตะต้องเลย ไม่มีการเลื่อนเพราะไม่มีอะไรที่เป็น positional ให้เลื่อน นี่คือเหตุผลที่ for_each เป็นตัวเลือกที่ปลอดภัยกว่าสำหรับ resource กลุ่มที่อาจโตหรือหดจากตรงกลาง ส่วน count ควรเก็บไว้ใช้เมื่อต้องการสำเนาที่เหมือนกัน N ชุดจริง ๆ โดยไม่มี identity ที่มีความหมายเกินกว่าตัวเลข

พิสูจน์ด้วยสถานการณ์เดียวกันเป๊ะ ใช้ 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.

ลบหนึ่งรายการ วางแผน destroy หนึ่งรายการ — ไม่มีการไล่ลามใด ๆ

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

variable "vm_sizes" {
type = map(string)
description = "VM size per role name"
default = {
primary = "Standard_D2s_v3"
secondary = "Standard_D2s_v3"
dr = "Standard_D4s_v3"
}
}
resource "azurerm_linux_virtual_machine" "web" {
for_each = var.vm_sizes
name = "${var.environment}-web-${each.key}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
size = each.value
admin_username = "azureuser"
network_interface_ids = [
azurerm_network_interface.web[each.key].id,
]
admin_ssh_key {
username = "azureuser"
public_key = file("~/.ssh/id_rsa.pub")
}
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
}

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

$ cat out-primary.txt
vm primary has size Standard_D2s_v3
$ cat out-dr.txt
vm dr has size Standard_D4s_v3

each.key ตั้งชื่อ instance (ใช้กับ name ของ VM และการค้นหา network interface) ส่วน each.value แบกขนาดที่ต่างกันเฉพาะสำหรับ role dr

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

variable "enable_monitoring" {
type = bool
default = false
}
resource "azurerm_linux_virtual_machine" "monitoring" {
count = var.enable_monitoring ? 1 : 0
# ... same shape as above
}
output "monitoring_vm_id" {
value = one(azurerm_linux_virtual_machine.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 on
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
monitoring_file_id = "3ea3f9802accf8817bacd6f3df46a73b93ccddec"

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

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

resource "random_pet" "names" {
count = 2
}
resource "azurerm_linux_virtual_machine" "web" {
for_each = toset(random_pet.names[*].id)
# ...
}

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

$ 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.
╵

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 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.

การใช้ for_each ตามที่มันถูกใช้จริง: หนึ่ง subnet ต่อหนึ่งชื่อ อ้างอิงด้วย key เพื่อให้เพิ่มหรือลบ subnet ไม่รบกวน subnet อื่นเลย พิสูจน์ด้วย terraform test และ mock_provider "azurerm" {} เพราะ plan จริงต้องใช้ Azure credentials ที่คอร์สนี้ไม่มี:

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 "azurerm_virtual_network" "main" {
name = "vnet-demo"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
address_space = ["10.0.0.0/16"]
}
resource "azurerm_subnet" "this" {
for_each = var.subnets
name = each.key
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.main.name
address_prefixes = [each.value]
}

รัน terraform test จริง: สร้างทั้งสามตัว แล้วลบ "app" ออกจาก map แล้ว plan อีกครั้ง:

tests/removal.tftest.hcl
mock_provider "azurerm" {}
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
# azurerm_subnet.this["app"] will be destroyed
Plan: 0 to add, 0 to change, 1 to destroy.
Success! 2 passed, 0 failed.

azurerm_subnet.this["app"] ถูก destroy พอดี — web และ db ไม่ปรากฏใน plan เลย เพราะ key ของมันไม่เคยขยับ

for expression เป็น syntax แยกต่างหากที่บังเอิญใช้ชื่อคล้าย for_each และการสับสนระหว่างสองอย่างนี้เป็นความผิดพลาดที่เกิดง่าย for_each (และ count) เป็น resource meta-argument ที่ตัดสินใจว่าจะสร้าง resource กี่ instance for expression เป็น comprehension ทั่วไป — รับ list หรือ map ที่มีอยู่แล้วแล้วสร้าง list หรือ map ใหม่ ใช้ได้ทุกที่ที่ expression ใช้ได้: ใน local, output, ค่า default ของ variable หรือ argument ของฟังก์ชัน

locals {
# [for x in list : expr] transforms a list into a new list
vm_names_upper = [for name in var.vm_names : upper(name)]
# {for k, v in map : k => expr} transforms a map into a new map
role_to_vm_id = {
for role, vm in azurerm_linux_virtual_machine.web : role => vm.id
}
# a for expression can filter with an if clause too
prod_only_roles = [for role in var.web_server_roles : role if role != "dr"]
}

local.vm_names_upper ไม่เกี่ยวอะไรกับจำนวน virtual machine ที่ถูกสร้างเลย มันแค่แปลงทุก string ใน var.vm_names ให้เป็นตัวพิมพ์ใหญ่แล้วคืน list ใหม่ ใช้ได้ทุกที่ที่ต้องการค่าที่แปลงแล้วแทนค่าดั้งเดิม local.role_to_vm_id ทำงานแบบเดียวกันสำหรับ map จับคู่แต่ละ for_each key จาก azurerm_linux_virtual_machine.web กับ id ของ instance นั้น ซึ่งเป็นวิธีทั่วไปในการส่ง mapping role -> id ที่สะอาดให้ output หรือ resource อื่น ทั้งหมดนี้ไม่ได้สร้างหรือทำลาย resource instance เลย มันแค่จัดรูปแบบข้อมูลที่มีอยู่แล้วใหม่ resource-level for_each ที่เห็นข้างบนกับ for expression พวกนี้บังเอิญใช้ตัวอักษรสี่ตัวเหมือนกัน แต่แก้ปัญหาคนละเรื่องกันโดยสิ้นเชิง

flowchart TB
  subgraph countMeta["count: indexed by position"]
    cl["list: web-a, web-b, web-c"] --> c0["index 0 = web-a"]
    cl --> c1["index 1 = web-b"]
    cl --> c2["index 2 = web-c"]
    cr["remove web-b from the middle"] --> cs1["index 1 now web-c: destroy and recreate"]
    cr --> cs2["index 2 no longer exists: destroy"]
  end
  subgraph forEachMeta["for_each: indexed by stable key"]
    fs["set: primary, secondary, dr"] --> f0["key primary"]
    fs --> f1["key secondary"]
    fs --> f2["key dr"]
    fr["remove secondary"] --> fs1["key secondary destroyed only"]
    fr --> fs2["key primary and key dr untouched"]
  end
การลบรายการตรงกลางทำให้ index ของ count เลื่อนหมด แต่ for_each เหลือทุก key อื่นไว้เหมือนเดิม

count และ for_each ตอบคำถาม “กี่สำเนา” เหมือนกัน แต่เข้ารหัส identity ต่างกันโดยสิ้นเชิง: count ได้ identity จาก ตำแหน่งใน list (เปราะบาง — ตำแหน่งเลื่อนทุกครั้งที่รูปร่างของ list เปลี่ยน) ในขณะที่ for_each ได้ identity จาก key ที่คงที่ซึ่งมีความหมายอยู่แล้ว (ชื่อ, role, map key ที่คุณเลือกเอง) — ทนทานกว่า เพราะลบสิ่งหนึ่งจะไม่ทำให้อีกสิ่งเปลี่ยนชื่อ ทุกการพิสูจน์ข้างบนคือผลจากความต่างข้อเดียวนี้

ตัวเลือกข้อดีต้นทุน
countSyntax ง่ายที่สุดสำหรับจำนวน resource ที่แทบเหมือนกันคงที่จริง ๆ และลำดับไม่สำคัญลบรายการตรงกลางทำให้เกิด destroy-and-recreate ไล่ตามทุก index หลังจากนั้น — พิสูจน์ข้างบนด้วย plan diff จริง
for_each (set)ลบ key เดียว destroy แค่ resource เดียว — พิสูจน์ข้างบนด้วย subnetต้องแปลง list เป็น set (หรือหา stable key เอง) ถ้าเริ่มจากข้อมูลที่มีลำดับ
for_each (map)each.key/each.value แยกกันทำให้แต่ละ instance มี config เฉพาะตัวได้จริง — พิสูจน์ข้างบนด้วยขนาด VM ต่อ roleทุก 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 กับ for expression เป็นฟีเจอร์เดียวกันเพราะใช้คำว่า ‘for’ ร่วมกัน” ทั้งสองไม่เกี่ยวข้องกันเลย: for_each คือ resource meta-argument ที่ทำซ้ำ resource block ส่วน for expression แปลงข้อมูลของ collection หนึ่งเป็นอีก collection หนึ่ง ไม่สร้างหรือทำลายอะไรเลย
  • “อ้างอิง azurerm_linux_virtual_machine.monitoring[0] ได้อย่างปลอดภัยแม้ count อาจเป็น 0” พิสูจน์ข้างบนแล้วว่านี่ error ทันทีที่ count ประเมินเป็น 0 one(azurerm_linux_virtual_machine.monitoring[*].id) คือรูปแบบที่ปลอดภัย
  • “for_each ใช้ key จากค่า computed อะไรก็ได้ เหมือนที่ argument ธรรมดาอ้างอิงได้” พิสูจน์ข้างบนแล้วว่า key ของ for_each ต้องรู้ครบก่อน plan — key ที่มาจาก apply-time attribute ของ resource อื่นคือ error จริงที่มีชื่อเฉพาะ
  • “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] สำหรับอะไรก็ตามที่อาจถูกลบรายการตรงกลางในอนาคต — พิสูจน์ข้างบนแล้วว่านี่คือรูปแบบที่ทำให้เกิด 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 ที่มีความหมายอยู่แล้ว (ชื่อ, role) — นี่เอาความเปราะบางของ count กลับมาพร้อม syntax ที่มากขึ้น
  • for_each บนค่าที่มาจาก computed attribute ของ resource อื่น (ID) โดยไม่เช็คว่า attribute นั้นรู้ก่อน apply หรือไม่ — พิสูจน์ข้างบนแล้วว่านี่คือ error จริงตอน plan ทันที
  • null_resource ยังโผล่ในโค้ดที่ generate สด ๆ สำหรับ resource ที่มีหน้าที่แค่ trigger — terraform_data คือตัวแทนสมัยใหม่ที่ไม่ต้องมี provider และเป็นตัวเดียวในสองตัวที่รองรับ replace_triggered_by
  • ค่า replace_triggered_by ที่ชี้ไปยัง argument ที่เป็นส่วนหนึ่งของ config ของ resource เองอยู่แล้ว (ซ้ำซ้อน) แทนที่จะเป็นสัญญาณภายนอกที่ resource ไม่มีทางอื่นจะตอบสนองได้

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

variable "vm_names" {
type = list(string)
default = ["web-a", "web-b", "web-c"]
}
resource "azurerm_linux_virtual_machine" "web" {
count = length(var.vm_names)
name = "${var.environment}-${var.vm_names[count.index]}"
# ...
}
# Later: ops wants to remove "web-b" from the deploy
# variable "vm_names" { default = ["web-a", "web-c"] }
เฉลย

การลบ "web-b" ออกจากตรงกลางของ list นี้ ไม่ได้ แค่ destroy VM ของ web-b เท่านั้น พิสูจน์ข้างบนด้วยรูปแบบเดียวกันเป๊ะ (local_file แทน azurerm_linux_virtual_machine กลไกเหมือนกัน): 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.vm_names) (หรือ map ถ้าต้องการ config เฉพาะตัวเกินกว่าชื่อ): ลบ "web-b" แล้วจะ plan แค่ 1 to destroy เท่านั้น ไม่มีอะไรอื่นถูกแตะ

เกิดอะไรขึ้นเมื่อลบรายการหนึ่งออกจากตรงกลางของ list ที่ใช้กับ count
ทำไม for_each ถึงไม่มีปัญหา index-shifting เหมือน count
ความต่างระหว่าง for expression กับ for_each meta-argument ของ resource คืออะไร
instance ที่สร้างด้วย for_each ถูกอ้างอิงจากภายนอก resource block อย่างไร
รัน terraform plan กับโค้ดนี้จะเกิดอะไรขึ้น
resource "random_pet" "names" {
  count = 2
}

resource "azurerm_linux_virtual_machine" "web" {
  for_each = toset(random_pet.names[*].id)
}
resource หนึ่งมี count = var.enable_x ? 1 : 0 และอีก resource อ้างอิง id ของมันด้วย one(that_resource[*].id) one() จะคืนค่าอะไรเมื่อ count ประเมินเป็น 0
terraform_data เพิ่มอะไรเหนือกว่า null_resource แบบเก่า นอกจากไม่ต้องมี provider แยก