Dependencies Between Units
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”dependency block ทำให้ unit หนึ่งอ่าน output ของอีก unit ข้าม state boundary ที่แยกทั้งสองออกจากกันได้ และ Terragrunt ก็ใช้ block เดียวกันนี้คำนวณลำดับที่ unit ต่าง ๆ ต้องถูก apply
ทำไม module composition ถึงไม่พอแล้ว
หัวข้อที่มีชื่อว่า “ทำไม module composition ถึงไม่พอแล้ว”ใน module Modules คุณเห็น output-to-input composition แล้ว output ของ module หนึ่งป้อนตรงเข้า input argument ของอีก module ภายใน main.tf เดียว evaluate เป็น Terraform plan เดียวกับ state ไฟล์เดียว วิธีนั้นทำงานได้เพราะทั้งสอง module อยู่ใน apply เดียวกัน
Terragrunt unit ตั้งใจทำลาย assumption นั้นทิ้ง unit vpc กับ unit ec2 ต่างมี terragrunt.hcl ของตัวเองและ state ไฟล์แยกของตัวเอง apply กันอย่างอิสระ module.vpc.outputs.vpc_id ที่อ้างอิงตรง ๆ แบบตอนอยู่ใน Terraform config เดียว จากมุมมองของ unit ec2 reference นั้นไม่มีอยู่จริงเลย เพราะไม่มี config ไฟล์ที่ share กันให้ reference นั้นอยู่ข้างใน ต้องมีอะไรสักอย่างอ่านข้าม state boundary นั้นตรง ๆ และสิ่งนั้นคือ dependency block
dependency: อ่านข้าม state boundary
หัวข้อที่มีชื่อว่า “dependency: อ่านข้าม state boundary”include "root" { path = find_in_parent_folders("root.hcl")}
terraform {}
dependency "vpc" { config_path = "../vpc"
mock_outputs = { vpc_id = "mock-vpc-id" }
mock_outputs_allowed_terraform_commands = ["plan"]}
inputs = { vpc_id = dependency.vpc.outputs.vpc_id}config_path ชี้ไปที่ directory ของอีก unit หนึ่ง ไม่ใช่ Terraform module แต่เป็น Terragrunt unit ก่อนรัน Terragrunt จะรัน terraform output กับ state จริงของ unit นั้น แล้ว expose ทุก output ที่เจอเป็น dependency.vpc.outputs.NAME ให้ใช้ได้ทุกที่ใน config ของ unit นี้ รวมถึงข้างใน inputs ด้วย
mock_outputs: ทำให้ plan สำเร็จได้ก่อน dependency จะมีจริง
หัวข้อที่มีชื่อว่า “mock_outputs: ทำให้ plan สำเร็จได้ก่อน dependency จะมีจริง”dependency.vpc.outputs.vpc_id จะมีค่าจริงก็ต่อเมื่อ unit vpc ถูก apply ไปแล้วอย่างน้อยหนึ่งครั้ง มีสองสถานการณ์ที่ทำลาย assumption นี้ คือตอนตั้ง environment ใหม่เอี่ยมครั้งแรก ที่ยังไม่มีอะไรถูก apply เลย และตอน CI validate pull request ที่แก้แค่ไฟล์ของ unit ec2 ซึ่งไม่มีใครอยากให้ plan fail แค่เพราะ unit อื่นที่ไม่เกี่ยวข้องยังไม่ถูก apply ใน environment นั้น
mock_outputs มาปิด gap นี้พอดี โดยให้ค่า placeholder ที่ Terragrunt สลับใช้แทนทุกครั้งที่ output จริงยังไม่มี และ mock_outputs_allowed_terraform_commands จำกัดว่า command ไหนถึง fallback ไปใช้ค่านั้นได้ ["plan"] ข้างบนแปลว่า plan บน ec2 สำเร็จได้ด้วย vpc_id = "mock-vpc-id" แม้ยังไม่มี state จริงของ vpc เลย ส่วน apply ยังคงปฏิเสธไม่ยอมรันกับค่าปลอม และบังคับให้ dependency จริงถูก apply ก่อนเสมอ
Terragrunt คำนวณลำดับ unit ให้เองอัตโนมัติยังไง
หัวข้อที่มีชื่อว่า “Terragrunt คำนวณลำดับ unit ให้เองอัตโนมัติยังไง”Terraform เองก็คำนวณลำดับการรันภายใน state เดียวอยู่แล้ว คือ dependency graph ระหว่าง resource แต่ละตัว dependency block ส่งข้อมูลแบบเดียวกันนี้ให้ Terragrunt ในระดับที่สูงขึ้นมาอีกชั้น คือระหว่าง unit ทั้งก้อน เพราะ ec2 ประกาศ dependency ไปที่ vpc Terragrunt ก็รู้ว่าต้อง apply vpc ก่อน ขยายแบบนี้ไปทั่ว tree ที่มีหลายสิบ unit จริง ๆ Terragrunt จะสร้างกราฟเต็มรูปแบบจากทุก dependency block ที่เจอ แล้วเดินตามลำดับที่ถูกต้องเวลามี command ไหนก็ตามที่แตะมากกว่าหนึ่ง unit พร้อมกัน ซึ่งจะพูดถึงเต็ม ๆ ในบทถัดไป โดยไม่ต้องมีใครจำหรือ maintain ลำดับนั้นด้วยมือเอง
flowchart LR vpcstate["vpc unit state (applied separately)"] --> vpcout["vpc outputs: vpc_id"] vpcout --> dep["ec2 terragrunt.hcl: dependency vpc block"] dep --> ecin["ec2 inputs: vpc_id = dependency.vpc.outputs.vpc_id"] mock["mock_outputs: vpc_id = mock-vpc-id"] -.->|lets plan succeed before vpc is applied| dep
พิสูจน์: two-unit tree จริง, run --all plan ก่อน vpc ถูก apply
หัวข้อที่มีชื่อว่า “พิสูจน์: two-unit tree จริง, run --all plan ก่อน vpc ถูก apply”นี่คือ Terragrunt 1.1.0 tree จริง ใช้ local backend (ไม่ต้องมี AWS เพื่อพิสูจน์ กลไก — vpc output string vpc_id แบบเดียวกับที่ aws_vpc.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" "vpc" { filename = "${path.module}/vpc-id.txt" content = "vpc-0123456789abcdef0\n"}
output "vpc_id" { value = "vpc-0123456789abcdef0"}ec2/terragrunt.hcl คือ dependency block เดียวกันเป๊ะกับที่โชว์ข้างบน (config_path = "../vpc", mock_outputs_allowed_terraform_commands = ["plan"]) module ของมัน:
terraform { required_providers { local = { source = "hashicorp/local", version = "~> 2.5" } }}
variable "vpc_id" { type = string }
resource "local_file" "ec2" { filename = "${path.module}/ec2-vpc-ref.txt" content = "launched into ${var.vpc_id}\n"}ก่อนที่ vpc จะเคย 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:╰── vpc ╰── ec2WARN [ec2] Config .../vpc/terragrunt.hcl is a dependency of .../ec2/terragrunt.hcl that has no outputs, but mock outputs provided and returning those in dependency output.STDOUT [ec2] terraform: # local_file.ec2 will be createdSTDOUT [ec2] terraform: + content = <<-EOT launched into mock-vpc-id EOT
❯❯ Run Summary 2 units 2s Succeeded 2พิสูจน์สองเรื่องพร้อมกัน: ordering log (“starting with dependencies and then their dependents”) list vpc ก่อน ec2 มาจาก dependency "vpc" block ล้วน ๆ — ไม่มีอะไรใน config ของ unit ไหนบอกว่า “รันฉันทีหลัง” เลย และเนื้อหา plan เอง launched into mock-vpc-id คือค่า mock_outputs literal พิสูจน์ว่า mock_outputs_allowed_terraform_commands = ["plan"] คือตัวที่ทำให้สำเร็จได้โดยไม่มี state จริงของ vpc อยู่บน disk เลย
ตอนนี้ apply vpc จริง (ยัง offline เต็มรูปแบบ — local_file ไม่มี cloud) แล้วรัน plan ของ ec2 ใหม่ โดยไม่แก้ config อะไรเลย:
$ terragrunt run --non-interactive -- apply -auto-approve # inside vpc/Outputs:vpc_id = "vpc-0123456789abcdef0"
$ terragrunt run --non-interactive -- plan # inside ec2/, unchanged configSTDOUT terraform: + content = <<-EOT launched into vpc-0123456789abcdef0 EOTec2/terragrunt.hcl เดิม dependency block เดิม — เนื้อหา plan สลับจากค่า mock ไปเป็นค่า terraform output จริงทันทีที่ vpc มี state จริง โดยฝั่ง ec2 ไม่ต้องแก้อะไรเลย นี่คือกลไกทั้งหมดที่ dependency.vpc.outputs.vpc_id ทำงานอยู่บน: Terragrunt shell out ไปที่ terraform output -json กับ state ของ vpc แล้ว fallback ไปใช้ mock_outputs ก็ต่อเมื่อผลลัพธ์นั้นว่างเปล่า (และ command ปัจจุบันอยู่ใน mock_outputs_allowed_terraform_commands)
error จริงเมื่อ dependency ไม่มี output และ command ไม่อยู่ใน allow-list
หัวข้อที่มีชื่อว่า “error จริงเมื่อ dependency ไม่มี output และ command ไม่อยู่ใน allow-list”apply ถูกตั้งใจเอาออกจาก mock_outputs_allowed_terraform_commands = ["plan"] ข้างบน รัน terragrunt apply กับ ec2 ก่อนที่ vpc จะถูก apply — error จริงของ Terragrunt 1.1.0 ไม่ได้ paraphrase:
$ terragrunt apply --non-interactive # ec2/, before vpc has been applied, apply not in the allow-listERROR resolving dependency "vpc" outputs: .../vpc/terragrunt.hcl is a dependency of .../ec2/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: ... If you do not require outputs from your dependency, consider using the dependencies block instead: https://docs.terragrunt.com/reference/config-blocks-and-attributes/#dependenciesนี่คือ guardrail ที่ mock_outputs_allowed_terraform_commands มีไว้เพื่อบังคับใช้: plan ไปต่อได้ด้วย placeholder แต่ apply ถูกปฏิเสธตรง ๆ แทนที่จะ apply ec2 เงียบ ๆ ไปกับ vpc_id ปลอม — พฤติกรรมเดียวกับที่อธิบายไว้ก่อนหน้านี้ในบท ตอนนี้เห็นเป็น error text จริงของ Terragrunt แทนที่จะเป็นแค่คำกล่าวอ้าง
skip_outputs: บังคับใช้ mock แม้ dependency จริงจะถูก apply แล้ว
หัวข้อที่มีชื่อว่า “skip_outputs: บังคับใช้ mock แม้ dependency จริงจะถูก apply แล้ว”skip_outputs = true บน dependency block แปลว่า “ไม่ต้องเรียก terraform output เลย ใช้ mock_outputs เสมอถ้าตั้งไว้” พิสูจน์ — unit ec2 เดิม vpc มี state จริงที่ apply ไปแล้วจากข้างบน เพิ่ม skip_outputs = true เข้า dependency "vpc" block ไม่แก้อะไรอื่น:
$ terragrunt run --non-interactive -- plan # skip_outputs = true, vpc already applied for realSTDOUT terraform: + content = <<-EOT launched into mock-vpc-id EOTแม้ output จริงของ vpc จะอยู่บน disk ตรงนั้นเลย (vpc_id = "vpc-0123456789abcdef0" พิสูจน์ข้างบน) skip_outputs ก็ข้ามการอ่านไปเลยทั้งหมด mock ชนะ นี่คือพฤติกรรมที่เอกสารระบุไว้สำหรับสถานการณ์เดียวเป๊ะ ๆ คือ CI ที่ตั้ง remote_state.disable_init ไว้ ที่ไม่อยากให้ Terragrunt พยายามอ่าน backend ไหนของ dependency เลย ทั้งจริงหรือ fallback เป็น mock ก็ตาม — อยากได้ mock แบบไม่มีเงื่อนไข ทุกครั้ง
dependencies (พหูพจน์) เทียบ dependency (เอกพจน์): แค่ลำดับ เทียบกับ มี output
หัวข้อที่มีชื่อว่า “dependencies (พหูพจน์) เทียบ dependency (เอกพจน์): แค่ลำดับ เทียบกับ มี output”dependencies block คือกลไกเก่ากว่า คนละตัว ที่มีผลแค่กับลำดับของ run --all เท่านั้น — ไม่ expose output เลย:
dependencies { paths = ["../vpc", "../rds"]}ไม่มี dependencies.vpc.outputs อยู่ที่ไหนเลย — block นี้คือ list ของ path เปล่า ๆ ที่หน้าที่เดียวคือบอก run --all ว่า “apply พวกนี้ก่อน” ไม่มี output-passing, mock_outputs, หรือ per-command allow-list แบบ dependency เลย ใช้ dependencies แค่ตอนที่ unit ต้องการลำดับแต่ไม่มีค่า config จริงที่ต้องอ่านจากอีก unit เลย ทันทีที่ต้องการค่าจริงแม้แค่ค่าเดียวจากอีก unit นั่นต้องเป็น dependency block ที่มีชื่อแทน — dependencies ไม่มี dependency.NAME.outputs.OUTPUT ให้อ่านเลย
อ่านโค้ดที่ AI สร้าง
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้าง”ตอนรีวิว dependency block ที่ AI สร้าง ให้เช็คสิ่งเหล่านี้:
mock_outputs_allowed_terraform_commandsที่มี"apply"รวมอยู่ด้วย — พิสูจน์ข้างบนว่า guardrail เฉพาะนี้มีไว้เพื่อหยุดapplyไม่ให้รันกับข้อมูลปลอม การเพิ่ม"apply"เข้า allow-list ทำลาย guardrail นั้นทั้งหมดแบบเงียบ ๆmock_outputsที่มีอยู่แต่ไม่มีmock_outputs_allowed_terraform_commandsเลย — mock ไม่ถูกใช้เลย (allow-list default เป็นค่าว่าง ไม่ใช่ “ใช้เสมอ”) ดังนั้นplanครั้งแรกบน environment ใหม่ล้วนจะ fail ด้วย error “detected no outputs” เป๊ะ ๆ แบบที่พิสูจน์ข้างบน ทั้งที่mock_outputsblock ที่ไม่ถูกใช้ดูเหมือนควรจะป้องกันสิ่งนั้นได้dependencies { paths = [...] }block ที่โค้ดข้างในพยายามอ่านdependencies.vpc.something— พิสูจน์ข้างบนว่า attribute นั้นไม่มีอยู่บนdependenciesblock เลย มีแค่dependency(เอกพจน์) เท่านั้นที่ expose.outputsskip_outputs = trueคู่กับไม่มีmock_outputsเลย — พิสูจน์จาก quote เอกสารข้างบนoutputsจะกลายเป็น empty map ในกรณีนั้น แล้วโค้ดที่ reference key เฉพาะข้างในจะ fail ตอน evaluate ไม่ใช่ด้วย message ชัด ๆ ว่า “คุณลืม mock_outputs”
🐛 หา bug ในโค้ดนี้
dependency "vpc" {config_path = "../vpc"
mock_outputs = { vpc_id = "mock-vpc-id"}
mock_outputs_allowed_terraform_commands = ["plan", "apply"]}
inputs = {vpc_id = dependency.vpc.outputs.vpc_id}เฉลย
บั๊กคือ "apply" ที่อยู่ใน mock_outputs_allowed_terraform_commands ควบคู่กับ "plan" พิสูจน์ข้างบนว่าการจำกัด list เหลือแค่ ["plan"] คือตัวที่ทำให้ apply ปฏิเสธไม่รันด้วย error จริง จนกว่า vpc จะถูก apply จริง ๆ — ถ้ามี "apply" รวมอยู่ terragrunt apply บน ec2 จะสำเร็จเงียบ ๆ โดยใช้ vpc_id = "mock-vpc-id" สร้าง infrastructure จริงที่ต่อเข้ากับค่า placeholder แทนที่จะเป็น VPC จริง ทางแก้คือ config เดียวกับที่โชว์ไว้ก่อนหน้าในบทนี้เป๊ะ: เอา apply ออกจาก allow-list เพื่อให้ไปเจอ error “detected no outputs” แล้วบังคับให้ vpc ถูก apply ก่อน
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”dependency block คือตัวแทนของ cross-module reference ที่ Terraform ให้ฟรีภายใน state เดียว — พิสูจน์ข้างบนว่ามันทำงานด้วยการ shell out ไปเรียก terraform output -json กับอีก unit จริง ๆ แล้วเอาผลลัพธ์มาแทนที่ก่อนที่ plan/apply ของ unit นี้จะเริ่มด้วยซ้ำ ไม่มีอะไร share หรือ link กันในระดับ Terraform เลย พอถึงตอนที่ Terraform เองรัน dependency.vpc.outputs.vpc_id ถูก resolve เป็น string ธรรมดาและถูก bake เข้าไปใน inputs เรียบร้อยแล้ว mock_outputs มีอยู่เพราะ terraform output call นั้นอาจว่างเปล่า และ Terragrunt ต้องการคำตอบที่ชัดเจน scope ตาม command ว่า “ทำอะไรต่อ” แทนที่จะ fail ตลอดหรือเดาตลอด
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ทางเลือก | ได้อะไร | เสียอะไร |
|---|---|---|
dependency block แทนการใช้ Terraform state เดียวร่วมกัน | state แยกอิสระต่อ unit (บทก่อนหน้า) run --all ยังได้ลำดับที่ถูกต้อง พิสูจน์ข้างบน | มี terraform output shell-out เพิ่มต่อ dependency ต่อ run — ต้นทุนจริงที่วัดได้เมื่อมีหลายสิบ unit พูดถึงใน --parallelism บทถัดไป |
mock_outputs scope ด้วย mock_outputs_allowed_terraform_commands | plan/CI บน environment ที่ apply ไปแค่บางส่วนไม่ fail แข็ง — พิสูจน์ข้างบน | พิสูจน์ข้างบนเหมือนกัน: ตั้ง allow-list ผิด (รวม apply เข้าไป) แลกจาก fail ที่ดังและถูกต้อง เป็น fail เงียบ ๆ ที่ผิด |
skip_outputs | mock ที่ deterministic ไม่มีการอ่าน backend เลย มีประโยชน์ตอนตั้ง remote_state.disable_init ใน CI | พิสูจน์ข้างบน: มันข้าม state จริงแม้จะมีอยู่ mock เก่าอาจบัง drift จริงใน plan ที่ควรจับมันได้ |
dependencies (path เท่านั้น) แทน dependency | ง่ายกว่าเมื่อ unit ต้องการแค่ลำดับ ไม่ต้องการค่า | ไม่มี .outputs ให้อ่านเลย ทันทีที่ต้องการค่าจริงแม้แค่ค่าเดียว ต้องเปลี่ยนเป็น dependency block |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “dependency block link state ของสอง unit เข้าด้วยกัน Terraform เห็นทั้งคู่พร้อมกัน” พิสูจน์ข้างบน: Terraform ไม่เห็น
vpcเลยตอน planec2Terragrunt resolvedependency.vpc.outputs.vpc_idเป็น string ธรรมดา ก่อน เรียก Terraform ด้วยซ้ำ ผ่านterraform outputcall แยกกับ state ของvpcเอง - “mock_outputs ถูกใช้อัตโนมัติทุกครั้งที่ state จริงของ dependency หายไป” พิสูจน์ข้างบนด้วย error จริงของ apply แบบ “detected no outputs”: mock ถูกใช้แค่กับ command ที่ list ไว้ใน
mock_outputs_allowed_terraform_commandsตรง ๆ เท่านั้น list ว่างหรือไม่มี list เลยแปลว่า mock ไม่ถูกใช้เลย แล้ว command นั้น fail แทน - “skip_outputs แปลว่า ‘ข้าม mock ไป ใช้ของจริง’” ตรงข้ามเลย พิสูจน์ข้างบน:
skip_outputs = trueแปลว่าใช้mock_outputsเสมอ (ไม่เรียกterraform outputเลย) แม้ state จริงจะมีอยู่ก็ตาม - “dependencies กับ dependency คือ block เดียวกัน สะกดได้สองแบบ” พิสูจน์ข้างบน:
dependencies { paths = [...] }มีผลแค่กับลำดับrun --allและไม่ expose อะไรเลยdependency "name" { ... }(เอกพจน์ มี label) ต่างหากที่ expose.outputsทั้งสองใช้แทนกันไม่ได้