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

Input Variables

input variable คือ parameter ที่มีชื่อและมี type ชัดเจน ทำให้ config เดียวกันสร้าง Azure infrastructure ต่างกันได้ตามคนเรียกและค่าที่ส่งเข้ามา

variable block มีสี่ส่วนที่ควรรู้ไว้ให้แม่น type กำหนดรูปร่างของค่าที่ยอมรับได้ default เป็นค่า fallback เวลาไม่มีใครส่งค่ามา description ไว้บอกความตั้งใจให้คนอ่านคนถัดไป และ sensitive ซ่อนค่าจาก CLI output ส่วนใหญ่

variable "environment" {
type = string
description = "Deployment environment name, e.g. dev, staging, prod"
default = "dev"
}
variable "vm_count" {
type = number
description = "Number of virtual machines to launch"
default = 2
}
variable "enable_monitoring" {
type = bool
description = "Whether to enable Azure Monitor diagnostics on the virtual machines"
default = true
}
variable "allowed_ports" {
type = list(number)
description = "TCP ports allowed inbound on the network security group"
default = [22, 443]
}
variable "resource_tags" {
type = map(string)
description = "Tags applied to every resource in this configuration"
default = {
project = "payments"
team = "platform"
}
}
variable "db_config" {
type = object({
sku_name = string
storage_mb = number
backup_retention_days = number
})
description = "Structured configuration for the PostgreSQL Flexible Server"
default = {
sku_name = "B_Standard_B1ms"
storage_mb = 32768
backup_retention_days = 7
}
}
variable "sql_admin_password" {
type = string
description = "Administrator login password for the Azure SQL server"
sensitive = true
}

object({...}) ควรพูดถึงแยกไว้ต่างหาก เป็น structure ที่มี type และรูปร่างตายตัว ทุก key ที่ list ไว้ต้อง required (เว้นแต่จะใส่ default ผ่าน optional()) และ Terraform ปฏิเสธค่าที่ caller ส่งมาผิดรูปร่างตั้งแต่ตอน plan ก่อนที่จะไปเรียก Azure API เลยด้วยซ้ำ

การ list ทุก key เป็น required ทำให้ object type เปราะ — caller ที่สนใจแค่ sku_name ก็ยังต้องใส่ทุก field ที่เหลือครบ optional(<type>, <default>) ข้างใน object({...}) ทำเครื่องหมายว่า attribute นั้นไม่ required และมี default ของตัวเอง แยกจาก default ระดับบนสุดของ variable:

variable "db_config" {
type = object({
sku_name = string
storage_mb = optional(number, 32768)
backup_retention_days = optional(number, 7)
geo_redundant_backup = optional(bool, false)
})
default = {
sku_name = "B_Standard_B1ms"
}
}

session terraform console จริงกับ block นี้เป๊ะ ๆ evaluate var.db_config โดยใส่แค่ sku_name — Terraform เติมทุก attribute ที่เว้นไว้จาก optional() ให้เองตาม default:

$ echo 'var.db_config' | terraform console
{
"backup_retention_days" = 7
"geo_redundant_backup" = false
"sku_name" = "B_Standard_B1ms"
"storage_mb" = 32768
}

ต่างจาก object({...}) เปล่า ๆ ข้างบน — ถ้าไม่มี optional() caller ที่ไม่ใส่ storage_mb จะเจอ error “attribute is required” ทันทีตอน plan แต่มี optional() แล้ว object จะ required แค่บางส่วนจริง ๆ — มีแค่ sku_name ที่ต้องใส่ ส่วน attribute อื่นมี fallback ของตัวเองแยกกัน

caller ที่ส่งค่าผิดรูปร่างสำหรับ type จะไม่มีทางไปถึง azurerm provider เลย — type-checking ของ Terraform เองปฏิเสธก่อน พร้อมระบุ variable และตำแหน่งชัดเจน:

variable "vm_count" {
type = number
}
# terraform.tfvars: vm_count = "not-a-number"
$ terraform plan
Planning failed. Terraform encountered an error while generating this plan.
╷
│ Error: Invalid value for input variable
│
│ on terraform.tfvars line 1:
│ 1: vm_count = "not-a-number"
│
│ The given value is not suitable for var.vm_count declared at
│ main.tf:1,1-26: a number is required.
╵

