Input Variables
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”input variable คือ parameter ที่มีชื่อและมี type ชัดเจน ทำให้ config เดียวกันสร้าง infrastructure ต่างกันได้ตามคนเรียกและค่าที่ส่งเข้ามา
ประกาศ variable block
หัวข้อที่มีชื่อว่า “ประกาศ variable block”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 "instance_count" { type = number description = "Number of Compute Engine instances to launch" default = 2}
variable "enable_deletion_protection" { type = bool description = "Whether to enable deletion protection on the Compute Engine instance" default = true}
variable "allowed_ports" { type = list(number) description = "TCP ports allowed inbound on the firewall rule" default = [22, 443]}
variable "resource_labels" { type = map(string) description = "Labels applied to every resource in this configuration" default = { project = "payments" team = "platform" }}
variable "db_config" { type = object({ database_version = string tier = string disk_size_gb = number }) description = "Structured configuration for the Cloud SQL instance" default = { database_version = "POSTGRES_15" tier = "db-custom-1-3840" disk_size_gb = 20 }}
variable "db_password" { type = string description = "Password for the Cloud SQL application user" sensitive = true}object({...}) ควรพูดถึงแยกไว้ต่างหาก เป็น structure ที่มี type และรูปร่างตายตัว ทุก key ที่ list ไว้ต้อง required (เว้นแต่จะใส่ default ผ่าน optional()) และ Terraform ปฏิเสธค่าที่ caller ส่งมาผิดรูปร่างตั้งแต่ตอน plan ก่อนที่จะไปเรียก GCP API เลยด้วยซ้ำ
Optional attribute ใน object ด้วย optional()
หัวข้อที่มีชื่อว่า “Optional attribute ใน object ด้วย optional()”การ list ทุก key เป็น required ทำให้ object type เปราะ — caller ที่สนใจแค่ database_version ก็ยังต้องใส่ทุก field ที่เหลือครบ optional(<type>, <default>) ข้างใน object({...}) ทำเครื่องหมายว่า attribute นั้นไม่ required และมี default ของตัวเอง แยกจาก default ระดับบนสุดของ variable:
variable "db_config" { type = object({ database_version = string tier = optional(string, "db-custom-1-3840") disk_size_gb = optional(number, 20) availability_type = optional(string, "ZONAL") }) default = { database_version = "POSTGRES_15" }}session terraform console จริงกับ block นี้เป๊ะ ๆ evaluate var.db_config โดยใส่แค่ database_version — Terraform เติมทุก attribute ที่เว้นไว้จาก optional() ให้เองตาม default:
$ echo 'var.db_config' | terraform console{ "availability_type" = "ZONAL" "database_version" = "POSTGRES_15" "disk_size_gb" = 20 "tier" = "db-custom-1-3840"}ต่างจาก object({...}) เปล่า ๆ ข้างบน — ถ้าไม่มี optional() caller ที่ไม่ใส่ tier จะเจอ error “attribute is required” ทันทีตอน plan แต่มี optional() แล้ว object จะ required แค่บางส่วนจริง ๆ — มีแค่ database_version ที่ต้องใส่ ส่วน attribute อื่นมี fallback ของตัวเองแยกกัน
Type-mismatch error เกิดก่อนที่จะไปถึง provider
หัวข้อที่มีชื่อว่า “Type-mismatch error เกิดก่อนที่จะไปถึง provider”caller ที่ส่งค่าผิดรูปร่างสำหรับ type จะไม่มีทางไปถึง Google provider เลย — type-checking ของ Terraform เองปฏิเสธก่อน พร้อมระบุ variable และตำแหน่งชัดเจน:
variable "instance_count" { type = number}# terraform.tfvars: instance_count = "not-a-number"$ terraform planPlanning failed. Terraform encountered an error while generating this plan.
╷│ Error: Invalid value for input variable││ on terraform.tfvars line 1:│ 1: instance_count = "not-a-number"││ The given value is not suitable for var.instance_count declared at│ main.tf:1,1-26: a number is required.╵nullable = false: ห้าม null แต่ไม่เสมอไป
หัวข้อที่มีชื่อว่า “nullable = false: ห้าม null แต่ไม่เสมอไป”nullable = false บอกว่า caller ห้ามส่ง null มาให้ variable นี้ — แต่สิ่งที่เกิดขึ้นจริงขึ้นกับว่ามี default ด้วยหรือเปล่า พิสูจน์ด้วย block เดียวกันเป๊ะ ๆ กับ tfvars สองไฟล์ที่ต่างกัน:
variable "zone" { type = string nullable = false default = "us-central1-a"}# terraform.tfvars: zone = null$ terraform planChanges to Outputs: + zone = "us-central1-a"พอมี default อยู่ null ที่ส่งมาชัด ๆ จะถูกแทนที่ด้วย default แบบเงียบ ๆ ไม่ error เลย เอา default ออกแล้วใช้ null ตัวเดียวกันเป๊ะ ๆ จะกลายเป็น error จริงแทน:
# same variable, no default, terraform.tfvars: zone = null$ terraform planPlanning failed. Terraform encountered an error while generating this plan.
╷│ Error: Required variable not set││ on terraform.tfvars line 1:│ 1: zone = null││ The given value is not suitable for var.zone 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 บน db_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 ได้ดีอยู่แล้ว
ตรวจสอบ variable ด้วย validation block
หัวข้อที่มีชื่อว่า “ตรวจสอบ variable ด้วย validation block”default หรือ type constraint เช็คได้แค่รูปร่างของค่า ไม่ได้เช็คว่าค่านั้นสมเหตุสมผลไหม validation block เพิ่ม condition แบบ custom กับ error_message เฉพาะเจาะจง ทำให้ caller ได้ feedback ทันทีตอน plan แทนที่จะเจอ error งง ๆ จากลึก ๆ ใน Google provider
variable "zone" { type = string description = "GCP zone the resources are deployed into, e.g. us-central1-a"
validation { condition = can(regex("^[a-z]+-[a-z0-9]+-[a-z]$", var.zone)) error_message = "zone must look like a GCP zone, e.g. us-central1-a or europe-west1-b." }}
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 แรก การส่ง zone = "not-a-real-zone" จะผ่าน terraform plan ไปเฉย ๆ แล้วไปพังตอน Terraform เรียก Google provider จริง ๆ เพื่อสร้าง resource ใน zone นั้น — feedback loop ช้ากว่ามาก และ error message ก็เฉพาะเจาะจงน้อยกว่า “zone must look like a GCP zone” มาก can() ที่ wrap ไว้ทำหน้าที่แปลง error จาก regex() ให้กลายเป็น false แทนที่จะ error ตรง ๆ ซึ่งตรงกับสิ่งที่ condition expression ต้องการพอดี เพราะต้อง evaluate ออกมาเป็น true หรือ false เสมอ ห้าม raise error
Cross-variable validation (Terraform 1.9 ขึ้นไป)
หัวข้อที่มีชื่อว่า “Cross-variable validation (Terraform 1.9 ขึ้นไป)”ก่อน 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 ไปหา
ค่า ephemeral ที่ไหลไปที่ที่ต้อง persist
หัวข้อที่มีชื่อว่า “ค่า ephemeral ที่ไหลไปที่ที่ต้อง persist”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 เลย — ไม่ทันได้ไปถึง Google provider ด้วยซ้ำ เพราะ schema ของ argument content ไม่ได้ mark ว่าเป็น write-only และ Terraform รู้อยู่แล้วว่า argument ที่ไม่ใช่ write-only ทุกตัวจะถูกเก็บลง state บทเรื่อง Secrets and Credential Management แสดง pattern ที่ถูกต้องคู่กับ write-only argument ที่ปลายทาง
ตั้งค่า variable และใครชนะ
หัวข้อที่มีชื่อว่า “ตั้งค่า variable และใครชนะ”Terraform ยอมให้ตั้งค่า variable ตัวเดียวกันจากหลายที่พร้อมกัน แล้วแก้ conflict ด้วยลำดับความสำคัญที่ตายตัว เรียงจากสำคัญที่สุด (ชนะ) ไปน้อยที่สุด:
-varและ-var-fileบน command line ตามลำดับที่ใส่ — วิธีที่ explicit ที่สุดและชั่วคราวที่สุด- ไฟล์
*.auto.tfvars/*.auto.tfvars.jsonที่ auto-load จาก working directory ตามลำดับตัวอักษรของชื่อไฟล์ terraform.tfvars/terraform.tfvars.jsonที่ auto-load ถ้ามีไฟล์นี้อยู่- environment variable
TF_VAR_<name>— มีประโยชน์กับ CI pipeline และกับค่าที่ไม่อยากให้อยู่ในไฟล์เลย defaultของ variable เอง — ใช้ก็ต่อเมื่อไม่มีอะไรข้างบนส่งค่ามาเลย
# Highest precedence: an explicit CLI flag for this one runterraform apply -var="zone=europe-west1-b"
# Or point at a specific tfvars fileterraform apply -var-file="prod.tfvars"
# Environment variable equivalent of setting zoneexport TF_VAR_zone="us-east1-b"terraform applyถ้า zone ถูกตั้งไว้ใน terraform.tfvars, ตั้งอีกทีใน prod.auto.tfvars, แล้วตั้งอีกทีผ่าน -var บน command line ค่าจาก -var จะชนะ Terraform ไม่ได้ merge ค่าแบบ scalar ข้ามแหล่งกัน แต่เลือกแหล่งที่ priority สูงสุดที่มีค่าอยู่เท่านั้น collection type อย่าง map กับ object เป็นข้อยกเว้นที่ควรจำไว้สำหรับบทหลัง ๆ — เครื่องมือบางตัว merge ค่าที่ nested กันแทนที่จะ override ทั้งก้อน แต่สำหรับแหล่งข้อมูลปกติข้างบนนี้ ค่าที่ประกาศไว้ในแหล่งที่ priority สูงกว่าจะแทนที่ค่าจากแหล่งที่ priority ต่ำกว่าไปเลยสำหรับ variable ตัวนั้น
พิสูจน์ครบทั้งห้าระดับตามลำดับ ด้วย variable "zone" { default = "us-central1-a" } ตัวเดียว เติมแหล่งที่ priority สูงกว่าไปทีละแหล่ง:
$ terraform plan # 1. default only + zone = "us-central1-a"
$ terraform plan # 2. terraform.tfvars now present + zone = "from-terraform-tfvars"
$ terraform plan # 3. zzz.auto.tfvars now also present + zone = "from-auto-tfvars"
$ TF_VAR_zone="from-env-var" terraform plan # 4. env var too -- auto.tfvars still wins + zone = "from-auto-tfvars"
$ TF_VAR_zone="from-env-var" \ terraform plan -var="zone=from-cli-var" # 5. -var beats everything + zone = "from-cli-var"step 4 คือจุดที่ควรหยุดคิดสักนิด — TF_VAR_* ไม่ ชนะไฟล์ *.auto.tfvars ถึงแม้ env var จะถูกตั้งค่า หลัง ไฟล์นั้นมีอยู่แล้วก็ตาม — precedence คือเรื่องของ “ประเภทแหล่งข้อมูล” ไม่ใช่ “คำสั่งไหนพิมพ์ล่าสุด” ถ้าไม่มีไฟล์ tfvars เลย TF_VAR_zone ตัวเดียวกันจะชนะ default ได้ ยืนยันว่า TF_VAR_* อยู่เหนือ default แต่อยู่ใต้ทุกแหล่งที่เป็นไฟล์ tfvars
flowchart LR a["-var / -var-file (CLI flag)"] --> r["Resolved value of var.zone"] b["*.auto.tfvars (alphabetical)"] --> r c["terraform.tfvars"] --> r d["TF_VAR_zone (env var)"] --> r e["default in the variable block"] --> r r --> res["google_compute_instance.web"]
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ทุกครั้งที่ resolve input variable จริง ๆ คือการตอบสองคำถามแยกกันตามลำดับ: แหล่งไหนชนะ (precedence chain ที่ resolve เสร็จก่อน HCL ใน module จะรันด้วยซ้ำ) กับ ค่าที่ชนะนั้นยอมรับได้ไหม (type, nullable, validation ตามลำดับนี้ ก่อนที่จะไปถึง Google 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 + default | caller ส่ง 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 typestring,-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 ไม่ใช่สูงกว่า
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”validationblock ที่ condition อ้าง variable ตัวอื่น แต่ไม่เคยเช็คว่า Terraform version รองรับไหม (1.9+) — เช็คterraform versionจริง อย่าเชื่อสมมติฐานของ model-varที่ใช้ “unset” variable ให้เป็นnull— พิสูจน์ข้างล่างว่าสำหรับ variable typestringนี่ไม่การันตี —nullจริงต้องมาจาก HCL literalnullจริง ๆ (ผ่าน-var-fileหรือ entry ใน.tfvars) ไม่ใช่ quoted string ใน shellnullable = falseที่ใส่เพื่อ “บังคับ required” โดยไม่เช็คว่ามีdefaultด้วยหรือเปล่า — พิสูจน์ข้างบนว่าถ้ามีทั้งคู่ Terraform จะแทน default แบบเงียบ ๆ ไม่ errorobject({...})ที่ทุก attribute เป็น required ทั้งที่ prose บอกว่า “field ส่วนใหญ่ optional” — เช็คว่าขาดoptional()wrapper หรือเปล่าsensitive = trueที่ถูกมองว่าเพียงพอสำหรับจัดการ secret — พิสูจน์แล้วในคอร์สนี้ว่า state ยังเก็บค่าดิบอยู่ มีแค่ephemeral/write-only เท่านั้นที่กันการ persist ได้จริง- ตัวแปร
ephemeral = trueที่ generate มาแล้วถูก assign เข้า argument ปกติของ resource ตรง ๆ (เช่นcontent,labels, argument ไหนก็ตามที่ไม่ใช่ write-only) — นี่คือ error จริงตอนterraform validateไม่ใช่อะไรที่เพิ่งไปโผล่ตอน apply
🐛 หา bug ในโค้ดนี้
variable "zone" {type = stringnullable = falsedefault = "us-central1-a"}
# CI job "clears" the variable between runs:# terraform plan -var="zone=null"เฉลย
โค้ดนี้ไม่ได้เคลียร์ variable ให้เป็น null — สำหรับ variable type string, -var="zone=null" ส่ง string สามตัวอักษรตามตัวอักษร "null" ไม่ใช่ค่า null จริง พิสูจน์ด้วย block เดียวกันเป๊ะ ๆ:
$ terraform plan -var="zone=null" + zone = "null"เทียบกับการส่ง null จริงผ่านไฟล์ .tfvars (zone = null เป็น HCL literal ไม่ใช่ quoted string ใน shell) ซึ่ง fallback ไปที่ default ได้ถูกต้อง:
$ terraform plan # terraform.tfvars: zone = null + zone = "us-central1-a"ทางแก้คือใช้ HCL null จริง (entry ใน -var-file/.tfvars — มีแค่ type อื่นที่ไม่ใช่ string หรือ null literal ในไฟล์เท่านั้นที่ไม่กำกวม) ไม่ใช่ bare -var flag เมื่อเป้าหมายคือ null จริง ๆ