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

Worked review 2: multi-environment Terragrunt live tree

Prompt คือ: “ตั้ง Terragrunt live tree ที่มี dev กับ prod environment แต่ละอันมี network unit และ app unit ที่ depend บนมัน” สิ่งที่ตามมาคือ draft แรกแบบที่ AI assistant ส่งกลับมาให้กับ prompt แบบนี้พอดี — root.hcl ที่ใช้ร่วมกัน, terragrunt.hcl สี่ไฟล์ที่ดูสมเหตุสมผลรายตัว, terragrunt.stack.hcl, และ CI snippet ที่อ่านแล้วเหมือน Terragrunt tutorial ทุกอันที่ตีพิมพ์ก่อน CLI redesign 1.1 มี bug จริง 8 ข้อ; ดู AI bug catalog สำหรับ pattern ทั่วไปเบื้องหลังแต่ละข้อ เจ็ดในแปดข้อนี้เป็น mechanics ของ Terragrunt-core ที่ไม่ขึ้นกับ provider เหมือนกันเป๊ะกับ worked review 2 ของ review ฝั่ง AWS — พิสูจน์สดใหม่ที่นี่ ใน repo นี้ กับ azurerm — บวกอีกหนึ่ง bug ที่เฉพาะ backend ของ azurerm จริงๆ แทนคู่ dynamodb_table/use_lockfile ของ AWS ทุกข้อที่กล่าวถึงด้านล่างพิสูจน์ด้วย terragrunt hcl validate/render/run --all/find --dag จริง กับ Terragrunt 1.1.0 และ Terraform 1.16.4 โดยใช้ backend = "local" แทน backend azurerm จริง — ตามธรรมเนียมของคอร์สนี้ การพิสูจน์ offline ไม่แตะ Azure จริงเลย

# root.hcl, as generated -- realistic azurerm backend (see review walk for why the proof tree below uses backend = "local" instead)
remote_state {
backend = "azurerm"
generate = {
path = "backend.tf"
if_exists = "overwrite"
}
config = {
resource_group_name = "terraform-state-rg"
storage_account_name = "mycompanytfstate"
container_name = "tfstate"
key = "${path_relative_to_include()}/terraform.tfstate"
}
}
# live tree, as generated -- live/dev/network/terragrunt.hcl and live/prod/network/terragrunt.hcl (byte-identical)
include "root" {
path = find_in_parent_folders()
}
locals {
environment = "dev"
subscription_id = "00000000-0000-0000-0000-000000000000"
location = "East US"
}
terraform {
source = "${get_terragrunt_dir()}/../../../modules/network"
}
inputs = {
environment = local.environment
}
# live tree, as generated -- live/dev/app/terragrunt.hcl and live/prod/app/terragrunt.hcl (byte-identical)
include "root" {
path = find_in_parent_folders()
}
dependency "network" {
config_path = "../network"
}
terraform {
source = "${get_terragrunt_dir()}/../../../modules/app"
}
inputs = {
vnet_id = dependency.network.outputs.vnet_id
}
# ci/deploy.sh, as generated -- fragment, not a collected fence
#!/usr/bin/env bash
set -euo pipefail
cd live/dev
terragrunt run-all plan --terragrunt-non-interactive
terragrunt plan-all --terragrunt-non-interactive --queue-include-dir network

อาการ — จริง รันตรง (mechanics เดียวกับ review ฝั่ง AWS เพราะเป็น Terragrunt-core ไม่ใช่เฉพาะ provider):

$ terragrunt plan-all --non-interactive --no-color
ERROR unknown command: "plan-all". Terragrunt no longer forwards unknown commands by default. Use 'terragrunt run -- plan-all ...' or a supported shortcut. Learn more: https://docs.terragrunt.com/migrate/cli-redesign/#use-the-new-run-command
$ terragrunt run-all plan --non-interactive --no-color
ERROR unknown command: "run-all". Terragrunt no longer forwards unknown commands by default. Use 'terragrunt run -- run-all ...' or a supported shortcut. Learn more: https://docs.terragrunt.com/migrate/cli-redesign/#use-the-new-run-command

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