nullable = false บอกว่า caller ห้ามส่ง null มาให้ variable นี้ — แต่สิ่งที่เกิดขึ้นจริงขึ้นกับว่ามี default ด้วยหรือเปล่า พิสูจน์ด้วย block เดียวกันเป๊ะ ๆ กับ tfvars สองไฟล์ที่ต่างกัน:

variable "vm_size" {
type = string
nullable = false
default = "Standard_D2s_v3"
}
# terraform.tfvars: vm_size = null
$ terraform plan
Changes to Outputs:
+ vm_size = "Standard_D2s_v3"

พอมี default อยู่ null ที่ส่งมาชัด ๆ จะถูกแทนที่ด้วย default แบบเงียบ ๆ ไม่ error เลย เอา default ออกแล้วใช้ null ตัวเดียวกันเป๊ะ ๆ จะกลายเป็น error จริงแทน:

# same variable, no default, terraform.tfvars: vm_size = null
$ terraform plan
Planning failed. Terraform encountered an error while generating this plan.
╷
│ Error: Required variable not set
│
│ on terraform.tfvars line 1:
│ 1: vm_size = null
│
│ The given value is not suitable for var.vm_size defined at
│ main.tf:1,1-26: required variable may not be set to null.
╵

nullable = false เฉย ๆ ไม่ได้แปลว่า “error ทุกครั้งที่เจอ null” — มันแปลว่า “null ไม่มีทางเป็นค่าสุดท้าย” และ default คือกลไกที่ Terraform ใช้ resolve null ออกไปแทนที่จะปฏิเสธ

sensitive = true บน sql_admin_password มีผลแค่กับการ แสดงผล เท่านั้น Terraform จะซ่อนค่าใน output ของ plan/apply และใน CLI ให้เป็น (sensitive value) แต่ไม่ได้เข้ารหัสหรือตัดค่าออกจาก state file — module เรื่อง state management พูดไปแล้วว่า state เก็บทุก attribute ของทุก resource ที่ manage อยู่เป็น plain text ในไฟล์ JSON รวมถึงค่าที่มาจาก sensitive variable ด้วย ให้มอง sensitive = true เป็นแค่การป้องกันตอนแสดงผลบน terminal กับ CI log ไม่ใช่ตัวแทนของ remote backend ที่ควบคุม access ได้ดีอยู่แล้ว

default หรือ type constraint เช็คได้แค่รูปร่างของค่า ไม่ได้เช็คว่าค่านั้นสมเหตุสมผลไหม validation block เพิ่ม condition แบบ custom กับ error_message เฉพาะเจาะจง ทำให้ caller ได้ feedback ทันทีตอน plan แทนที่จะเจอ error งง ๆ จากลึก ๆ ใน azurerm provider

variable "location" {
type = string
description = "Azure region to deploy resources into, e.g. eastus, westeurope"
validation {
condition = can(regex("^(eastus|eastus2|westeurope|southeastasia)$", var.location))
error_message = "location must be one of: eastus, eastus2, westeurope, southeastasia."
}
}
variable "environment" {
type = string
description = "Deployment environment name"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment must be one of: dev, staging, prod."
}
}

ถ้าไม่มี validation block แรก การส่ง location = "mars" จะผ่าน terraform plan ไปเฉย ๆ แล้วไปพังตอน Terraform เรียก Azure Resource Manager API จริง ๆ เพื่อสร้าง resource group ใน region นั้น — feedback loop ช้ากว่ามาก และ error message ก็เฉพาะเจาะจงน้อยกว่า “location must be one of: eastus, eastus2, westeurope, southeastasia” มาก can() ที่ wrap ไว้ทำหน้าที่แปลง error จาก regex() ให้กลายเป็น false แทนที่จะ error ตรง ๆ ซึ่งตรงกับสิ่งที่ condition expression ต้องการพอดี เพราะต้อง evaluate ออกมาเป็น true หรือ false เสมอ ห้าม raise error

