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

Dependencies Between Units

dependency block ทำให้ unit หนึ่งอ่าน output ของอีก unit ข้าม state boundary ที่แยกทั้งสองออกจากกันได้ โดย mock_outputs ทำหน้าที่แทนค่าจริงไปก่อน จนกว่าจะ apply unit อีกฝั่งจริง

โมดูล 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 ระบุชื่อ unit อื่นด้วย directory ของ unit นั้น แล้ว expose ทุก output ของ unit นั้นออกมา

vm/terragrunt.hcl
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

มีปัญหาชัดเจนอยู่ในโครงสร้างข้างต้น ครั้งแรกที่ใครก็ตามรัน 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 จริงกับค่าปลอมนั้นไม่ควรเกิดขึ้นเด็ดขาด

ไม่มีตรงไหนใน 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
output ของ vnet ไหลเข้า vm ผ่าน dependency block โดย mock_outputs เชื่อมช่องว่างก่อน apply vnet

Terragrunt 1.1.0 tree จริง ใช้ local backend (ไม่ต้องมี Azure เพื่อพิสูจน์ กลไก — vnet output string subnet_id แบบเดียวกับที่ azurerm_subnet.this.id จะทำ):

root.hcl
remote_state {
backend = "local"
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
config = {
path = "${get_terragrunt_dir()}/terraform.tfstate"
}
}
vnet/terragrunt.hcl
include "root" {
path = find_in_parent_folders("root.hcl")
}
terraform {
source = "."
}
vnet/main.tf
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 ของมัน:

vm/main.tf
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 -- plan
INFO The following units will be run, starting with dependencies and then their dependents:
╰── vnet
╰── vm
WARN [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-id

ordering 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 config
STDOUT terraform: launched into subnet-0123456789abcdef0

vm/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-list
ERROR 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 ในโค้ดนี้

vm/terragrunt.hcl
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

ตอนรีวิว 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 นั้นไม่มีอยู่บน dependencies block เลย มีแค่ dependency (เอกพจน์) เท่านั้นที่ expose .outputs

dependency block คือตัวแทนของ cross-module reference ที่ Terraform ให้ฟรีภายใน state เดียว — พิสูจน์ข้างบนว่ามันทำงานด้วยการ shell out ไปเรียก terraform output -json กับอีก unit จริง ๆ แล้วเอาผลลัพธ์มาแทนที่ก่อนที่ plan/apply ของ unit นี้จะเริ่มด้วยซ้ำ mock_outputs มีอยู่เพราะ call นั้นอาจว่างเปล่า และ Terragrunt ต้องการคำตอบที่ชัดเจน scope ตาม command ว่า “ทำอะไรต่อ”

ทางเลือกได้อะไรเสียอะไร
dependency block แทนการใช้ Terraform state เดียวร่วมกันstate แยกอิสระต่อ unit run --all ยังได้ลำดับที่ถูกต้อง พิสูจน์ข้างบนมี terraform output shell-out เพิ่มต่อ dependency ต่อ run
mock_outputs scope ด้วย mock_outputs_allowed_terraform_commandsplan/CI บน environment ที่ apply ไปแค่บางส่วนไม่ fail แข็ง พิสูจน์ข้างบนพิสูจน์ข้างบน: ตั้ง allow-list ผิด (รวม apply เข้าไป) แลกจาก fail ที่ดังและถูกต้อง เป็น fail เงียบ ๆ ที่ผิด — ตรงนี้คือ VM จริงที่ต่อเข้ากับ subnet ปลอม
  • “dependency block link state ของสอง unit เข้าด้วยกัน” พิสูจน์ข้างบน: Terraform ไม่เห็น vnet เลยตอน plan vm Terragrunt resolve dependency.vnet.outputs.subnet_id เป็น string ธรรมดาก่อน ผ่าน terraform output call แยกต่างหาก
  • “mock_outputs ถูกใช้อัตโนมัติทุกครั้งที่ state จริงของ dependency หายไป” พิสูจน์ข้างบนด้วย error จริงของ apply: mock ถูกใช้แค่กับ command ที่ list ไว้ใน mock_outputs_allowed_terraform_commands ตรง ๆ เท่านั้น
ทำไม module composition จาก main.tf เดียวถึงไม่พอ เมื่อ vnet กับ vm กลายเป็น Terragrunt unit แยกกัน
mock_outputs ให้ unit ทำอะไรได้
Terragrunt กำหนดลำดับ apply ที่ถูกต้องข้ามหลาย unit ที่พึ่งพากันได้อย่างไร
ใน dependency "vnet" { config_path = "../vnet" } นั้น config_path ชี้ไปที่อะไร
mock_outputs_allowed_terraform_commands ถูกตั้งเป็น ["plan", "apply"] แทนที่จะเป็น ["plan"] เฉย ๆ มีผลจริงอะไรบ้าง พิสูจน์ข้างบน
state จริงของ dependency มี output subnet_id จริงอยู่บน disk แล้ว และ vm ถูก plan ไปครั้งหนึ่งแล้วกับค่า mock การรัน terragrunt run --all -- plan ครั้งถัดไปจะโชว์อะไร พิสูจน์ข้างบน