Dependencies Between Units
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”dependency block ทำให้ unit หนึ่งอ่าน output ของอีก unit ข้าม state boundary ที่แยกทั้งสองออกจากกันได้ โดย mock_outputs ทำหน้าที่แทนค่าจริงไปก่อน จนกว่าจะ apply unit อีกฝั่งจริง
ทำไม module composition อย่างเดียวไม่พออีกต่อไป
หัวข้อที่มีชื่อว่า “ทำไม module composition อย่างเดียวไม่พออีกต่อไป”โมดูล Modules ก่อนหน้านี้แสดงให้เห็นว่า output ของ module หนึ่งไหลตรงเข้าไปเป็น input ของอีก module ได้ ทั้งหมดอยู่ใน main.tf เดียว ถูก resolve โดย Terraform ใน plan เดียวกัน กับ state ที่แชร์กันชุดเดียว วิธีนี้ทำงานได้เพราะทั้งสอง module เป็นส่วนหนึ่งของ Terraform run เดียวกัน Terraform จึงเห็น dependency graph ทั้งหมดในครั้งเดียว
unit ตั้งใจทำลายสมมติฐานนั้น unit vnet และ unit vm ต่างมี terragrunt.hcl และ state file แยกกันของตัวเอง apply แยกกันอย่างอิสระ unit vm ยังต้องการ subnet ID ที่ vnet สร้างขึ้นอยู่ดี แต่ไม่มี Terraform run เดียวที่เห็นทั้งสอง unit นี้อีกต่อไป จากมุมมองของ Terraform ทั้งสอง unit เป็นโปรเจกต์คนละเรื่องกันโดยสิ้นเชิง Terragrunt จึงต้องมีกลไกของตัวเองสำหรับส่งค่าจาก state ของ unit หนึ่ง ข้าม boundary นั้นเข้าไปใน inputs ของอีก unit และกลไกนั้นคือ dependency block
dependency block: ข้าม state boundary
หัวข้อที่มีชื่อว่า “dependency block: ข้าม state boundary”dependency block ระบุชื่อ unit อื่นด้วย directory ของ unit นั้น แล้ว expose ทุก output ของ unit นั้นออกมา
include "root" { path = find_in_parent_folders("root.hcl")}
dependency "vnet" { config_path = "../vnet"}
inputs = { subnet_id = dependency.vnet.outputs.subnet_id}config_path = "../vnet" ชี้ไปที่ directory ของ unit vnet เบื้องหลัง Terragrunt จะรัน terragrunt output กับ unit นั้น แล้ว expose ผลลัพธ์ออกมาภายใต้ dependency.vnet.outputs ทำให้ dependency.vnet.outputs.subnet_id เข้าถึงสิ่งที่ Terraform module ของ unit vnet ประกาศไว้เป็น output "subnet_id" ได้โดยตรง จาก terragrunt.hcl ของ unit vm เอง โค้ดตรงนี้อ่านได้เกือบเหมือนกับการใช้ output ของ module ภายใน main.tf เดียวกันเลย ความต่างมองไม่เห็นในตัว syntax แม้ว่าจริง ๆ แล้วมี apply และ state file แยกกันโดยสิ้นเชิงอยู่เบื้องหลัง dependency.vnet.outputs
mock_outputs: ให้ plan สำเร็จได้ก่อนเวลา
หัวข้อที่มีชื่อว่า “mock_outputs: ให้ plan สำเร็จได้ก่อนเวลา”มีปัญหาชัดเจนอยู่ในโครงสร้างข้างต้น ครั้งแรกที่ใครก็ตามรัน terragrunt plan กับ unit vm unit vnet อาจจะยังไม่ถูก apply เลยก็ได้ ทำให้ dependency.vnet.outputs.subnet_id ไม่มีค่าจริงให้อ่าน mock_outputs แก้ปัญหานี้ได้พอดี
dependency "vnet" { config_path = "../vnet"
mock_outputs = { subnet_id = "mock-subnet-id" }
mock_outputs_allowed_terraform_commands = ["plan"]}เมื่อ output จริงยังไม่พร้อมใช้งาน Terragrunt จะแทนที่ด้วยค่าใน mock_outputs แทนการ fail ไปเลย ทำให้ plan แสดงสิ่งที่ unit vm จะสร้างได้ แม้ว่า vnet จะยังไม่มีอยู่จริงก็ตาม mock_outputs_allowed_terraform_commands จำกัดว่า command ไหนบ้างที่อนุญาตให้ fallback ไปใช้ mock ได้ การระบุแค่ plan ในตัวอย่างนี้ หมายความว่า apply ยัง fail อย่างชัดเจนถ้า dependency จริงยังไม่ถูก apply นั่นคือ safety net ที่ต้องการพอดี การ preview plan กับ subnet ID ปลอมนั้นไม่มีปัญหา แต่การสร้าง virtual machine จริงกับค่าปลอมนั้นไม่ควรเกิดขึ้นเด็ดขาด
ลำดับการ apply ถูกคำนวณอัตโนมัติ
หัวข้อที่มีชื่อว่า “ลำดับการ apply ถูกคำนวณอัตโนมัติ”ไม่มีตรงไหนใน terragrunt.hcl ของ unit vm ที่บอกว่า “apply vnet ก่อน” Terragrunt คำนวณเรื่องนี้ให้เองทั้งหมด ทุก dependency block ที่เจอระหว่างสแกน tree ของ unit ต่าง ๆ จะกลายเป็น edge หนึ่งใน dependency graph และเมื่อรัน operation ข้าม unit หลายตัวพร้อมกัน Terragrunt จะใช้ graph นั้นเป็นเงื่อนไขลำดับ vnet รับประกันว่าจะ apply เสร็จก่อนที่ vm จะเริ่ม เพราะ vm ประกาศ dependency ชี้ไปหา ไม่ต้อง hard-code ลำดับนี้หรือจำเพื่อรัน command ตามลำดับที่ถูกต้องด้วยมือเลย บทถัดไปจะพูดถึง command ที่ใช้ trigger การรันข้ามหลาย unit จริง ๆ
flowchart LR vnetunit["vnet unit (own state)"] vnetout["outputs.subnet_id"] dep["vm/terragrunt.hcl dependency vnet"] vminputs["vm inputs.subnet_id"] vnetunit --> vnetout --> dep --> vminputs mock["mock_outputs (plan only, before vnet is applied)"] -.-> dep
พิสูจน์: two-unit tree จริง, run --all plan ก่อน vnet ถูก apply
หัวข้อที่มีชื่อว่า “พิสูจน์: two-unit tree จริง, run --all plan ก่อน vnet ถูก apply”Terragrunt 1.1.0 tree จริง ใช้ local backend (ไม่ต้องมี Azure เพื่อพิสูจน์ กลไก — vnet output string subnet_id แบบเดียวกับที่ azurerm_subnet.this.id จะทำ):
remote_state { backend = "local" generate = { path = "backend.tf" if_exists = "overwrite_terragrunt" } config = { path = "${get_terragrunt_dir()}/terraform.tfstate" }}include "root" { path = find_in_parent_folders("root.hcl")}
terraform { source = "."}terraform { required_providers { local = { source = "hashicorp/local", version = "~> 2.5" } }}
resource "local_file" "vnet" { filename = "${path.module}/subnet-id.txt" content = "subnet-0123456789abcdef0\n"}
output "subnet_id" { value = "subnet-0123456789abcdef0"}vm/terragrunt.hcl คือ dependency block เดียวกันเป๊ะกับที่โชว์ข้างบน (config_path = "../vnet", mock_outputs_allowed_terraform_commands = ["plan"]) module ของมัน:
terraform { required_providers { local = { source = "hashicorp/local", version = "~> 2.5" } }}
variable "subnet_id" { type = string }
resource "local_file" "vm" { filename = "${path.module}/vm-subnet-ref.txt" content = "launched into ${var.subnet_id}\n"}ก่อนที่ vnet จะเคย apply เลย รัน terragrunt run --all -- plan จากบนสุดของ tree:
$ terragrunt run --all --non-interactive -- planINFO The following units will be run, starting with dependencies and then their dependents:╰── vnet ╰── vmWARN [vm] Config .../vnet/terragrunt.hcl is a dependency of .../vm/terragrunt.hcl that has no outputs, but mock outputs provided and returning those in dependency output.STDOUT [vm] terraform: launched into mock-subnet-idordering log list vnet ก่อน vm มาจาก dependency "vnet" block ล้วน ๆ — ไม่มี unit ไหนบอกว่า “รันฉันทีหลัง” เลย และเนื้อหา plan launched into mock-subnet-id คือค่า mock_outputs literal พิสูจน์ว่า mock_outputs_allowed_terraform_commands = ["plan"] คือตัวที่ทำให้สำเร็จได้โดยไม่มี state จริงของ vnet เลย
ตอนนี้ apply vnet จริง (ยัง offline เต็มรูปแบบ — local_file ไม่มี cloud) แล้วรัน plan ของ vm ใหม่ โดยไม่แก้ config อะไรเลย:
$ terragrunt run --non-interactive -- apply -auto-approve # inside vnet/Outputs:subnet_id = "subnet-0123456789abcdef0"
$ terragrunt run --non-interactive -- plan # inside vm/, unchanged configSTDOUT terraform: launched into subnet-0123456789abcdef0vm/terragrunt.hcl เดิม dependency block เดิม — เนื้อหา plan สลับจากค่า mock ไปเป็นค่า terraform output จริงทันทีที่ vnet มี state จริง โดยฝั่ง vm ไม่ต้องแก้อะไรเลย
error จริงเมื่อ dependency ไม่มี output และ command ไม่อยู่ใน allow-list
หัวข้อที่มีชื่อว่า “error จริงเมื่อ dependency ไม่มี output และ command ไม่อยู่ใน allow-list”apply ถูกตั้งใจเอาออกจาก mock_outputs_allowed_terraform_commands = ["plan"] ข้างบน รัน terragrunt apply กับ vm ก่อนที่ vnet จะถูก apply — error จริงของ Terragrunt 1.1.0 ไม่ได้ paraphrase:
$ terragrunt apply --non-interactive # vm/, before vnet has been applied, apply not in the allow-listERROR resolving dependency "vnet" outputs: .../vnet/terragrunt.hcl is a dependency of .../vm/terragrunt.hcl but detected no outputs. Either the target module has not been applied yet, or the module has no outputs.
If this dependency is accessed before the outputs are ready (which can happen during the planning phase of an unapplied stack), consider using mock_outputs: ...นี่คือ guardrail ที่ mock_outputs_allowed_terraform_commands มีไว้เพื่อบังคับใช้ — พิสูจน์ตรงนี้เป็น error text จริงของ Terragrunt ไม่ใช่แค่พูดลอย ๆ: plan ไปต่อได้ด้วย placeholder แต่ apply ถูกปฏิเสธตรง ๆ แทนที่จะสร้าง Azure virtual machine จริงที่ต่อเข้ากับ subnet ID ปลอมเงียบ ๆ
🐛 หา bug ในโค้ดนี้
dependency "vnet" {config_path = "../vnet"
mock_outputs = { subnet_id = "mock-subnet-id"}
mock_outputs_allowed_terraform_commands = ["plan", "apply"]}
inputs = {subnet_id = dependency.vnet.outputs.subnet_id}เฉลย
บั๊กคือ "apply" ที่อยู่ใน mock_outputs_allowed_terraform_commands ควบคู่กับ "plan" พิสูจน์ข้างบนว่าการจำกัด list เหลือแค่ ["plan"] คือตัวที่ทำให้ apply ปฏิเสธไม่รันด้วย error จริง จนกว่า vnet จะถูก apply จริง ๆ — ถ้ามี "apply" รวมอยู่ terragrunt apply บน vm จะสำเร็จเงียบ ๆ โดยใช้ subnet_id = "mock-subnet-id" สร้าง Azure virtual machine จริงที่ต่อเข้ากับค่า placeholder แทนที่จะเป็น subnet จริง ทางแก้คือ config เดียวกับที่โชว์ไว้ข้างบนเป๊ะ: เอา apply ออกจาก allow-list
อ่านโค้ดที่ AI สร้าง
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้าง”ตอนรีวิว dependency block ที่ AI สร้าง ให้เช็คสิ่งเหล่านี้:
mock_outputs_allowed_terraform_commandsที่มี"apply"รวมอยู่ด้วย — พิสูจน์ข้างบนว่า guardrail เฉพาะนี้มีไว้เพื่อหยุดapplyไม่ให้รันกับข้อมูลปลอมmock_outputsที่มีอยู่แต่ไม่มีmock_outputs_allowed_terraform_commandsเลย — mock ไม่ถูกใช้เลย (allow-list default เป็นค่าว่าง ไม่ใช่ “ใช้เสมอ”) ดังนั้นplanครั้งแรกบน environment ใหม่ล้วนจะ fail ด้วย error “detected no outputs” เป๊ะ ๆ แบบที่พิสูจน์ข้างบนdependencies { paths = [...] }block ที่โค้ดข้างในพยายามอ่านdependencies.vnet.something— attribute นั้นไม่มีอยู่บนdependenciesblock เลย มีแค่dependency(เอกพจน์) เท่านั้นที่ expose.outputs
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”dependency block คือตัวแทนของ cross-module reference ที่ Terraform ให้ฟรีภายใน state เดียว — พิสูจน์ข้างบนว่ามันทำงานด้วยการ shell out ไปเรียก terraform output -json กับอีก unit จริง ๆ แล้วเอาผลลัพธ์มาแทนที่ก่อนที่ plan/apply ของ unit นี้จะเริ่มด้วยซ้ำ mock_outputs มีอยู่เพราะ call นั้นอาจว่างเปล่า และ Terragrunt ต้องการคำตอบที่ชัดเจน scope ตาม command ว่า “ทำอะไรต่อ”
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ทางเลือก | ได้อะไร | เสียอะไร |
|---|---|---|
dependency block แทนการใช้ Terraform state เดียวร่วมกัน | state แยกอิสระต่อ unit run --all ยังได้ลำดับที่ถูกต้อง พิสูจน์ข้างบน | มี terraform output shell-out เพิ่มต่อ dependency ต่อ run |
mock_outputs scope ด้วย mock_outputs_allowed_terraform_commands | plan/CI บน environment ที่ apply ไปแค่บางส่วนไม่ fail แข็ง พิสูจน์ข้างบน | พิสูจน์ข้างบน: ตั้ง allow-list ผิด (รวม apply เข้าไป) แลกจาก fail ที่ดังและถูกต้อง เป็น fail เงียบ ๆ ที่ผิด — ตรงนี้คือ VM จริงที่ต่อเข้ากับ subnet ปลอม |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “dependency block link state ของสอง unit เข้าด้วยกัน” พิสูจน์ข้างบน: Terraform ไม่เห็น
vnetเลยตอน planvmTerragrunt resolvedependency.vnet.outputs.subnet_idเป็น string ธรรมดาก่อน ผ่านterraform outputcall แยกต่างหาก - “mock_outputs ถูกใช้อัตโนมัติทุกครั้งที่ state จริงของ dependency หายไป” พิสูจน์ข้างบนด้วย error จริงของ apply: mock ถูกใช้แค่กับ command ที่ list ไว้ใน
mock_outputs_allowed_terraform_commandsตรง ๆ เท่านั้น