ก่อน 1.9 condition ของ validation block อ้างได้แค่ variable ตัวที่มันติดอยู่เท่านั้น Terraform 1.9 ยกเลิกข้อจำกัดนี้ — condition อ้าง variable ตัวอื่น ใน module เดียวกันได้แล้ว ซึ่งตรงกับที่คู่ input แบบ min/max ต้องการพอดี:

variable "min_instances" {
type = number
default = 5
}
variable "max_instances" {
type = number
validation {
condition = var.max_instances >= var.min_instances
error_message = "max_instances (${var.max_instances}) must be >= min_instances (${var.min_instances})."
}
}

error จริงเมื่อ max_instances = 2 เทียบกับ default ของ min_instances ที่ 5:

$ terraform plan
╷
│ Error: Invalid value for variable
│
│ on terraform.tfvars line 1:
│ 1: max_instances = 2
│ ├────────────────
│ │ var.max_instances is 2
│ │ var.min_instances is 5
│
│ max_instances (2) must be >= min_instances (5).
│
│ This was checked by the validation rule at variables.tf:8,3-13.
╵

สังเกต context block พิเศษที่ Terraform print เพิ่ม — var.max_instances is 2 / var.min_instances is 5 — ระบุ variable ทุกตัวที่ condition อ้างถึงจริง ๆ validation แบบ variable เดียวไม่มีทาง print แบบนี้ เพราะไม่มีอะไรอื่นให้ attribute error ไปหา

sensitive = true ซ่อนค่าจาก output แต่ยังเขียนลง state อยู่ดี ส่วน ephemeral = true (Terraform 1.10 ขึ้นไป) กันออกจริง ค่าใช้ได้ระหว่างที่ operation รันอยู่ และถูกตัดออกจากทั้ง state และ plan file ข้อแลกเปลี่ยนคือค่า ephemeral ไหลไปได้แค่ที่ที่ไม่เก็บค่าเหมือนกัน — write-only argument ของ resource, ephemeral variable หรือ resource ตัวอื่น, provider block หรือ provisioner การเอาไป assign ให้ argument ปกติของ resource เป็น error จริงที่จับได้ก่อน apply ไม่ใช่แค่ lint warning:

variable "session_token" {
type = string
ephemeral = true
default = "demo-token"
}
resource "local_file" "leak" {
filename = "${path.module}/leak.txt"
content = var.session_token
}
$ terraform validate
╷
│ Error: Invalid use of ephemeral value
│
│ with local_file.leak,
│ on main.tf line 15, in resource "local_file" "leak":
│ 15: content = var.session_token
│
│ Ephemeral values are not valid for "content", because it is not a
│ write-only attribute and must be persisted to state.
╵

Terraform จับตั้งแต่ตอน validate แบบ static เลย — ไม่ทันได้ไปถึง azurerm provider ด้วยซ้ำ เพราะ schema ของ argument content ไม่ได้ mark ว่าเป็น write-only และ Terraform รู้อยู่แล้วว่า argument ที่ไม่ใช่ write-only ทุกตัวจะถูกเก็บลง state บทเรื่อง Secrets and Credential Management แสดง pattern ที่ถูกต้องคู่กับ write-only argument (และ Azure Key Vault) ที่ปลายทาง

Terraform ยอมให้ตั้งค่า variable ตัวเดียวกันจากหลายที่พร้อมกัน แล้วแก้ conflict ด้วยลำดับความสำคัญที่ตายตัว เรียงจากสำคัญที่สุด (ชนะ) ไปน้อยที่สุด:

  1. -var และ -var-file บน command line ตามลำดับที่ใส่ — วิธีที่ explicit ที่สุดและชั่วคราวที่สุด
  2. ไฟล์ *.auto.tfvars / *.auto.tfvars.json ที่ auto-load จาก working directory ตามลำดับตัวอักษรของชื่อไฟล์
  3. terraform.tfvars / terraform.tfvars.json ที่ auto-load ถ้ามีไฟล์นี้อยู่
  4. environment variable TF_VAR_<name> — มีประโยชน์กับ CI pipeline และกับค่าที่ไม่อยากให้อยู่ในไฟล์เลย
  5. default ของ variable เอง — ใช้ก็ต่อเมื่อไม่มีอะไรข้างบนส่งค่ามาเลย