terragrunt run-all plan --terragrunt-non-interactive
terragrunt plan-all --terragrunt-non-interactive --queue-include-dir network
เฉลย

bug เดียวกับ AI bug catalog entry 16 — ทั้งสอง command เกลื่อนอยู่ใน tutorial ก่อน 1.1 และ training data ของ AI และทั้งคู่ถูกแทนที่ด้วย run --all ของ CLI redesign 1.1 วิธีแก้: terragrunt run --all --non-interactive -- plan

อาการ — จริง รันตรงกับ command run --all ที่แก้แล้ว:

$ terragrunt run --all --terragrunt-non-interactive --no-color -- plan
ERROR flag `-terragrunt-non-interactive` is not a Terragrunt flag. If this is an OpenTofu/Terraform flag, use `--` to forward it (e.g., `terragrunt run -- <command> -terragrunt-non-interactive`).

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

terragrunt run --all --terragrunt-non-interactive --no-color -- plan
เฉลย

ทุก flag ของ Terragrunt หลุด prefix terragrunt- ใน CLI redesign เดียวกับที่แทนที่ run-all flag ที่มี prefix --terragrunt-* ถูกปฏิเสธตรงๆ ไม่ใช่ถูกเมินแบบเงียบๆ — CI script เก่าพังตั้งแต่ run แรกเลย แทนที่จะทำงานต่างไปแบบเงียบๆ วิธีแก้: ตัด prefix terragrunt- ออกจากทุก flag

# excerpt, as generated -- entries/03-find-in-parent-no-arg/unit/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}

อาการ — terragrunt hcl validate จริง ทั้ง 4 unit ในต้นเดียวกัน พังแบบเดียวกันพร้อมกันหมด:

$ terragrunt hcl validate --working-dir .
Error: Error in function call
on live/dev/network/terragrunt.hcl line 2, in include "root":
2: path = find_in_parent_folders()
Call to function "find_in_parent_folders" failed: ParentFileNotFoundError:
Could not find a terragrunt.hcl in any of the parent folders of
live/dev/network/terragrunt.hcl. Cause: Traversed all the way to the root..
The include configuration in live/dev/app/terragrunt.hcl must specify a 'path' parameter
The include configuration in live/dev/network/terragrunt.hcl must specify a 'path' parameter
... (identical for live/prod/network and live/prod/app)
4 HCL validation error(s) found

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

include "root" {
path = find_in_parent_folders()
}
เฉลย

find_in_parent_folders() ที่เรียกโดย ไม่มี argument เลย default ไปหาไฟล์ที่ชื่อ terragrunt.hcl เป๊ะๆ — ธรรมเนียมก่อน 1.1 root config ของคอร์สนี้ เหมือนคำแนะนำ Terragrunt ปัจจุบัน ชื่อ root.hcl เจาะจงเพื่อเลี่ยงไม่ให้ไฟล์นี้ถูกเข้าใจผิดว่าเป็น unit config ของตัวเอง วิธีแก้: find_in_parent_folders("root.hcl") ระบุชื่อไฟล์จริง

# excerpt, as generated -- entries/04-no-mock-outputs/app/terragrunt.hcl
dependency "network" {
config_path = "../network"
}

อาการ — terragrunt run --all plan จริง กับทั้งต้น สดใหม่ network ไม่เคยถูก apply เลย:

$ terragrunt run --all --non-interactive --no-color -- plan
❯❯ Run Summary 4 units
Succeeded 2
Failed 2
ERROR ... resolving dependency "network" outputs: live/dev/network/terragrunt.hcl is
a dependency of live/dev/app/terragrunt.hcl but detected no outputs. Either the target
module has not been applied yet, or the module has no outputs.
... There is no variable named "dependency".

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

dependency "network" {
config_path = "../network"
}
เฉลย

