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

Testing Modules

terraform validate, terraform plan และ terraform test แต่ละตัวตอบคำถามคนละข้อ — “syntax ถูกไหม” “Azure ยอมรับไหม” และ “ทำงานตามที่คาดไว้ไหม” — การเอาตัวใดตัวหนึ่งมาแทนอีกสองตัวคือวิธีที่ module พังไปถึง production ทั้ง ๆ ที่ผ่านสีเขียวมาตลอดทาง

น่าล่อใจที่จะมองว่า “ไม่มี error” แปลว่า “ทำงานได้” แต่จริง ๆ แล้ว Terraform ให้ check สามแบบที่ต่างกันชัดเจน แต่ละแบบจับ mistake คนละประเภท และแต่ละแบบที่ผ่านก็แทบไม่บอกอะไรเลยเกี่ยวกับแบบที่อยู่ถัดไป

terraform validate เช็คแค่ HCL syntax กับความสอดคล้องภายในของ configuration เท่านั้น โดยยืนยันว่า block ของคุณเขียนถูกรูปแบบ argument type ตรงกับที่ resource ต้องการ และ reference อย่าง var.name หรือ module.storage.primary_blob_endpoint ชี้ไปที่สิ่งที่มีอยู่จริง ที่สำคัญคือ validate ไม่คุยกับ Azure เลย จึงไม่มีทางรู้ว่า VM size ที่คุณพิมพ์ไปมีอยู่จริงใน region ที่คุณเลือกหรือเปล่า หรือชื่อ storage account ที่คุณตั้งถูกใช้ไปแล้วโดย subscription คนอื่น configuration หนึ่งสามารถผ่าน validate ได้อย่างสะอาดหมดจด แล้วยังพังทันทีที่แตะ Azure API จริง

Terminal window
terraform validate
# Success! The configuration is valid.
#
# ...but validate has no idea whether "Standard_ZZZ99" is a real VM size,
# or whether "acmeapplogs" is already taken as a storage account name.

terraform plan ก้าวไปอีกขั้นจริง ๆ โดย authenticate เข้า Azure resolve data source และถาม provider ว่าจะเกิดอะไรขึ้น ตรงนี้คือจุดที่ mistake เรื่อง region กับ SKU, ชื่อชนกัน, หรือปัญหา quota จะโผล่ขึ้นมาจริง เพราะ plan คุยกับ Azure API จริงและแสดง diff ที่จะ apply สิ่งที่ plan ไม่ ทำคือ assert อะไรเลยว่า diff นั้น ถูกต้อง แค่แสดง plan ให้คุณดู ไม่ได้เช็ค plan นั้นกับสิ่งที่คุณคาดหวังไว้ ไม่มีอะไรหยุด plan จากการสำเร็จ ทั้งที่เงียบ ๆ สร้าง storage account ที่มี replication type ผิด เพราะไม่มีใครบอก Terraform ว่า “ถูกต้อง” สำหรับ module นี้แปลว่าอะไร

Terminal window
terraform plan
# Plan: 1 to add, 0 to change, 0 to destroy.
#
# This confirms Azure accepts the request. It does not confirm the
# storage account it is about to create actually has the settings
# this module was supposed to guarantee.

terraform test คือตัวเดียวที่ assert อะไรบางอย่างจริง ๆ เป็น native testing framework ของ Terraform และเป็นตัวเดียวในสามตัวนี้ที่ encode ว่า “นี่คือ behavior เฉพาะที่ฉันคาดหวัง” แล้ว fail ทันทีที่ความเป็นจริงต่างออกไป ส่วน validate เช็ครูปแบบ และ plan เช็คว่า Azure ยอมรับ request นั้น test เช็คผลลัพธ์

test file อยู่ใน extension .tftest.hcl ตามธรรมเนียมมักอยู่ใต้ directory tests/ และรันด้วยคำสั่ง terraform test test file ประกอบด้วย block สองแบบ: variables block ระดับบนสุดที่ให้ input value เฉพาะสำหรับ test และ run block หนึ่งตัวหรือมากกว่า แต่ละตัวรัน configuration แล้วเช็คด้วย assert block หนึ่งตัวหรือมากกว่า

