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" }
instance_count = var.environment == "prod" ? 5 : 2}
resource "aws_instance" "web" { count = local.instance_count ami = var.ami_id instance_type = var.instance_type
tags = merge(local.common_tags, { Name = "${local.name_prefix}-web-${count.index}" })}
resource "aws_s3_bucket" "app_data" { bucket = "${local.name_prefix}-app-data" tags = local.common_tags}local.common_tags ถูกคำนวณครั้งเดียวจาก var.project กับ var.environment แล้วถูกอ้างอิงทั้งจาก aws_instance.web และ aws_s3_bucket.app_data ถ้าไม่มี local ตัวนี้ expression merge(...) เดิมกับคู่ key-value ทั้งสามตัวต้องถูก copy ไปใส่ทุก resource block ที่ต้องใช้ แล้วถ้าวันหนึ่งต้องเปลี่ยน scheme การ tag ก็ต้องไล่หาทุก copy ที่ว่านั้น local.instance_count ทำหน้าที่คล้ายกันกับ conditional เล็ก ๆ การตัดสินใจ “5 ตัวใน prod, 2 ตัวที่อื่น” ถูกเขียนไว้ครั้งเดียวแล้วอ้างอิงด้วยชื่อทุกที่ที่ต้องใช้ 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_instance_ids" { description = "IDs of the EC2 instances created for the web tier" value = aws_instance.web[*].id}
output "app_data_bucket_arn" { description = "ARN of the S3 bucket used for application data" value = aws_s3_bucket.app_data.arn}
output "db_password" { description = "Master password generated for the RDS instance" value = aws_db_instance.main.password sensitive = true}# List every output and its valueterraform output
# Print one output's value, unquoted for scriptingterraform output web_instance_ids
# Machine-readable output, for piping into jq or another toolterraform output -json app_data_bucket_arnterraform output เฉย ๆ จะ print ทุก output ที่ declare ไว้หลัง apply ล่าสุด terraform output <name> print เฉพาะตัวเดียว และ terraform output -json render ค่า (หรือทุกค่า ถ้าไม่ระบุชื่อ) ออกมาเป็น JSON ซึ่งเหมาะกับตอนที่ CI pipeline หรือ script อื่นต้องเอาผลลัพธ์ไปใช้แบบ programmatic ไม่ใช่ให้คนอ่านเป็น plain text sensitive = true บน db_password ทำงานเหมือนกับตอนอยู่บน variable เป๊ะ ๆ Terraform จะ print (sensitive value) ใน output ของ plan/apply และใน terraform output เฉย ๆ แต่ terraform output db_password ยังคง print ค่าจริงออกมาถ้าระบุชื่อตรง ๆ และค่ายังถูกเก็บเป็น plain text ใน state อยู่ดี นี่คือการป้องกันตอนแสดงผลบน terminal กับ log ไม่ใช่ access control
พิสูจน์ครบทุกขั้นบน output ที่ sensitive = true:
$ terraform output # bare -- redacteddb_password = <sensitive>
$ terraform output db_password # by name -- revealed"s3cr3t-value"
$ terraform output -json # JSON -- also revealed, sensitive flag included{ "db_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 หรือ apply):
variable "ami_id" { type = string default = "not-an-ami"}
resource "aws_instance" "web" { ami = var.ami_id instance_type = var.instance_type
lifecycle { precondition { condition = can(regex("^ami-", var.ami_id)) error_message = "ami_id must look like a real AMI id, e.g. ami-0123456789abcdef0. Got: ${var.ami_id}" } }}precondition fail จริงเมื่อ ami_id ผิดรูปแบบ กับ aws_instance จริง (mock credentials ไม่แตะ network เลย — precondition ถูกเช็คก่อนที่ Terraform จะไปเรียก EC2 API ด้วยซ้ำ):
$ terraform planPlanning failed. Terraform encountered an error while generating this plan.
╷│ Error: Resource precondition failed││ on main.tf line 17, in resource "aws_instance" "web":│ 17: condition = can(regex("^ami-", var.ami_id))│ ├────────────────│ │ var.ami_id is "not-an-ami"││ ami_id must look like a real AMI id, e.g. ami-0123456789abcdef0. Got:│ not-an-ami╵postcondition ต้องมี self attribute จริงหลัง apply ให้เช็ค ซึ่งทำ offline กับ aws_instance ไม่ได้ — พิสูจน์ด้วย terraform_data แทน (กลไก lifecycle เดียวกัน ไม่ผูกกับ cloud):
resource "terraform_data" "web" { input = "not-an-instance-id"
lifecycle { postcondition { condition = startswith(self.output, "i-") error_message = "web output must look like an EC2 instance id (start with i-). 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 must look like an EC2 instance id (start with i-). 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)
precondition { condition = length(trimspace(terraform_data.web.output)) > 0 error_message = "web_instance_id must not be empty." }
postcondition { condition = startswith(self.value, "i-") error_message = "web_instance_id must look like an EC2 instance id (start with i-). Got: ${self.value}" }}$ terraform validate╷│ Error: Postconditions are not allowed││ on main.tf line 13, in output "web_instance_id":│ 13: 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 EC2 instance id." }}depends_on บน output
หัวข้อที่มีชื่อว่า “depends_on บน output”value expression ของ output ปกติจะสร้าง implicit dependency ให้เองอยู่แล้ว (การอ้าง aws_instance.web.id บอก Terraform ไปแล้วว่า output นี้ขึ้นกับ instance นั้น) depends_on มีไว้สำหรับกรณีที่หายากกว่า — บังคับลำดับที่ explicit ซึ่ง value expression เองไม่ได้เผยให้เห็น เช่นเปิดเผย output ก็ต่อเมื่อ resource ที่ไม่เกี่ยวกันแต่มี side effect ถูกสร้างไปแล้ว:
resource "terraform_data" "bootstrap" { input = "bootstrap-done"}
resource "aws_instance" "web" { ami = var.ami_id instance_type = var.instance_type}
output "web_status" { value = aws_instance.web.id depends_on = [terraform_data.bootstrap]}terraform graph -type=plan DOT output จริงยืนยัน edge พิเศษที่ depends_on เพิ่มเข้ามา (edge นั้นจะไม่มีจาก value เพียงอย่างเดียว เพราะ value expression ของ output ไม่เคยอ้าง terraform_data.bootstrap เลย):
$ terraform graph -type=plan"[root] output.web_status (expand)" -> "[root] terraform_data.bootstrap (expand)""[root] output.web_status (expand)" -> "[root] aws_instance.web (expand)"Outputs คือกลไกของการ compose
หัวข้อที่มีชื่อว่า “Outputs คือกลไกของการ compose”ภายใน 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["aws_instance.web"] lp --> rb["aws_s3_bucket.app_data"] rb --> ob["output app_data_bucket_arn"] 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 ใด ๆ เลย (พิสูจน์ข้างบนกับ aws_instance) | เข้าถึงได้แค่ค่าที่รู้อยู่แล้วก่อน 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
อ่านโค้ดที่ 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 = aws_instance.web[0].id
postcondition { condition = startswith(self.value, "i-") 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 ตรง ๆ (aws_instance.web[0].id ไม่ใช่ self.value เพราะ precondition ของ output รันก่อน value จะ finalize) หรือถ้าเจตนาจริง ๆ คือ “validate attribute หลัง apply ของ resource เอง” ก็ใช้ postcondition บน lifecycle block ของ resource แทน ซึ่ง self ใช้ได้ตรงนั้น