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

Outputs and Locals

local คือ expression ที่มีชื่อ คำนวณครั้งเดียวแล้วนำกลับมาใช้ซ้ำภายใน configuration ส่วน output จะดึงค่าออกจาก configuration ไปให้มองเห็นได้หลัง apply — รวมกับ variable แล้วทั้งสามคือ named value สามชนิดที่แตกต่างกันใน Terraform

variable ถูกตั้งค่าโดยคนที่เรียกใช้ configuration และ output ถูกเปิดเผยให้คนที่นำไปใช้ต่อ ส่วน local ไม่ใช่ทั้งสองอย่างนั้น เป็นแค่ความสะดวกภายใน เป็นชื่อที่ตั้งให้กับ expression เพื่อไม่ต้องเขียน expression เดิมซ้ำทุกครั้งที่ต้องใช้ผลลัพธ์ ไม่มีใครนอก configuration ตั้งค่า local ได้ และไม่มีใครนอก configuration อ่านได้โดยตรงเช่นกัน

locals {
name_prefix = "${var.environment}-${var.project}"
common_tags = {
Project = var.project
Environment = var.environment
ManagedBy = "terraform"
}
vm_count = var.environment == "prod" ? 5 : 2
# Azure Storage Account names must be 3-24 characters, lowercase letters and numbers only
storage_account_name = lower(replace("${local.name_prefix}appdata", "-", ""))
}
resource "azurerm_linux_virtual_machine" "web" {
count = local.vm_count
name = "${local.name_prefix}-web-${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"
}
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-web-${count.index}"
})
}
resource "azurerm_storage_account" "app_data" {
name = local.storage_account_name
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
account_tier = "Standard"
account_replication_type = "LRS"
tags = local.common_tags
}

local.common_tags ถูกคำนวณครั้งเดียวจาก var.project กับ var.environment แล้วถูกอ้างอิงทั้งจาก azurerm_linux_virtual_machine.web และ azurerm_storage_account.app_data ถ้าไม่มี local ตัวนี้ key-value สามคู่เดิมต้องถูก copy ไปทุก resource block ที่ต้องใช้ และถ้าวันหลังต้องเปลี่ยน tagging scheme ก็ต้องไล่หาทุก copy เอง local.vm_count ทำหน้าที่คล้ายกันกับ conditional เล็ก ๆ การตัดสินใจ “prod ใช้ 5 ที่อื่นใช้ 2” ถูกเขียนครั้งเดียวแล้วอ้างอิงด้วยชื่อทุกที่ที่ต้องใช้ local.storage_account_name ยิ่งเห็นประโยชน์ชัดกว่านั้นอีก — ชื่อ Azure Storage Account ต้องมีความยาว 3 ถึง 24 ตัวอักษร เป็นตัวพิมพ์เล็กกับตัวเลขเท่านั้น ห้ามมีขีดกลาง ดังนั้น expression นี้ตัดขีดกลางออกจาก name_prefix ครั้งเดียวในที่เดียว แทนที่ทุก resource ที่ต้องใช้ชื่อ storage account ที่ถูกต้องจะต้องเรียก lower(replace(...)) ซ้ำเอง local ไม่เคยถูก assign โดย caller และไม่ปรากฏใน terraform plan เป็นอะไรที่ override ได้ — แต่คือแค่ชื่อของค่าที่คำนวณไว้ภายใน configuration นี้เท่านั้น

Locals ไม่มีลำดับการ evaluate ตายตัว — และ cycle คือ error จริง

หัวข้อที่มีชื่อว่า “Locals ไม่มีลำดับการ evaluate ตายตัว — และ cycle คือ error จริง”

Terraform evaluate ทุก local (และทุก resource attribute) แบบ lazy ตามลำดับที่ dependency graph ต้องการ ไม่ใช่จากบนลงล่างตามที่เขียน local.a ที่อ้าง local.b ที่อ้าง local.a กลับไปไม่ใช่ “undefined behavior” แต่คือ error จริงที่ถูกจับได้:

locals {
a = local.b
b = local.a
}
output "a" {
value = local.a
}
$ terraform plan
╷
│ Error: Cycle:
│ local.a (expand)
│ local.b (expand)
╵

