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

Dependencies Between Units

dependency block ทำให้ unit หนึ่งอ่าน output ของอีก unit ได้ แม้ทั้งสอง unit จะมี Terraform state แยกกันคนละก้อนเลย และ mock_outputs ทำให้ unit ที่พึ่งพานั้น plan ได้ก่อนที่จะ apply unit ที่พึ่งพาจริง

ใน module Modules คุณเคยเห็น output ของ Terraform module หนึ่งไหลตรงเข้าไปเป็น input ของอีก module หนึ่ง network_id ที่มาจาก vpc module ถูกส่งเป็น argument เข้า compute module ทั้งสองต่อกันอยู่ใน main.tf เดียวกัน apply พร้อมกันเป็น terraform apply ครั้งเดียวกับ state ไฟล์เดียว composition แบบนี้ทำงานได้เพราะทั้งสอง module อยู่ใน Terraform run เดียวกัน Terraform เห็น dependency graph ทั้งหมดและ resolve ได้ในรอบเดียว

Terragrunt unit ตั้งใจทำลายสมมติฐานนั้น unit vpc กับ unit compute มี terragrunt.hcl ของตัวเอง มี state ไฟล์ของตัวเอง และมี terraform apply อิสระของตัวเอง ไม่มีอะไรใน Terraform module block ธรรมดาที่เอื้อมข้าม boundary นี้ได้ เพราะในมุมมองของ Terraform run ของแต่ละ unit เอง resource ของอีก unit ไม่มีอยู่จริงเลย การเชื่อม network ของ unit vpc เข้ากับ unit compute จึงต้องใช้กลไกที่สร้างมาเพื่อช่องว่างนี้โดยเฉพาะ นั่นคือ dependency block

dependency block เรียกชื่อ unit อื่นด้วย directory ของ unit นั้น แล้ว expose output ของ unit นั้นเป็นค่าที่ unit นี้ใช้ตรง ๆ ใน inputs ของตัวเองได้เลย

compute/terragrunt.hcl
include "root" {
path = find_in_parent_folders("root.hcl")
}
dependency "vpc" {
config_path = "../vpc"
mock_outputs = {
network_id = "mock-network-id"
}
mock_outputs_allowed_terraform_commands = ["plan"]
}
terraform {
source = "git::[email protected]:acme/infrastructure-modules.git//compute?ref=v1.2.0"
}
inputs = {
network_id = dependency.vpc.outputs.network_id
}

config_path = "../vpc" ชี้ไปที่ directory ของ unit vpc เบื้องหลัง Terragrunt รันสิ่งที่เทียบเท่ากับ terraform output กับ state ของ unit นั้น แล้ว expose ผลลัพธ์ออกมาเป็น dependency.vpc.outputs ทำให้ dependency.vpc.outputs.network_id เอื้อมเข้าไปหยิบค่า output network_id จาก state ที่ apply จริงของ unit vpc ชื่อ output เดียวกับที่ Terraform module ของ unit vpc เองประกาศไว้ด้วย output block ค่านั้นไหลตรงเข้า inputs ของ unit นี้ เหมือนค่า input อื่น ๆ ทั่วไป

การอ่าน output จริงทำงานได้ก็ต่อเมื่อ unit vpc ถูก apply ไปแล้วจริง ๆ และมี state จริงให้อ่าน ก่อนหน้านั้น plan บน compute จะ fail ทันที เพราะ dependency.vpc.outputs.network_id ไม่มีอะไรให้ resolve mock_outputs แก้ปัญหานี้ด้วยการให้ค่า placeholder ที่ Terragrunt ใช้แทนทุกครั้งที่ dependency ยังไม่ถูก apply และ mock_outputs_allowed_terraform_commands จำกัดว่า command ไหนบ้างที่ใช้ placeholder นี้ได้ ในตัวอย่างข้างบน mock-network-id แทนค่าจริงเฉพาะตอน plan เท่านั้น ส่วน apply จริงยังต้องการ output จริงของ unit vpc อยู่ดี ทำให้ค่า placeholder ไม่มีทางหลุดไปเขียนลง infrastructure จริงโดยไม่ตั้งใจ