ดู Dependencies Between Units สำหรับ mechanism เต็มรูปแบบ — นี่คือกลไก guardrail mock_outputs_allowed_terraform_commands ของบทนั้น แต่ปัญหาต้นตอเกิดก่อนหน้านั้นหนึ่งขั้น: ไม่มี mock_outputs ประกาศไว้เลย plan ของ network เองผ่าน; app พังเพราะมันต้องการ dependency.network.outputs.vnet_id และไม่มี state จริง — นี่คือ run --all plan ครั้งแรกของ environment สดใหม่พอดี วิธีแก้: เพิ่ม mock_outputs = { vnet_id = "mock-vnet-id" } และ mock_outputs_allowed_terraform_commands = ["plan"]

อาการ — จริง บนไฟล์ที่ AI สร้างจริง:

$ diff live/dev/network/terragrunt.hcl live/prod/network/terragrunt.hcl
(no output -- the files are byte-identical)

ทั้งสองไฟล์ประกาศ environment = "dev", subscription_id = "00000000-0000-0000-0000-000000000000" — รวมถึงข้างใน live/prod/network/terragrunt.hcl ด้วย

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

live/prod/network/terragrunt.hcl
locals {
environment = "dev"
subscription_id = "00000000-0000-0000-0000-000000000000"
}
เฉลย

bug เดียวกับ AI bug catalog entry 17 — terragrunt hcl validate/hcl format --check ผ่านทั้งคู่ ไม่มีอะไรเป็น HCL ที่ invalid เลย และด้วย backend = "local" สำหรับการพิสูจน์ offline ของคอร์สนี้ ไม่มีอะไรพังตอน plan ด้วย เทียบกับ backend azurerm จริงและ subscription_id จริงที่เล็งไป subscription ของ dev จากทั้ง dev และ prod นี่คือการ deploy prod เข้า Azure subscription ของ dev แบบเงียบๆ วิธีแก้: ย้ายค่าที่ต้องต่างกันต่อ environment ไปไว้ env.hcl ต่อ environment แล้วให้ network/terragrunt.hcl แต่ละตัวกลายเป็นเหมือนกันกับ sibling ของมัน อ่าน include.env.locals.* แทน

include ตัวที่สองสำหรับค่าที่ share กัน ประกาศโดยไม่มี expose = true

หัวข้อที่มีชื่อว่า “include ตัวที่สองสำหรับค่าที่ share กัน ประกาศโดยไม่มี expose = true”
# excerpt, as generated -- entries/06-include-no-expose/unit/terragrunt.hcl
include "env" {
path = find_in_parent_folders("env.hcl")
merge_strategy = "no_merge"
}
inputs = {
subscription_id = include.env.locals.subscription_id
}

อาการ — terragrunt render จริง:

$ terragrunt render --format json
Error: Unsupported attribute
on unit/terragrunt.hcl line 15:
15: subscription_id = include.env.locals.subscription_id
This object does not have an attribute named "env".

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

include "env" {
path = find_in_parent_folders("env.hcl")
merge_strategy = "no_merge"
}
เฉลย

mechanism เดียวกับ Dry Configuration with Include — expose = true คือสิ่งที่ทำให้เนื้อหาที่ parse แล้วของ include อ่านได้ผ่าน include.NAME.* วิธีแก้: เพิ่ม expose = true เข้า include block ของ env

# entries/07-remote-state-cross-env/main.tf -- @expect-error No stored state was found for the given workspace in the given backend.
data "terraform_remote_state" "prod_network" {
backend = "local"
config = {
path = "../../../prod/network/terraform.tfstate"
}
}

อาการ — terraform plan จริง:

Error: Unable to find remote state
with data.terraform_remote_state.prod_network,
on main.tf line 1, in data "terraform_remote_state" "prod_network":
1: data "terraform_remote_state" "prod_network" {
No stored state was found for the given workspace in the given backend.

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

data "terraform_remote_state" "prod_network" {
backend = "local"
config = { path = "../../../prod/network/terraform.tfstate" }
}
เฉลย

mechanism เดียวกับ AI bug catalog entry 6 ใช้กับรูปทรงที่ Terragrunt live tree ล่อ AI assistant ให้ทำพอดี: เอื้อมไปใช้ raw Terraform terraform_remote_state data source แทนที่จะใช้ Terragrunt dependency block นอกจาก hard-fail ที่พิสูจน์ไว้ที่นี่ pattern นี้ยังข้าม mock_outputs ไปเลย ไม่มีความรู้เรื่องลำดับ run --all เลย วิธีแก้: dependency block cross-environment ที่ถูกต้อง หรือลบ reference นี้ทิ้งไปเลย

# root.hcl (excerpt)
remote_state {
backend = "azurerm"
config = {
resource_group_name = "terraform-state-rg"
storage_account_name = "mycompanytfstate"
container_name = "tfstate"
}
}

อาการ — จริง terragrunt run -- init -backend=false กับ root.hcl นี้เป๊ะ (ธรรมเนียมของคอร์สนี้ในการพิสูจน์ผลของ generate — ไม่ต้องมี Azure Storage account จริง แค่เนื้อหา backend.tf ที่ generate ออกมาจริงก็พอ):

$ cat backend.tf
# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa
terraform {
backend "azurerm" {
container_name = "tfstate"
key = "vnet/terraform.tfstate"
resource_group_name = "terraform-state-rg"
storage_account_name = "mycompanytfstate"
}
}

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

remote_state {
backend = "azurerm"
config = {
resource_group_name = "terraform-state-rg"
storage_account_name = "mycompanytfstate"
container_name = "tfstate"
}
}
เฉลย

ทุก argument ใน config ถูก emit เข้า backend.tf ที่ generate ออกมาตรงตัว — พิสูจน์แล้วด้านบน output จริง ไม่มี use_azuread_auth เลยสักที่ แปลว่า Terraform authenticate เข้า data plane ของ storage account ด้วย shared account key หรือ SAS token โดย default ซึ่งเป็น secret แบบอายุยืนที่ต้องแจกจ่ายไปทุกเครื่องที่รัน init ต่างจาก identity-based auth ที่บท Remote Backends and Locking ของคอร์สนี้กำหนดเป็น pattern ที่แนะนำ วิธีแก้: เพิ่ม use_azuread_auth = true (และสำหรับ CI, use_oidc = true) เข้า remote_state.config authenticate ด้วย Microsoft Entra ID แทน

root.hcl
remote_state {
backend = "local"
generate = { path = "backend.tf", if_exists = "overwrite_terragrunt" }
config = { path = "${get_terragrunt_dir()}/terraform.tfstate" }
}
modules/network/main.tf
variable "environment" {
type = string
}
resource "terraform_data" "vnet" {
input = "vnet-${var.environment}"
}
output "vnet_id" {
value = terraform_data.vnet.id
}
modules/app/main.tf
variable "vnet_id" {
type = string
}
resource "terraform_data" "app" {
input = var.vnet_id
}
live/dev/env.hcl
locals {
environment = "dev"
subscription_id = "00000000-0000-0000-0000-000000000000"
location = "East US"
}
live/prod/env.hcl
locals {
environment = "prod"
subscription_id = "11111111-1111-1111-1111-111111111111"
location = "East US"
}
live/dev/network/terragrunt.hcl
include "root" {
path = find_in_parent_folders("root.hcl")
}
include "env" {
path = find_in_parent_folders("env.hcl")
expose = true
merge_strategy = "no_merge"
}
terraform {
source = "${get_terragrunt_dir()}/../../../modules/network"
}
inputs = {
environment = include.env.locals.environment
}
live/prod/network/terragrunt.hcl
include "root" {
path = find_in_parent_folders("root.hcl")
}
include "env" {
path = find_in_parent_folders("env.hcl")
expose = true
merge_strategy = "no_merge"
}
terraform {
source = "${get_terragrunt_dir()}/../../../modules/network"
}
inputs = {
environment = include.env.locals.environment
}
live/dev/app/terragrunt.hcl
include "root" {
path = find_in_parent_folders("root.hcl")
}
dependency "network" {
config_path = "../network"
mock_outputs = { vnet_id = "mock-vnet-id" }
mock_outputs_allowed_terraform_commands = ["plan"]
}
terraform {
source = "${get_terragrunt_dir()}/../../../modules/app"
}
inputs = {
vnet_id = dependency.network.outputs.vnet_id
}
live/prod/app/terragrunt.hcl
include "root" {
path = find_in_parent_folders("root.hcl")
}
dependency "network" {
config_path = "../network"
mock_outputs = { vnet_id = "mock-vnet-id" }
mock_outputs_allowed_terraform_commands = ["plan"]
}
terraform {
source = "${get_terragrunt_dir()}/../../../modules/app"
}
inputs = {
vnet_id = dependency.network.outputs.vnet_id
}

พิสูจน์จริง ทั้งต้นพร้อมกัน:

$ terragrunt hcl validate --working-dir .
(no output -- 0 errors)
$ diff live/dev/network/terragrunt.hcl live/prod/network/terragrunt.hcl
(no output -- now correctly identical; env.hcl differs instead)
$ terragrunt run --all --non-interactive --no-color --working-dir . -- plan
❯❯ Run Summary 4 units
Succeeded 4
$ terragrunt find --dag --working-dir .
live/dev/network
live/prod/network
live/dev/app
live/prod/app
#Bugวิธีแก้
1run-all/plan-all ใน CI scriptrun --all — real “unknown command” rejection ทั้งคู่ ก่อนแก้
2--terragrunt-non-interactive และ flag prefix ก่อน 1.1 อื่นๆ--non-interactive เปล่าๆ ฯลฯ — real “not a Terragrunt flag” rejection ก่อนแก้
3find_in_parent_folders() ไม่มี argument, root file ชื่อ root.hclfind_in_parent_folders("root.hcl") — real “Could not find a terragrunt.hcl” ทั้ง 4 unit ก่อนแก้
4dependency "network" ไม่มี mock_outputs เลยmock_outputs + mock_outputs_allowed_terraform_commands = ["plan"] — real run --all plan พังบน environment สดใหม่ก่อนแก้
5dev กับ prod network unit มี locals เหมือนกันเป๊ะ (รวม subscription_id)ย้ายค่าที่ต้องต่างไปไว้ env.hcl ต่อ environment; ไฟล์ unit กลายเป็นเหมือนกันแทน — diff จริงพิสูจน์ทั้ง bug และวิธีแก้
6include "env" ตัวที่สองไม่มี expose = trueเพิ่ม expose = true — real “This object does not have an attribute named env” ก่อนแก้
7terraform_remote_state อ่าน state ของ environment อื่นตรงๆ จากข้างใน moduleแทนที่ด้วย dependency block ที่ถูกต้อง (หรือลบทิ้ง) — real “No stored state was found” ก่อนแก้
8root.hcl azurerm remote_state ไม่มี use_azuread_authเพิ่ม use_azuread_auth = true — พิสูจน์ด้วย backend.tf ที่ generate จริงไม่พูดถึงมันเลย default ไป shared-key auth

สี่ใน bug แปดข้อนี้ (1, 2, 3, 6) เป็น hard failure ที่ชัดทันทีระดับ CLI/HCL-evaluation เหมือนกับ review ฝั่ง AWS ข้อหนึ่ง (4) เป็น hard failure แต่เกิดเฉพาะ run --all plan ครั้งแรกกับ environment สดใหม่จริงๆ เท่านั้น ข้อหนึ่ง (7) เป็น hard failure เฉพาะ state ที่ไม่เคยถูก apply ที่ระดับ Terraform สองข้อที่เหลือ (5, 8) ไม่ error เลย — และ entry 8 คือ bug ข้อเดียวในบทนี้ทั้งหมดที่เฉพาะ azurerm จริงๆ ไม่ใช่ reuse จาก review ฝั่ง AWS: แทนที่จะเป็น argument dynamodb_table ที่ deprecated (AWS) ช่องว่างที่เงียบเทียบเท่าที่นี่คือ backend.tf ที่ generate จริงที่ไม่พูดถึง mechanism การ auth เลยที่บท state-management ของคอร์สนี้เองแนะนำ

  • “ถ้า terragrunt hcl validate ผ่าน แปลว่า multi-environment tree ปลอดภัยที่จะ deploy จริง” พิสูจน์แล้วด้านบน: hcl validate กับ network unit ที่เหมือนกันเป๊ะสองตัวคืนค่า error ศูนย์ข้อ — HCL validation ไม่มีแนวคิดเรื่อง “ค่านี้ควรต่างจากไฟล์ sibling นั้น” เลย
  • “dependency block ที่ไม่มี mock_outputs พังแค่ apply เท่านั้น ไม่เคยพัง plan” พิสูจน์แล้วด้านบน: บน environment สดใหม่สมบูรณ์ที่ dependency ไม่เคยถูก apply เลยแม้แต่ครั้งเดียว run --all plan เองก็พัง
  • “generate block ของ backend สำคัญแค่เรื่อง locking กับตำแหน่ง state ไม่เกี่ยวกับ auth” พิสูจน์แล้วด้านบน: backend.tf ที่ generate จริงสำหรับ root.hcl ของ tree นี้ไม่พูดถึง use_azuread_auth เลยสักที่ — mechanism generate เดียวกันเป๊ะที่ scope key ต่อ unit ได้ถูกต้อง ก็จะ emit backend ที่ authenticate ด้วย shared key อายุยืนได้เหมือนกัน ถ้า config map บอกแบบนั้น (หรือไม่บอกอะไรเลย)
flowchart TD
  CI["ci/deploy.sh"] -->|"1: run-all/plan-all removed"| CI
  CI -->|"2: --terragrunt-* flags rejected"| CI
  CI --> H["terragrunt hcl validate"]
  H -->|"3: find_in_parent_folders() no arg"| H
  H -->|"6: include without expose"| H
  H --> R["terragrunt run --all plan"]
  R -->|"4: dependency, no mock_outputs -- fresh env"| R
  R --> M["module-level terraform plan"]
  M -->|"7: terraform_remote_state cross-env"| M
  D["diff dev vs prod files"] -->|"5: byte-identical locals"| D
  L["root.hcl remote_state, generated backend.tf"] -->|"8: use_azuread_auth missing"| L
แต่ละ bug อยู่ตรงไหนในเส้นทางประเมินของ live tree นี้
find_in_parent_folders() ถูกเรียกโดยไม่มี argument ใน tree ที่ root config ชื่อ root.hcl terragrunt hcl validate จริงแสดงอะไร?
environment สดใหม่ที่ network ไม่เคยถูก apply เลย และ app มี dependency "network" block ที่ไม่มี mock_outputs เลย terragrunt run --all plan จริงแสดงอะไร?
backend.tf ที่ generate จริงสำหรับ root.hcl ของ tree นี้ไม่พูดถึง use_azuread_auth เลย นี่พิสูจน์อะไรเกี่ยวกับ authentication ที่เกิดขึ้นจริง และ generate mechanism ที่นี่มีอะไรเหมือนกับ entry dynamodb_table ของ review ฝั่ง AWS?
dev กับ prod network unit เหมือนกันเป๊ะ รวม subscription_id ด้วย terragrunt hcl validate แสดงอะไร และอะไรจับ bug นี้ได้จริง?
include "env" ตัวที่สองประกาศด้วย merge_strategy = "no_merge" แต่ไม่มี expose = true แล้ว inputs บรรทัดหนึ่งอ่าน include.env.locals.subscription_id terragrunt render จริงแสดงอะไร?