นี่คือ error ประเภทเดียวกับที่ terraform graph (พูดถึงในบท HCL, Providers & Resources) ทำให้เห็นชัดสำหรับ resource — Terraform สร้าง dependency graph เดียวครอบคลุมทุก local, resource, และ output ใน configuration และ cycle ตรงไหนก็ตามคือจุดหยุดแข็ง ๆ ก่อนที่จะ produce plan ได้เลย

output block ดึงค่าที่มีอยู่ภายใน configuration — ส่วนใหญ่คือ attribute ของ resource ที่รู้ค่าจริงได้ก็ต่อเมื่อ apply เสร็จแล้ว — แล้วทำให้ค่านั้นใช้งานได้จากภายนอก configuration description ไว้บอกความหมายของค่า และ sensitive = true ซ่อนค่าจาก CLI output ปกติเหมือนที่ทำกับ variable

output "web_vm_ids" {
description = "IDs of the virtual machines created for the web tier"
value = azurerm_linux_virtual_machine.web[*].id
}
output "app_data_storage_account_id" {
description = "Resource ID of the storage account used for application data"
value = azurerm_storage_account.app_data.id
}
output "sql_admin_password" {
description = "Administrator login password generated for the Azure SQL server"
value = azurerm_mssql_server.main.administrator_login_password
sensitive = true
}
Terminal window
# List every output and its value
terraform output
# Print one output's value, unquoted for scripting
terraform output web_vm_ids
# Machine-readable output, for piping into jq or another tool
terraform output -json app_data_storage_account_id

terraform output เฉย ๆ จะ print output ทุกตัวที่ประกาศไว้หลัง apply ล่าสุด terraform output <name> print แค่ตัวเดียว และ terraform output -json แสดงค่า (หรือทุกค่าถ้าไม่ระบุชื่อ) เป็น JSON ซึ่งจำเป็นเวลา CI pipeline หรือ script อื่นต้องเอาผลลัพธ์ไปใช้ต่อแบบ programmatic ไม่ใช่ให้คนอ่านเฉย ๆ sensitive = true บน sql_admin_password ทำงานเหมือนกับตอนอยู่บน variable ทุกอย่าง Terraform จะ print (sensitive value) ใน output ของ plan/apply และใน terraform output เปล่า ๆ แต่ terraform output sql_admin_password ยังคง print ค่าจริงถ้าเรียกชื่อตรง ๆ และค่านั้นยังถูกเก็บเป็น plain text ใน state เหมือนเดิม นี่คือการป้องกันตอนแสดงผลบน terminal กับ log ไม่ใช่ access control

พิสูจน์ครบทุกขั้นบน output ที่ sensitive = true (ผ่าน terraform test กับ mock_provider "azurerm" {} เพราะ plan จริงต้องใช้ Azure credentials ที่คอร์สนี้ไม่มี):

$ terraform output # bare -- redacted
sql_admin_password = <sensitive>
$ terraform output sql_admin_password # by name -- revealed
"s3cr3t-value"
$ terraform output -json # JSON -- also revealed, sensitive flag included
{
"sql_admin_password": {
"sensitive": true,
"type": "string",
"value": "s3cr3t-value"
}
}

-json ไม่ได้ respect sensitive = true เลยด้วยซ้ำ — มัน mark field ว่า "sensitive": true ให้ consumer สังเกต แต่ยังใส่ "value" จริงเข้าไปอยู่ดี อะไรก็ตามที่ pipe terraform output -json เข้า log line, CI artifact, หรือ chat notification จะ print secret เต็ม ๆ ออกมา — JSON flag เป็นแค่ metadata ไม่ใช่การ redact

lifecycle block บน resource รับได้ทั้ง precondition (เช็คก่อน plan resource) และ postcondition (เช็ค attribute ของ resource เองผ่าน self หลัง plan) plan จริงกับ azurerm ต้องใช้ Azure credentials ที่คอร์สนี้ไม่มีเลย เลยพิสูจน์ด้วย terraform test กับ mock_provider "azurerm" {} — กลไก mock ของ Terraform เอง ไม่ใช่การเรียก network:

variable "vm_size" {
type = string
default = "not-a-real-size"
}
resource "azurerm_resource_group" "main" {
name = "rg-demo"
location = "eastus"
}
resource "azurerm_network_interface" "web" {
name = "web-nic"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
ip_configuration {
name = "internal"
subnet_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-demo/providers/Microsoft.Network/virtualNetworks/vnet/subnets/subnet1"
private_ip_address_allocation = "Dynamic"
}
}
resource "azurerm_linux_virtual_machine" "web" {
name = "web-vm"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
size = var.vm_size
admin_username = "azureuser"
network_interface_ids = [
azurerm_network_interface.web.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"
}
lifecycle {
precondition {
condition = can(regex("^Standard_", var.vm_size))
error_message = "vm_size must be a valid Azure VM size, e.g. Standard_D2s_v3. Got: ${var.vm_size}"
}
}
}
tests/precondition.tftest.hcl
mock_provider "azurerm" {}
variables {
vm_size = "not-a-real-size"
}
run "shows_real_error" {
command = plan
}

error จริง จาก terraform test กับ provider ที่ mock ไว้:

$ terraform test
╷
│ Error: Resource precondition failed
│
│ on main.tf line 52, in resource "azurerm_linux_virtual_machine" "web":
│ 52: condition = can(regex("^Standard_", var.vm_size))
│ ├────────────────
│ │ var.vm_size is "not-a-real-size"
│
│ vm_size must be a valid Azure VM size, e.g. Standard_D2s_v3. Got:
│ not-a-real-size
╵
Failure! 0 passed, 1 failed.

เขียนเป็น test จริงด้วย expect_failures (pattern ที่ module Testing Modules พูดถึงแบบละเอียด) run เดียวกันจะ pass แทนที่จะ fail เพราะ precondition ที่ fail คือ behavior ที่ตั้งใจ test ไว้:

run "rejects_bad_vm_size" {
command = plan
expect_failures = [
azurerm_linux_virtual_machine.web.lifecycle[0].precondition[0],
]
}
$ terraform test
run "rejects_bad_vm_size"... pass
Success! 1 passed, 0 failed.

postcondition ต้องมี self จริงหลัง apply ซึ่งต้องมี resource ถูกสร้างขึ้นมาจริง — พิสูจน์ด้วย terraform_data แทน (กลไก lifecycle เดียวกัน ไม่ผูกกับ cloud และรันได้โดยไม่ต้อง mock อะไรเลย):

resource "terraform_data" "web" {
input = "not-an-instance-id"
lifecycle {
postcondition {
condition = startswith(self.output, "i-")
error_message = "web output looks wrong. Got: ${self.output}"
}
}
}
$ terraform apply -auto-approve
terraform_data.web: Creating...
terraform_data.web: Creation complete after 0s [id=e18876e2-035e-cf41-d0df-1750ee7f8900]
╷
│ Error: Resource postcondition failed
│
│ on main.tf line 6, in resource "terraform_data" "web":
│ 6: condition = startswith(self.output, "i-")
│ ├────────────────
│ │ self.output is "not-an-instance-id"
│
│ web output looks wrong. Got: not-an-instance-id
╵

สังเกตว่า resource ถูกสร้างขึ้นจริงก่อนที่ postcondition check จะรัน — ต่างจาก precondition, postcondition ห้าม resource ไม่ให้ถูกสร้างไม่ได้ ทำได้แค่ report failure แล้วหยุด run หลังจากนั้น

output block กลับรับได้แค่ precondition เท่านั้น — การใส่ postcondition บน output คือ error จริงทันที ไม่ใช่แค่พฤติกรรมแปลก ๆ นี่ขัดกับสมมติฐานทั่วไปที่ว่า output รองรับทั้งคู่ — จริง ๆ รองรับแค่ precondition:

