Outputs and Locals
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”local คือ expression ที่มีชื่อ คำนวณครั้งเดียวแล้วนำกลับมาใช้ซ้ำภายใน configuration ส่วน output จะดึงค่าออกจาก configuration ไปให้มองเห็นได้หลัง apply — รวมกับ variable แล้วทั้งสามคือ named value สามชนิดที่แตกต่างกันใน Terraform
Locals: คำนวณค่าครั้งเดียวแล้วใช้ซ้ำ
หัวข้อที่มีชื่อว่า “Locals: คำนวณค่าครั้งเดียวแล้วใช้ซ้ำ”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 ได้เลย
Outputs: เปิดเผยค่าหลัง apply
หัวข้อที่มีชื่อว่า “Outputs: เปิดเผยค่าหลัง apply”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}# List every output and its valueterraform output
# Print one output's value, unquoted for scriptingterraform output web_vm_ids
# Machine-readable output, for piping into jq or another toolterraform output -json app_data_storage_account_idterraform 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 -- redactedsql_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
precondition กับ postcondition: ไม่สมมาตรกันระหว่าง resource กับ output
หัวข้อที่มีชื่อว่า “precondition กับ postcondition: ไม่สมมาตรกันระหว่าง resource กับ output”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}" } }}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 testrun "rejects_bad_vm_size"... passSuccess! 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-approveterraform_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." }}depends_on บน output
หัวข้อที่มีชื่อว่า “depends_on บน output”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)"Outputs คือกลไกของการต่อยอด (composition)
หัวข้อที่มีชื่อว่า “Outputs คือกลไกของการต่อยอด (composition)”ภายใน 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"]
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”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 postcondition | validate 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 ล้วน ๆ
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”postconditionblock ที่ generate มาข้างในoutput— นี่คือ error จริงตอนterraform validateวันนี้ พิสูจน์ข้างบน มีแค่preconditionที่ legal บน outputerror_messageของ precondition/postcondition ที่อ้างselfข้างในpreconditionของ output — precondition ของ output รันก่อนที่ value จะ finalize ดังนั้นไม่มีselfให้อ้างแบบที่ resourcepostconditionมี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 ตรง ๆ ในvalueexpression จะสร้างลำดับเดียวกันแบบ 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 validateError: Postconditions are not allowedOutput 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 ใช้ได้ตรงนั้น