เพราะ dependency block เรียกชื่อ unit อื่นตรง ๆ Terragrunt จึงเห็น graph ทั้งหมดว่า unit ไหนพึ่งพา unit ไหน ทั่วทั้ง directory tree เวลาคุณรัน operation กับหลาย unit พร้อมกัน ที่เป็นหัวข้อของบทถัดไป Terragrunt ใช้ graph นั้นคำนวณลำดับที่ถูกต้องเอง unit vpc ถูก apply ก่อน unit compute ที่อ่าน output โดยไม่มีใครต้อง hard-code ลำดับนั้นหรือคอยจำว่าต้องรัน unit ไหนก่อนหลังเอง

flowchart LR
  vpc["vpc unit (own state)"] -->|terraform output| dep["dependency vpc: outputs.network_id"]
  dep --> inputsBlock["compute unit inputs: network_id"]
  mock["mock_outputs: network_id = mock-network-id"] -.->|used only for plan, before vpc is applied| dep
output ของ unit vpc ไหลเข้า dependency block และ inputs ของ unit compute

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

root.hcl
remote_state {
backend = "local"
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
config = {
path = "${get_terragrunt_dir()}/terraform.tfstate"
}
}
vpc/terragrunt.hcl
include "root" {
path = find_in_parent_folders("root.hcl")
}
terraform {
source = "."
}
vpc/main.tf
terraform {
required_providers {
local = { source = "hashicorp/local", version = "~> 2.5" }
}
}
resource "local_file" "vpc" {
filename = "${path.module}/network-id.txt"
content = "network-0123456789abcdef0\n"
}
output "network_id" {
value = "network-0123456789abcdef0"
}

compute/terragrunt.hcl คือ dependency block เดียวกันเป๊ะกับที่โชว์ข้างบน module ของมัน:

compute/main.tf
terraform {
required_providers {
local = { source = "hashicorp/local", version = "~> 2.5" }
}
}
variable "network_id" { type = string }
resource "local_file" "compute" {
filename = "${path.module}/compute-network-ref.txt"
content = "launched into ${var.network_id}\n"
}

ก่อนที่ vpc จะเคย 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:
╰── vpc
╰── compute
WARN [compute] Config .../vpc/terragrunt.hcl is a dependency of .../compute/terragrunt.hcl
that has no outputs, but mock outputs provided and returning those in
dependency output.
STDOUT [compute] terraform: launched into mock-network-id

ordering log list vpc ก่อน compute มาจาก dependency "vpc" block ล้วน ๆ และเนื้อหา plan launched into mock-network-id คือค่า mock_outputs literal ตอนนี้ apply vpc จริง (ยัง offline เต็มรูปแบบ) แล้วรัน plan ของ compute ใหม่ โดยไม่แก้ config อะไรเลย:

$ terragrunt run --non-interactive -- apply -auto-approve # inside vpc/
Outputs:
network_id = "network-0123456789abcdef0"
$ terragrunt run --non-interactive -- plan # inside compute/, unchanged config
STDOUT terraform: launched into network-0123456789abcdef0

compute/terragrunt.hcl เดิม dependency block เดิม — เนื้อหา plan สลับจากค่า mock ไปเป็นค่า terraform output จริงทันทีที่ vpc มี state จริง

error จริงเมื่อ dependency ไม่มี output และ command ไม่อยู่ใน allow-list

หัวข้อที่มีชื่อว่า “error จริงเมื่อ dependency ไม่มี output และ command ไม่อยู่ใน allow-list”

apply ถูกตั้งใจเอาออกจาก mock_outputs_allowed_terraform_commands = ["plan"] ข้างบน รัน terragrunt apply กับ compute ก่อนที่ vpc จะถูก apply — error จริงของ Terragrunt 1.1.0:

$ terragrunt apply --non-interactive # compute/, before vpc has been applied, apply not in the allow-list
ERROR resolving dependency "vpc" outputs: .../vpc/terragrunt.hcl is a dependency of
.../compute/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:
...

🐛 หา bug ในโค้ดนี้

compute/terragrunt.hcl
dependency "vpc" {
config_path = "../vpc"
mock_outputs = {
network_id = "mock-network-id"
}
mock_outputs_allowed_terraform_commands = ["plan", "apply"]
}
inputs = {
network_id = dependency.vpc.outputs.network_id
}
เฉลย

บั๊กคือ "apply" ที่อยู่ใน mock_outputs_allowed_terraform_commands ควบคู่กับ "plan" พิสูจน์ข้างบนว่าการจำกัด list เหลือแค่ ["plan"] คือตัวที่ทำให้ apply ปฏิเสธไม่รันด้วย error จริง จนกว่า vpc จะถูก apply จริง ๆ — ถ้ามี "apply" รวมอยู่ terragrunt apply บน compute จะสำเร็จเงียบ ๆ โดยใช้ network_id = "mock-network-id" ต่อ infrastructure จริงของ GCP เข้ากับค่า placeholder แทนที่จะเป็น network จริง

  • mock_outputs_allowed_terraform_commands ที่มี "apply" รวมอยู่ด้วย — พิสูจน์ข้างบนว่า guardrail เฉพาะนี้มีไว้เพื่อหยุด apply ไม่ให้รันกับข้อมูลปลอม
  • mock_outputs ที่มีอยู่แต่ไม่มี mock_outputs_allowed_terraform_commands เลย — mock ไม่ถูกใช้เลย plan ครั้งแรกบน environment ใหม่ล้วนจะ fail ด้วย error “detected no outputs” เป๊ะ ๆ แบบที่พิสูจน์ข้างบน
  • dependencies { paths = [...] } block ที่โค้ดข้างในพยายามอ่าน dependencies.vpc.something — attribute นั้นไม่มีอยู่บน dependencies block เลย มีแค่ dependency (เอกพจน์) เท่านั้นที่ expose .outputs

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

ทางเลือกได้อะไรเสียอะไร
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 ผิด แลกจาก fail ที่ดังและถูกต้อง เป็น fail เงียบ ๆ ที่ผิด
  • “dependency block link state ของสอง unit เข้าด้วยกัน” พิสูจน์ข้างบน: Terraform ไม่เห็น vpc เลยตอน plan compute Terragrunt resolve ค่าก่อน ผ่าน terraform output call แยกต่างหาก
  • “mock_outputs ถูกใช้อัตโนมัติทุกครั้งที่ state จริงของ dependency หายไป” พิสูจน์ข้างบนด้วย error จริงของ apply: mock ถูกใช้แค่กับ command ที่ list ไว้ใน mock_outputs_allowed_terraform_commands ตรง ๆ เท่านั้น
ทำไม module composition แบบธรรมดาของ Terraform ถึงไม่พอสำหรับเชื่อม unit vpc เข้ากับ unit compute ใน Terragrunt
mock_outputs ใน dependency block มีไว้ทำอะไร
Terragrunt กำหนดลำดับ apply ที่ถูกต้องของหลาย unit ที่พึ่งพากันยังไง
ใน inputs block ของ unit compute dependency.vpc.outputs.network_id หมายถึงอะไร
mock_outputs_allowed_terraform_commands ถูกตั้งเป็น ["plan", "apply"] แทนที่จะเป็น ["plan"] เฉย ๆ มีผลจริงอะไรบ้าง พิสูจน์ข้างบน
state จริงของ dependency มี output network_id จริงอยู่บน disk แล้ว และ compute ถูก plan ไปครั้งหนึ่งแล้วกับค่า mock การรัน terragrunt run --all -- plan ครั้งถัดไปจะโชว์อะไร พิสูจน์ข้างบน