tests/storage_account_name.tftest.hcl
variables {
resource_group_name = "rg-app-logs"
}
run "creates_storage_account_with_expected_name" {
command = plan
variables {
name = "acmeapplogs"
}
assert {
condition = azurerm_storage_account.this.name == "acmeapplogs"
error_message = "Storage account name did not match the name variable passed in"
}
}

assert block แต่ละตัวต้องการสองอย่างเป๊ะ ๆ: condition ที่ต้อง evaluate ออกมาเป็น true และ error_message string ที่โชว์ให้คุณเห็นทันทีที่ condition นั้นเป็น false variables block ข้างใน run block override หรือให้ value ที่ scope เฉพาะ run นั้น ส่วน variables block ระดับบนสุดตั้ง default ที่แชร์กันทุก run block ใน file — ตัวอย่างนี้คือ module storage-account จากบทก่อนหน้าในหน่วยนี้เป๊ะ ๆ ที่ถูกส่ง name = "acmeapplogs" เข้าไป แล้ว assert ว่าสร้าง resource ที่มีชื่อนั้นจริง

run block ทุกตัวมี command argument และการเลือกระหว่างสองค่านั้น คือ plan กับ apply เป็นการตัดสินใจที่สำคัญที่สุดใน test file

command = plan เร็วและไม่สร้าง resource จริงเลย Terraform สร้าง plan และ assert block ของคุณเช็ค planned value — นี่คือสิ่งที่ตัวอย่างข้างบนทำเป๊ะ ๆ เพราะ name มาจาก variable ตรง ๆ และรู้ค่าได้ก่อนที่จะมี apply ใด ๆ เกิดขึ้น ใช้ command = plan สำหรับ test case ส่วนใหญ่: อะไรก็ตามที่คุณ verify ได้จาก configuration อย่างเดียว อย่าง “input นี้ทำให้เกิด attribute ที่คาดไว้ไหม” ควรอยู่ตรงนี้

command = apply สร้าง resource จริงใน Azure จริง ๆ assert กับ attribute จริงที่ได้ผลลัพธ์ออกมา แล้ว teardown ทุกอย่างอีกครั้งเมื่อ test file รันจบ นี่คือ integration testing จริง ๆ — เพราะเป็นวิธีเดียวที่ assert กับ value ที่ Azure คำนวณให้คุณได้ เช่น endpoint ที่ถูก generate หรือ resource ID เพราะ value นั้นไม่มีอยู่จริงจนกว่าจะมีการ apply เกิดขึ้นจริง

tests/storage_account_endpoint.tftest.hcl
variables {
name = "acmeapplogs"
resource_group_name = "rg-app-logs"
}
run "creates_storage_account_with_working_endpoint" {
command = apply
assert {
condition = output.primary_blob_endpoint != ""
error_message = "Expected a non-empty primary_blob_endpoint after apply"
}
}

command = apply ช้ากว่าและมีค่าใช้จ่ายจาก cloud resource จริงในช่วงสั้น ๆ เพราะ Terraform provision แล้วก็ destroy จริง ๆ เป็นส่วนหนึ่งของการรัน test ใช้อย่างตั้งใจ สำหรับ assertion ที่ต้องการ Azure-computed value จริง ๆ เท่านั้น ไม่ใช่เป็นค่า default ของทุก run block ใน file

flowchart TB
  subgraph rung1["terraform validate"]
    v1["HCL syntax and internal consistency"] --> v2["never talks to Azure"]
  end
  subgraph rung2["terraform plan"]
    p1["talks to the real Azure API"] --> p2["shows the diff, asserts nothing about it"]
  end
  subgraph rung3["terraform test"]
    t1["run blocks with assert"] --> t2["condition and error_message enforce expected behavior"]
  end
  rung1 -->|catches malformed config| rung2
  rung2 -->|catches provider-side errors| rung3
  rung3 -->|catches wrong results| done["confidence the module actually works"]
Three levels of confidence, each catching a different class of mistake
terraform validate จับอะไรได้ และจับอะไรไม่ได้เลย
ทำไม terraform plan ที่ผ่านแบบสะอาดหมดจดถึงไม่รับประกันว่า module ทำงานได้ตามที่ตั้งใจจริง
ใน run block ของ .tftest.hcl ความต่างจริง ๆ ระหว่าง command = plan กับ command = apply คืออะไร
ทำไม test ที่ต้องการ assert กับ value อย่าง primary_blob_endpoint ถึงต้องใช้ command = apply แทน command = plan