Why Terragrunt
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”Terragrunt คือ wrapper บาง ๆ ที่ห่อรอบ terraform (หรือ tofu) binary จริง คอย generate และจัดการ boilerplate ของ backend, provider, และ input ที่ซ้ำกันข้าม environment ทำให้ได้ทั้ง configuration ที่ DRY และ isolation ที่แข็งแรงพร้อมกัน แทนที่จะต้องแลกอย่างใดอย่างหนึ่งไป
ช่องว่างที่ทั้งสองวิธีของ Terraform เองปิดไม่ได้
หัวข้อที่มีชื่อว่า “ช่องว่างที่ทั้งสองวิธีของ Terraform เองปิดไม่ได้”สองบทที่แล้วพาไปดูสองคำตอบที่ Terraform เองมีให้กับปัญหา multi-environment และแต่ละคำตอบปิดช่องว่างได้แค่ครึ่งเดียว
- workspace เก่งเรื่อง DRY reuse มาก configuration เดียว, backend เดียว, state file หลายชุด แต่ isolation อ่อนแอ ทุก workspace แชร์ backend เดียวกัน, provider configuration เดียวกัน, และโค้ด
.tfเดียวกัน มีแค่ string ที่ CLI เลือกไว้เป็นตัวแยกเท่านั้น - directory-per-environment เก่งเรื่อง isolation มาก directory จริง, backend จริง, อาจมี AWS account แยกกันจริงต่อ environment แต่พา duplication กลับมา backend block, provider block, และ argument
sourceของ module จบลงด้วยการ copy-paste แทบทุกตัวอักษรในทุก environment directory
Terraform เปล่า ๆ ไม่มี primitive ระดับ first-class ที่ให้ทั้ง isolation ที่แข็งแรงและ configuration ที่ไม่ duplicate เลยพร้อมกัน เลือก pattern ของ Terraform เองแบบไหนก็ต้องยอมรับจุดอ่อนของอีก pattern หนึ่งเสมอ
Terragrunt คืออะไรจริง ๆ
หัวข้อที่มีชื่อว่า “Terragrunt คืออะไรจริง ๆ”Terragrunt คือ wrapper และ orchestrator บาง ๆ ที่วางอยู่บน setup Terraform (หรือ OpenTofu) ที่มีอยู่แล้ว ไม่ได้เพิ่ม resource syntax ใหม่ และไม่ได้แทนที่อะไรข้างใน modules/app เลย โค้ด module จริงของคุณไม่เปลี่ยนแปลงเลย ยังเป็น Terraform ธรรมดา เหมือนที่เห็นมาในทุกบทก่อนหน้านี้ สิ่งที่ Terragrunt เพิ่มเข้ามาคือ layer ที่อยู่ข้างบนนั้น รับผิดชอบ boilerplate ที่เพิ่งเห็นสะสมอยู่ใน directory-per-environment pattern
- generate backend configuration ของแต่ละ environment จาก definition ศูนย์กลางเดียว แทนที่จะเอา
backend "s3"block ไปใส่มือในทุก environment directory - generate provider configuration ของแต่ละ environment ด้วยวิธีเดียวกัน
- ใส่ input variable ของแต่ละ environment ไว้จุดเดียวต่อ environment โดยไม่ต้องทำ boilerplate ของ
moduleblock รอบ ๆ ซ้ำ - เข้าใจ dependency ordering เมื่อ infrastructure ถูกแยกเป็นหลาย unit ที่ apply แยกกันอิสระ เช่น unit
vpcที่ unitec2ต้องใช้ output
Terragrunt unit เล็ก ๆ สำหรับ environment dev จากบทที่แล้วหน้าตาเป็นแบบนี้ สังเกตว่า field ของ backend เป็นตัวเดียวกับที่เขียนมือไว้ก่อนหน้านี้เป๊ะ เพียงแต่ตอนนี้ generate มาจาก configuration แทนที่จะ copy-paste
remote_state { backend = "s3" generate = { path = "backend.tf" if_exists = "overwrite" } config = { bucket = "acme-terraform-state-dev" key = "app/terraform.tfstate" region = "us-east-1" use_lockfile = true }}
terraform { source = "../../../modules/app"}
inputs = { environment = "dev" instance_type = "t3.micro" instance_count = 1}สิ่งที่ Terragrunt ไม่ใช่
หัวข้อที่มีชื่อว่า “สิ่งที่ Terragrunt ไม่ใช่”ตรงนี้ควรพูดให้ชัด เพราะเป็นความเข้าใจผิดที่พบบ่อยที่สุด Terragrunt ไม่ใช่ fork ของ Terraform และไม่ได้ compile configuration ของคุณไปเป็นภาษา infrastructure-as-code แบบอื่น ทุกคำสั่งของ Terragrunt สุดท้ายแล้ว shell out ไปเรียกคำสั่ง terraform หรือ tofu จริง ๆ ข้างใต้เสมอ รันกับ backend และ provider configuration ตัวเดียวกับที่ Terragrunt เพิ่ง assemble ไว้ สั่ง terragrunt plan แล้วข้างใต้คือ terraform plan ธรรมดาที่รันอยู่ หน้าที่ของ Terragrunt จบแค่การ generate input แล้ว orchestrate ว่าคำสั่งจริงนั้นควรรันตอนไหน
ปัญหา dependency-ordering ที่ Terragrunt แก้ได้ด้วย
หัวข้อที่มีชื่อว่า “ปัญหา dependency-ordering ที่ Terragrunt แก้ได้ด้วย”Terraform configuration เดียวเข้าใจลำดับ dependency ระหว่าง resource ภายใน state file เดียวอยู่แล้ว ถ้า security group อ้างถึง ID ของ VPC ตัวหนึ่ง Terraform คิดเองได้ว่าต้องสร้าง VPC ก่อน อัตโนมัติ แต่ graph นั้นหยุดอยู่ที่ขอบของ state file เดียว ทันทีที่ infrastructure ถูกแยกเป็นหลาย unit ที่ apply แยกกันอิสระ เช่น unit vpc ที่มี state ของตัวเอง กับ unit ec2 แยกที่มี state ของตัวเองซึ่งต้องใช้ subnet ID ของ VPC ไม่มีอะไรใน terraform apply ของแต่ละอันที่รู้เลยว่าอีก unit ต้องรันก่อน เพราะแต่ละอันคือ process คนละตัวที่ทำงานบน state file คนละไฟล์กันเลย
Terragrunt ปิดช่องว่างนั้นด้วย dependency block ที่อ่าน output ของ unit อื่น และ run queue ที่ apply unit ตามลำดับที่ถูกต้องให้อัตโนมัติ
# Terragrunt determines vpc must run before ec2, then applies both in orderterragrunt run --all applyflowchart LR ws["Workspaces: DRY, weak isolation"] --> gap["The gap: no primitive gives both at once"] dirEnv["Directory-per-environment: isolated, duplicated boilerplate"] --> gap gap --> tg["Terragrunt: generates config, orders units"] tg --> tf["Real terraform / tofu binary"]
พิสูจน์: terraform_remote_state เทียบ dependency ทั้งคู่รันจริง
หัวข้อที่มีชื่อว่า “พิสูจน์: terraform_remote_state เทียบ dependency ทั้งคู่รันจริง”Terraform เปล่า ๆ มีวิธีอ่าน output ของ state อื่นอยู่แล้ว ไม่ต้องมี Terragrunt เลย: terraform_remote_state พิสูจน์จริง two-unit vpc apply ก่อนด้วย local backend:
resource "local_file" "vpc" { filename = "${path.module}/vpc-id.txt" content = "vpc-0123456789abcdef0\n"}output "vpc_id" { value = "vpc-0123456789abcdef0"}data "terraform_remote_state" "vpc" { backend = "local"
config = { path = "../vpc/terraform.tfstate" }}
resource "local_file" "ec2" { filename = "${path.module}/ec2-vpc-ref.txt" content = "launched into ${data.terraform_remote_state.vpc.outputs.vpc_id}\n"}apply จริงใน ec2/ หลังจาก vpc/ apply ไปแล้ว:
$ terraform apply -auto-approveApply complete! Resources: 1 added, 0 changed, 0 destroyed.$ cat ec2-vpc-ref.txtlaunched into vpc-0123456789abcdef0นี่ทำงานได้จริงโดยไม่มี Terragrunt เกี่ยวข้องเลย Terraform เปล่า ๆ, data block, อ่านข้าม state จริง ความต่างจริง ๆ จาก dependency block ของ Terragrunt (บทที่แล้ว) โผล่มาทันทีที่ upstream state ยังไม่มีอยู่ ec2/main.tf เดิม ลบ vpc/terraform.tfstate ทิ้งก่อน:
$ terraform planError: Unable to find remote stateNo stored state was found for the given workspace in the given backend.เทียบกับพิสูจน์จริงของบทที่แล้วเรื่อง dependency + mock_outputs: สถานการณ์ “state ยังไม่มีอยู่” เดียวกันเป๊ะ แต่ที่นั่น plan สำเร็จได้ ใช้ค่า placeholder ส่วน apply ปฏิเสธแยกต่างหากอย่างชัดเจน terraform_remote_state ไม่มีอะไรเทียบเท่าเลย — มัน hard-fail ทันที ทุกครั้ง ทั้ง plan และ apply ไม่มี mock fallback ไม่มี allow-list ต่อ command ช่องว่างนั้น — ไม่ใช่ “อ่าน state ของ unit อื่น” ซึ่ง Terraform เปล่า ๆ ทำได้อยู่แล้ว — คือสิ่งที่ dependency เพิ่มเข้ามาจริง ๆ
รักษาความตรงไปตรงมา: Terraform เปล่า ๆ ทำอะไรได้อยู่แล้วบ้าง
หัวข้อที่มีชื่อว่า “รักษาความตรงไปตรงมา: Terraform เปล่า ๆ ทำอะไรได้อยู่แล้วบ้าง”จะไม่ตรงไปตรงมาถ้าปล่อยให้เข้าใจว่า Terraform เปล่า ๆ ไม่มี คำตอบให้กับปัญหา configuration duplication เลย — -backend-config กับ -var-file ลด duplication ได้จริง พิสูจน์จริง main.tf ไฟล์เดียวใช้ซ้ำได้สอง environment โดยไม่มี directory ซ้ำเลย:
# env/main.tf — the only .tf file, used for every environmentterraform { backend "local" {}}variable "environment" { type = string }variable "instance_count" { type = number }resource "local_file" "app" { filename = "${path.module}/app-${var.environment}.txt" content = "environment=${var.environment} count=${var.instance_count}\n"}path = "dev.tfstate"environment = "dev"instance_count = 1init กับ apply จริง parameterize ทั้ง backend และ input ทั้งหมดจาก command line ไม่ต้องมี directory environments/dev/ เลย:
$ terraform init -backend-config=../dev.backend.tfvars$ terraform apply -var-file=../dev.tfvars -auto-approveApply complete! Resources: 1 added, 0 changed, 0 destroyed.$ cat app-dev.txtenvironment=dev count=1คู่ staging.backend.tfvars/staging.tfvars หรือสคริปต์ wrapper เล็ก ๆ ที่ loop ผ่านชื่อ environment ก็เข้าใกล้สิ่งที่ directory-per-environment ให้ได้เกือบหมด — .tf ไฟล์เดียว backend และ input ต่อ environment ส่งเข้ามาจากภายนอก สิ่งที่วิธีนี้ ไม่ได้ ให้จริง ๆ คือ: การอ่าน output ข้าม unit แบบ dependency พร้อม mock_outputs (พิสูจน์ข้างบน terraform_remote_state คือสิ่งที่ใกล้เคียงที่สุดของ Terraform เปล่า ๆ และมันไม่มี mock fallback เลย), การจัดลำดับหลาย unit อัตโนมัติ (run --all — สคริปต์ที่เขียนมือต้อง encode ลำดับนั้นเอง หรือ apply unit ทีละตัวตามลำดับตายตัวโดยไม่มี dependency-graph awareness เลย), และจุดเดียวสำหรับเปลี่ยน รูปร่าง ของ logic การ generate backend/provider เอง ไม่ใช่แค่ค่าที่ป้อนเข้าไป คุณค่าจริงของ Terragrunt แคบกว่าและตรงไปตรงมากว่า “Terraform เปล่า ๆ ทำอะไรพวกนี้ไม่ได้เลย” — มันคือ dependency graph, generation logic, และ orchestration ข้ามหลาย unit โดยเฉพาะ ไม่ใช่ parameterization เอง
อ่านโค้ดที่ AI สร้าง
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้าง”ตอนรีวิว justification แบบ “ทำไมเราต้องใช้ Terragrunt” (จาก AI หรือเพื่อนร่วมทีม) ให้เช็คสิ่งเหล่านี้:
- คำกล่าวอ้างว่า Terraform เปล่า ๆ “ทำไม่ได้” เรื่องปรับ configuration ต่อ environment เลย — พิสูจน์ข้างบนด้วย
-backend-config/-var-filerun จริง นี่คือการพูดเกินจริง และมองข้ามสิ่งที่ plain-Terraform setup ที่จัดระเบียบดีทำได้อยู่แล้ว terraform_remote_stateที่ใช้ข้าม unit โดยไม่จัดการกรณี “state ยังไม่มีอยู่” เลย — พิสูจน์ข้างบน มัน hard-fail ด้วยUnable to find remote stateไม่มีmock_outputs-เทียบเท่าเลย โค้ดที่สมมติว่ามัน degrade อย่างนุ่มนวลแบบdependencyจะพังทันทีที่ environment ใหม่ถูกตั้งขึ้นครั้งแรก- แผน migration ที่มองว่า Terragrunt แทนที่โค้ด module ของ Terraform — พิสูจน์ข้างบน
terraform { source = ... }block ของenvironments/dev/app/terragrunt.hclยังชี้ไปที่modules/appตัวเดียวกันเป๊ะจากบทที่แล้ว ไม่มีอะไรเกี่ยวกับตัว module เองเปลี่ยนเลย
🐛 หา bug ในโค้ดนี้
data "terraform_remote_state" "vpc" {backend = "local"config = { path = "../vpc/terraform.tfstate"}}
resource "aws_instance" "app" {subnet_id = data.terraform_remote_state.vpc.outputs.subnet_id}เฉลย
นี่ถูก apply เป็นส่วนหนึ่งของ terraform apply ครั้งแรกสุดของ environment ใหม่เอี่ยม ก่อนที่ vpc จะเคย apply เลย พิสูจน์ข้างบน terraform_remote_state ไม่มีกลไก mock-output เลย — ต่างจาก Terragrunt dependency block ไม่มีทางให้ plan นี้สำเร็จด้วย placeholder พร้อมส่งสัญญาณว่า apply ควรยังปฏิเสธอยู่ ผลลัพธ์จริงที่เลี่ยงไม่ได้ตรงนี้คือ Error: Unable to find remote state และทางแก้เดียวภายใน Terraform เปล่า ๆ คือเรียง apply ด้วยมือ (apply vpc ก่อนเสมอ ด้วยมือหรือด้วยสคริปต์ที่ encode ลำดับนั้นไว้) — คือปัญหาเรื่องลำดับเป๊ะ ๆ ที่ dependency กับ run --all แก้ให้อัตโนมัติ พิสูจน์แล้วใน module terragrunt-fundamentals
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”คุณค่าของ Terragrunt เข้าใจได้ดีที่สุดว่า แคบและเจาะจง ไม่ใช่ “ทำทุกอย่างที่ Terraform เปล่า ๆ ทำไม่ได้” พิสูจน์ข้างบน: terraform_remote_state ของ Terraform เปล่า ๆ อ่าน state ข้าม unit ได้ปกติ และ -backend-config/-var-file parameterize ต่อ environment ได้ปกติ สิ่งที่ Terraform เปล่า ๆ ไม่มี primitive เลยคือ graceful degradation ตอน dependency ยังไม่พร้อม (mock_outputs บทที่แล้ว) และการจัดลำดับอัตโนมัติข้ามหลาย unit ที่ apply แยกกันอิสระ (run --all) Terragrunt คือ wrapper ที่เพิ่มสองอย่างนี้เข้ามาเป๊ะ ๆ บวกกับ layer generation ที่เก็บ logic ของ backend/provider เอง (ไม่ใช่แค่ค่าของมัน) ไว้ที่เดียว
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ทางเลือก | ได้อะไร | เสียอะไร |
|---|---|---|
terraform_remote_state (Terraform เปล่า ๆ) | ไม่ต้องมี tooling เพิ่มเลย ใช้ได้วันนี้ พิสูจน์ข้างบนด้วยการอ่านข้าม state จริง | ไม่มี mock fallback เลย พิสูจน์ข้างบน — plan แรกของ environment ใหม่ hard-fail จนกว่า dependency จะถูก apply ตามลำดับที่ถูกต้องด้วยมือ |
-backend-config/-var-file (Terraform เปล่า ๆ) | ลด duplication ได้จริง พิสูจน์ข้างบนด้วย main.tf เดียวรับใช้สอง environment | ไม่มี dependency graph ไม่มีการจัดลำดับอัตโนมัติ — สคริปต์ที่ wrap มันยังต้อง encode ลำดับ apply unit เองอยู่ดี |
Terragrunt (dependency + run --all + generate) | mock-output graceful degradation และการจัดลำดับอัตโนมัติ ทั้งคู่พิสูจน์แล้วในบทก่อนหน้า | เครื่องมือเพิ่มอีกตัวใน stack มี CLI surface และ version ของตัวเองที่ต้อง track ควบคู่กับ Terraform เอง |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “Terragrunt เป็นวิธีเดียวที่เลี่ยงการ copy-paste backend/provider block ได้” พิสูจน์ข้างบน:
-backend-config/-var-fileparameterizemain.tfเดียวต่อ environment ได้จริงด้วย Terraform เปล่า ๆ ไม่ต้องมี Terragrunt เลย - “terraform_remote_state กับ dependency ทำงานแบบเดียวกันตอนเป้าหมายยังไม่ถูก apply” พิสูจน์ข้างบนด้วยผลลัพธ์จริงที่ตรงข้ามกันสองแบบ:
terraform_remote_statehard-fail ด้วยUnable to find remote statedependency(บทที่แล้ว) สำเร็จได้ตอนplanผ่านmock_outputsและปฏิเสธแค่ตอนapply - “Terragrunt แทนที่หรือ fork โค้ด module ของ Terraform” พิสูจน์ข้างบน:
terraform { source = ... }block ยังชี้ไปที่modules/appตัวเดียวกันเป๊ะที่ใช้ตลอด module นี้ — Terragrunt generate wrapper รอบ module ไม่เคย generate ตัว module เอง