Terminal window
# Highest precedence: an explicit CLI flag for this one run
terraform apply -var="vm_size=Standard_D8s_v3"
# Or point at a specific tfvars file
terraform apply -var-file="prod.tfvars"
# Environment variable equivalent of setting vm_size
export TF_VAR_vm_size="Standard_D2s_v3"
terraform apply

ถ้า vm_size ถูกตั้งไว้ใน terraform.tfvars, ตั้งอีกทีใน prod.auto.tfvars, แล้วตั้งอีกทีผ่าน -var บน command line ค่าจาก -var จะชนะ Terraform ไม่ได้ merge ค่าแบบ scalar ข้ามแหล่งกัน แต่เลือกแหล่งที่ priority สูงสุดที่มีค่าอยู่เท่านั้น collection type อย่าง map กับ object เป็นข้อยกเว้นที่ควรจำไว้สำหรับบทหลัง ๆ — เครื่องมือบางตัว merge ค่าที่ nested กันแทนที่จะ override ทั้งก้อน แต่สำหรับแหล่งข้อมูลปกติข้างบนนี้ ค่าที่ประกาศไว้ในแหล่งที่ priority สูงกว่าจะแทนที่ค่าจากแหล่งที่ priority ต่ำกว่าไปเลยสำหรับ variable ตัวนั้น

พิสูจน์ครบทั้งห้าระดับตามลำดับ ด้วย variable "vm_size" { default = "Standard_D2s_v3" } ตัวเดียว เติมแหล่งที่ priority สูงกว่าไปทีละแหล่ง:

$ terraform plan # 1. default only
+ vm_size = "Standard_D2s_v3"
$ terraform plan # 2. terraform.tfvars now present
+ vm_size = "from-terraform-tfvars"
$ terraform plan # 3. zzz.auto.tfvars now also present
+ vm_size = "from-auto-tfvars"
$ TF_VAR_vm_size="from-env-var" terraform plan # 4. env var too -- auto.tfvars still wins
+ vm_size = "from-auto-tfvars"
$ TF_VAR_vm_size="from-env-var" \
terraform plan -var="vm_size=from-cli-var" # 5. -var beats everything
+ vm_size = "from-cli-var"

step 4 คือจุดที่ควรหยุดคิดสักนิด — TF_VAR_* ไม่ ชนะไฟล์ *.auto.tfvars ถึงแม้ env var จะถูกตั้งค่า หลัง ไฟล์นั้นมีอยู่แล้วก็ตาม — precedence คือเรื่องของ “ประเภทแหล่งข้อมูล” ไม่ใช่ “คำสั่งไหนพิมพ์ล่าสุด” ถ้าไม่มีไฟล์ tfvars เลย TF_VAR_vm_size ตัวเดียวกันจะชนะ default ได้ ยืนยันว่า TF_VAR_* อยู่เหนือ default แต่อยู่ใต้ทุกแหล่งที่เป็นไฟล์ tfvars

flowchart LR
  a["-var / -var-file (CLI flag)"] --> r["Resolved value of var.vm_size"]
  b["*.auto.tfvars (alphabetical)"] --> r
  c["terraform.tfvars"] --> r
  d["TF_VAR_vm_size (env var)"] --> r
  e["default in the variable block"] --> r
  r --> res["azurerm_linux_virtual_machine.web"]
หลายแหล่งข้อมูลถูก resolve เหลือค่าเดียวต่อ variable ตามลำดับความสำคัญ

ทุกครั้งที่ resolve input variable จริง ๆ คือการตอบสองคำถามแยกกันตามลำดับ: แหล่งไหนชนะ (precedence chain ที่ resolve เสร็จก่อน HCL ใน module จะรันด้วยซ้ำ) กับ ค่าที่ชนะนั้นยอมรับได้ไหม (type, nullable, validation ตามลำดับนี้ ก่อนที่จะไปถึง azurerm provider เลย) ephemeral กับ write-only argument เพิ่มแกนที่สามที่ตั้งฉากกับสองแกนแรก — ไม่ใช่ “ค่าอะไร” หรือ “valid ไหม” แต่คือ “ค่านี้มีสิทธิ์แตะ disk ได้หรือเปล่า” การแยกสามคำถามนี้ออกจากกันอธิบายทุก proof ข้างบน — cross-variable validation ที่ fail ระบุชื่อ variable ตัวอื่น เพราะ condition อ่านมันจริง ๆ; nullable = false ที่มี default ไม่ error เพราะขั้น “ค่ายอมรับได้ไหม” รันหลังจากแทน null ด้วย default ไปแล้ว ไม่ใช่ก่อน