resource "terraform_data" "web" {
input = "i-mock123"
}
output "web_instance_id" {
value = trimspace(terraform_data.web.output)
postcondition {
condition = startswith(self.value, "i-")
error_message = "web_instance_id must look like an instance id (start with i-). Got: ${self.value}"
}
}
$ terraform validate
╷
│ Error: Postconditions are not allowed
│
│ on main.tf line 8, in output "web_instance_id":
│ 8: postcondition {
│
│ Output values can only have preconditions, not postconditions.
╵

precondition ใน block รูปร่างเดียวกันถูกต้องและทำงานตามที่คาดหวัง — แค่ต้องเขียน check เป็นสิ่งที่ true ก่อน publish โดยอ้าง resource ที่ output อ่านมาโดยตรง (output ไม่มี self.value ให้เช็คข้างใน precondition เพราะค่ายังไม่ใช่ “self” ตอนนั้น):

output "web_instance_id" {
value = terraform_data.web.output
precondition {
condition = startswith(terraform_data.web.output, "i-")
error_message = "web_instance_id must look like an instance id."
}
}

value expression ของ output ปกติจะสร้าง implicit dependency ให้เองอยู่แล้ว depends_on มีไว้สำหรับกรณีที่หายากกว่า — บังคับลำดับที่ explicit ซึ่ง value expression เองไม่ได้เผยให้เห็น เช่นเปิดเผย output ก็ต่อเมื่อ resource ที่ไม่เกี่ยวกันแต่มี side effect ถูกสร้างไปแล้ว:

resource "terraform_data" "bootstrap" {
input = "bootstrap-done"
}
output "web_status" {
value = azurerm_linux_virtual_machine.web.id
depends_on = [terraform_data.bootstrap]
}

terraform graph -type=plan DOT output จริงยืนยัน edge พิเศษที่ depends_on เพิ่มเข้ามา (พิสูจน์กับคู่ aws_instance/terraform_data ที่เทียบเท่ากัน — กลไกเหมือนกันทุกประการไม่ว่า provider ไหน เพราะ depends_on เป็นของ Terraform core ไม่ใช่ของ provider):

$ terraform graph -type=plan
"[root] output.web_status (expand)" -> "[root] terraform_data.bootstrap (expand)"
"[root] output.web_status (expand)" -> "[root] azurerm_linux_virtual_machine.web (expand)"

ภายใน root configuration เดียว output ส่วนใหญ่เป็นแค่ข้อมูล — เป็นวิธีที่สะดวกในการโชว์ค่าให้คนที่รัน terraform apply เห็นเฉย ๆ ความสำคัญจริง ๆ จะเห็นชัดก็ต่อเมื่อ configuration ไม่ได้เป็นสิ่งเดียวโดด ๆ อีกต่อไป เวลาแยก infrastructure ออกเป็น module, module ไม่มีทางอื่นเลยที่จะส่งค่าที่คำนวณแล้วกลับไปให้ผู้เรียกใช้นอกจากผ่าน output block — module เรื่อง Modules ในคอร์สนี้จะพูดถึงการอ้างอิง module.<name>.<output> แบบละเอียดต่อไป เวลาแยก infrastructure ออกเป็น Terragrunt unit ที่ deploy แยกกันแทน dependency block จะอ่าน output ของอีก unit หนึ่งเพื่อเชื่อม unit เข้าด้วยกันโดยไม่ต้อง hard-code ค่าใด ๆ ระหว่างกันเลย — pattern นี้จะถูกพูดถึงอีกครั้งใน module ของ Terragrunt ทีหลัง ทั้งสองกรณีใช้กลไกเดียวกับที่เห็นตรงนี้ ค่าหนึ่งรู้ค่าจริงภายใน configuration หนึ่ง output block เปิดเผยออกมา แล้วสิ่งอื่น — parent module หรือ dependent unit — นำไปใช้ต่อ

flowchart LR
  ve["var.environment"] --> lp["local.name_prefix"]
  vp["var.project"] --> lp
  lp --> ri["azurerm_linux_virtual_machine.web"]
  lp --> rb["azurerm_storage_account.app_data"]
  rb --> ob["output app_data_storage_account_id"]
  ob --> dep["dependency block or module output reference"]
Local หนึ่งตัวป้อนเข้า resource สองตัว และ attribute ของ resource กลายเป็น output

Terraform เก็บ dependency graph เดียวครอบคลุมทุก local, resource, และ output ใน configuration — ไม่มี “รอบ locals” แยกที่รันก่อน “รอบ resource” ข้อเท็จจริงเดียวนี้อธิบายทุกอย่างข้างบน — locals cycle ถูกจับแบบเดียวกับที่ resource cycle จะถูกจับ เพราะเป็น graph เดียวกัน; precondition บน resource กัน plan ได้เพราะ node ของ resource ยังไม่ถูกไปถึง ในขณะที่ resource postcondition report failure ได้แค่หลังจากที่ apply ไปแล้ว เพราะตอนที่ self มีอยู่ apply ก็เกิดไปแล้ว; และ depends_on บน output มีความหมายเพราะ graph เป็นของจริง edge ใน graph เปลี่ยน execution order จริง ไม่ใช่แค่ documentation

ตัวเลือกได้อะไรเสียอะไร
resource preconditionหยุด plan ที่ผิดก่อนแตะ resource ใด ๆ เลย (พิสูจน์ข้างบนกับ azurerm_linux_virtual_machine ผ่าน terraform test)เข้าถึงได้แค่ค่าที่รู้อยู่แล้วก่อน plan resource นี้ — เช็ค attribute หลัง apply ของตัวเองไม่ได้
resource postconditionvalidate attribute จริงหลัง apply ของ resource เองผ่าน self ได้ (พิสูจน์ข้างบน)resource ถูกสร้าง (หรือเปลี่ยน) ไปแล้วตอนที่มัน fail — พิสูจน์ข้างบนว่า terraform_data.web มีอยู่แล้วก่อนที่ postcondition error จะหยุด run
output preconditionหยุด plan/apply ก่อนที่ค่าที่ผิดจะถูก publish ไปให้ consumerไม่มี postcondition เลย — พิสูจน์ข้างบนว่าเป็น error จริงทันทีตอน terraform validate ไม่ใช่แค่ warning เบา ๆ
depends_on บน outputบังคับลำดับที่ value expression เองไม่ได้เผยให้เห็น (พิสูจน์ข้างบนผ่าน terraform graph DOT output จริง)ง่ายที่จะหยิบมาใช้ทั้งที่ทางแก้จริง ๆ คือปรับ value expression ให้อ้าง dependency ตรง ๆ ซึ่งจะบันทึกไว้ด้วยว่า ทำไม ถึงมี dependency นี้ แทนที่จะปล่อยไว้เป็น resource reference เปล่า ๆ
  • “output มี precondition กับ postcondition ได้ทั้งคู่เหมือน resource lifecycle block” พิสูจน์ข้างบน: ไม่ได้ terraform validate ปฏิเสธ postcondition บน output ทันทีด้วย “Output values can only have preconditions, not postconditions”
  • “terraform output -json ปลอดภัยกับ sensitive value เพราะมันมีไว้ให้ machine อ่าน ไม่ใช่คน” พิสูจน์ข้างบน: JSON output ใส่ค่าจริงเข้าไปและมีแค่ marker "sensitive": true เพิ่มมา — pipe เข้า CI log หรือ chat message ก็ยังรั่ว secret เต็ม ๆ
  • “Locals ถูก evaluate จากบนลงล่าง ดังนั้น local.b ต้องประกาศไว้เหนือ local.a ถ้า a ใช้ b” Terraform สร้าง dependency graph แล้ว evaluate แบบ lazy — ลำดับในไฟล์ไม่มีผลกับความถูกต้อง มีผลแค่กับคนอ่าน สิ่งที่ invalid จริง ๆ คือ cycle จริง ๆ พิสูจน์ข้างบนด้วย error ที่ชัดเจนไม่กำกวม
  • “resource postcondition ป้องกันไม่ให้ resource ถูกสร้างได้ถ้า fail” พิสูจน์ข้างบน: resource ถูกสร้างก่อน (terraform_data.web: Creation complete) แล้วค่อย postcondition fail และหยุด run — มัน report แล้วหยุด ไม่ได้ rollback
  • “test resource precondition โดยไม่มี Azure credentials จริงทำไม่ได้” พิสูจน์ข้างบน: terraform test กับ mock_provider "azurerm" {} รัน plan-time graph รวมถึง precondition แบบ offline ล้วน ๆ
  • postcondition block ที่ generate มาข้างใน output — นี่คือ error จริงตอน terraform validate วันนี้ พิสูจน์ข้างบน มีแค่ precondition ที่ legal บน output
  • error_message ของ precondition/postcondition ที่อ้าง self ข้างใน precondition ของ output — precondition ของ output รันก่อนที่ value จะ finalize ดังนั้นไม่มี self ให้อ้างแบบที่ resource postcondition มี
  • terraform output -json ที่ pipe ตรงเข้า log line หรือ CI artifact สำหรับค่าที่เป็น sensitive = true — พิสูจน์ข้างบนว่ายังเผยค่าจริงอยู่ดี
  • Locals ที่เขียนเรียงลำดับ “ดูเหมือน” ต่อกันจากบนลงล่าง พร้อม comment อ้างว่าลำดับมีผล — evaluation order ของ Terraform ขับเคลื่อนด้วย graph ไม่ใช่ตัวอักษร comment นั้นทำให้เข้าใจผิดถึงแม้โค้ดจะรันผ่านก็ตาม
  • resource postcondition ที่ถูกใช้ในจุดที่จริง ๆ ต้องการ precondition (เช็คสิ่งที่รู้ได้ก่อน plan resource แต่ต้องจ่าย cost ตอน apply โดยไม่จำเป็น) — พิสูจน์ข้างบนว่า precondition หยุด plan ได้ก่อนแตะ resource ใด ๆ ส่วน postcondition ทำแบบนั้นไม่ได้
  • depends_on ที่ใส่เข้า output เป็น reflex แรกสำหรับ “สองอย่างนี้ดูเกี่ยวข้องกัน” โดยไม่เช็คก่อนว่าการอ้าง attribute ของ dependency ตรง ๆ ใน value expression จะสร้างลำดับเดียวกันแบบ implicit และอ่านเข้าใจง่ายกว่าไหม

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

output "web_instance_id" {
value = azurerm_linux_virtual_machine.web.id
postcondition {
condition = startswith(self.value, "vm-")
error_message = "web_instance_id looks wrong."
}
}
เฉลย

โค้ดนี้ไม่มีทางได้รันเลย — terraform validate ปฏิเสธทั้ง configuration ก่อนจะ produce plan ได้ด้วยซ้ำ เพราะ output ไม่รองรับ postcondition เลย พิสูจน์ข้างบนด้วย block รูปร่างเดียวกันเป๊ะ ๆ:

$ terraform validate
Error: Postconditions are not allowed
Output values can only have preconditions, not postconditions.

ทางแก้คือใช้ precondition ที่อ้าง resource attribute ตรง ๆ (azurerm_linux_virtual_machine.web.id ไม่ใช่ self.value เพราะ precondition ของ output รันก่อน value จะ finalize) หรือถ้าเจตนาจริง ๆ คือ “validate attribute หลัง apply ของ resource เอง” ก็ใช้ postcondition บน lifecycle block ของ resource แทน ซึ่ง self ใช้ได้ตรงนั้น

อะไรคือความต่างหลักในจุดประสงค์ระหว่าง local กับ output
local ต่างจาก variable อย่างไร
ทำไม output ถึงสำคัญกับการ compose infrastructure ทั้งที่ module กับ Terragrunt dependency จะพูดถึงละเอียดกว่าทีหลัง
terraform output -json มีประโยชน์ตรงไหน
รัน terraform validate กับโค้ดนี้จะเกิดอะไรขึ้น
output "web_instance_id" {
  value = azurerm_linux_virtual_machine.web.id

  postcondition {
    condition     = startswith(self.value, "vm-")
    error_message = "bad id"
  }
}
resource ตัวหนึ่งมี postcondition ที่ fail หลังจาก terraform apply สร้างมันขึ้นมา เกิดอะไรขึ้นกับ resource จริง ๆ
รัน terraform plan กับโค้ดนี้จะเกิดอะไรขึ้น
locals {
  a = local.b
  b = local.a
}