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

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"
}
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 ได้เลย

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
}
Terminal window
# List every output and its value
terraform output
# Print one output's value, unquoted for scripting
terraform output web_instance_ids
# Machine-readable output, for piping into jq or another tool
terraform output -json app_data_bucket_arn

terraform 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 -- redacted
db_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

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 plan
Planning 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-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 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."
}
}

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)"

ภายใน 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"]
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 ใด ๆ เลย (พิสูจน์ข้างบนกับ aws_instance)เข้าถึงได้แค่ค่าที่รู้อยู่แล้วก่อน 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
  • 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 = 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 validate
Error: Postconditions are not allowed
Output 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 ใช้ได้ตรงนั้น

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

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