ตัวเลือกได้อะไรเสียอะไร
validation block (รวม cross-variable, 1.9+)พังเร็วตอน plan พร้อม message เฉพาะเจาะจง (พิสูจน์ข้างบน) แทนที่จะพังลึก ๆ ใน providerทุก rule ถูกเช็คทุกครั้งที่ plan — rule ที่เรียก function แพง ๆ (can(regex(...)) กับ string ยาว ๆ) มี cost จริงที่ซ้ำทุกครั้ง
ephemeral = trueค่าไม่แตะ state หรือ plan file เลย — หายไปจริงหลัง operation จบ (พิสูจน์: ถูกปฏิเสธก่อนจะไปรั่วเข้า content)ไหลไปได้แค่ write-only argument, ephemeral value ตัวอื่น, provider block, หรือ provisioner — argument ปกติของ resource คือ error จริง ไม่ใช่ fallback
nullable = false + defaultcaller ส่ง null ได้และได้ default แบบเงียบ ๆ โดยไม่ต้องเขียน coalesce() เอง (พิสูจน์ข้างบน)การแทนค่าแบบเงียบ ๆ อาจซ่อนความผิดพลาดของ caller ไว้ — null ที่ตั้งใจสื่อความหมายบางอย่างกลายเป็นแค่ “ใช้ default”
optional() บน object attributeเฉพาะ key ที่ required จริง ๆ เท่านั้นที่ต้องใส่ key อื่นมี default ของตัวเองแยกกัน (พิสูจน์ผ่าน terraform console)ถ้า optional key พิมพ์ชื่อผิด จะถูกรับเป็น attribute เกินที่ไม่รู้จักก็ต่อเมื่อ object type ถูกขยายทีหลังเท่านั้น — วันนี้ validate/harness ยังจับ unknown attribute ได้อยู่
  • “-var="x=null" เคลียร์ string variable ให้เป็น null ได้” พิสูจน์ข้างบน (proof เรื่อง nullable): สำหรับ variable type string, -var จะรับ null เป็น HCL ก็จริง แต่ caller ที่พิมพ์ -var="x=null" ใน shell ที่ strip quote ไปแล้วอาจจบลงด้วยการส่ง string สามตัวอักษรตามตัวอักษร "null" ไม่ใช่ค่า null จริง — SpotTheBug ข้างล่างโชว์ความต่างชัด ๆ ด้วย plan output จริง
  • “nullable = false ปฏิเสธ null เสมอ” พิสูจน์ข้างบน: ถ้ามี default อยู่ null จะถูกแทนที่ด้วย default แบบเงียบ ๆ ไม่ error เลย มีแค่ nullable = false ที่ไม่มีdefault เท่านั้นที่ปฏิเสธ null ด้วย “required variable may not be set to null”
  • “sensitive = true ทำให้ค่าไม่เข้า state file” ไม่จริง — พิสูจน์ในโมดูล state-management ว่า state เก็บทุก attribute เป็น plain text ไม่ว่าจะ sensitive หรือเปล่า มีแค่ ephemeral = true (กับ write-only argument) เท่านั้นที่กันค่าไม่ให้เข้า state ได้จริง และกันได้แค่กับ argument ที่รองรับ
  • “environment variable TF_VAR_* ถูกเช็คก่อนไฟล์ tfvars เพราะ env var รู้สึก ‘สด’ กว่า” พิสูจน์ข้างบน: TF_VAR_* อยู่ต่ำกว่า terraform.tfvars กับ *.auto.tfvars ใน precedence ไม่ใช่สูงกว่า
  • validation block ที่ condition อ้าง variable ตัวอื่น แต่ไม่เคยเช็คว่า Terraform version รองรับไหม (1.9+) — เช็ค terraform version จริง อย่าเชื่อสมมติฐานของ model
  • -var ที่ใช้ “unset” variable ให้เป็น null — พิสูจน์ข้างล่างว่าสำหรับ variable type string นี่ไม่การันตี — null จริงต้องมาจาก HCL literal null จริง ๆ (ผ่าน -var-file หรือ entry ใน .tfvars) ไม่ใช่ quoted string ใน shell
  • nullable = false ที่ใส่เพื่อ “บังคับ required” โดยไม่เช็คว่ามี default ด้วยหรือเปล่า — พิสูจน์ข้างบนว่าถ้ามีทั้งคู่ Terraform จะแทน default แบบเงียบ ๆ ไม่ error
  • object({...}) ที่ทุก attribute เป็น required ทั้งที่ prose บอกว่า “field ส่วนใหญ่ optional” — เช็คว่าขาด optional() wrapper หรือเปล่า
  • sensitive = true ที่ถูกมองว่าเพียงพอสำหรับจัดการ secret — พิสูจน์แล้วในคอร์สนี้ว่า state ยังเก็บค่าดิบอยู่ มีแค่ ephemeral/write-only เท่านั้นที่กันการ persist ได้จริง
  • ตัวแปร ephemeral = true ที่ generate มาแล้วถูก assign เข้า argument ปกติของ resource ตรง ๆ (เช่น content, tags, argument ไหนก็ตามที่ไม่ใช่ write-only) — นี่คือ error จริงตอน terraform validate ไม่ใช่อะไรที่เพิ่งไปโผล่ตอน apply

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

variable "vm_size" {
type = string
nullable = false
default = "Standard_D2s_v3"
}
# CI job "clears" the variable between runs:
# terraform plan -var="vm_size=null"
เฉลย

โค้ดนี้ไม่ได้เคลียร์ variable ให้เป็น null — สำหรับ variable type string, -var="vm_size=null" ส่ง string สามตัวอักษรตามตัวอักษร "null" ไม่ใช่ค่า null จริง พิสูจน์ด้วย block เดียวกันเป๊ะ ๆ:

$ terraform plan -var="vm_size=null"
+ vm_size = "null"

เทียบกับการส่ง null จริงผ่านไฟล์ .tfvars (vm_size = null เป็น HCL literal ไม่ใช่ quoted string ใน shell) ซึ่ง fallback ไปที่ default ได้ถูกต้อง:

$ terraform plan # terraform.tfvars: vm_size = null
+ vm_size = "Standard_D2s_v3"

ทางแก้คือใช้ HCL null จริง (entry ใน -var-file/.tfvars — มีแค่ type อื่นที่ไม่ใช่ string หรือ null literal ในไฟล์เท่านั้นที่ไม่กำกวม) ไม่ใช่ bare -var flag เมื่อเป้าหมายคือ null จริง ๆ

validation block บน variable ป้องกันอะไรจริง ๆ
ถ้า variable ตัวหนึ่งมีค่าใน terraform.tfvars, ใน TF_VAR_ environment variable, และส่งผ่าน -var บน command line Terraform จะใช้ค่าไหน
sensitive = true บน variable ป้องกันอะไรจริง ๆ
ระหว่างไฟล์ *.auto.tfvars กับ terraform.tfvars ถ้าตั้ง variable ตัวเดียวกัน อันไหนชนะ
Terraform version ไหนที่เริ่มให้ validation condition อ้าง variable ตัวอื่นแบบนี้ได้
variable "max_instances" {
  type = number
  validation {
    condition     = var.max_instances >= var.min_instances
    error_message = "max_instances must be >= min_instances."
  }
}
รัน terraform validate กับ config นี้จะเกิดอะไรขึ้น
variable "session_token" {
  type      = string
  ephemeral = true
  default   = "demo-token"
}

resource "local_file" "leak" {
  filename = "out.txt"
  content  = var.session_token
}
variable ตัวหนึ่งประกาศ nullable = false พร้อม default caller ส่ง null มาชัด ๆ ผ่านไฟล์ .tfvars จะเกิดอะไรขึ้น