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

Why Terragrunt

Terraform workspace แก้ปัญหา DRY reuse ได้ดีแต่ isolation ยังอ่อน directory-per-environment แก้ isolation ได้ดีแต่ดึง boilerplate ของ backend กับ provider ที่ซ้ำซ้อนกลับมา และ Terragrunt เกิดมาเพื่อปิดช่องว่างที่เหลือนั้นด้วยการห่อ binary terraform ตัวจริงแล้ว generate configuration ที่ซ้ำซ้อนให้แทนคุณ

สองบทที่แล้วแต่ละบทแก้ครึ่งหนึ่งของปัญหาจากบทแรกสุดของ module นี้

  • workspace เก็บโค้ด .tf ชุดเดียว backend เดียว และ provider configuration เดียว — DRY จริง ๆ — แต่ทุก workspace ใช้ backend เดียวกันและ GCP project เดียวกัน ทำให้ isolation ระหว่าง environment อ่อนพอที่การเลือก terraform workspace select ผิดจะทำให้ apply การเปลี่ยนแปลงขนาด prod เข้าไปในสิ่งที่ใครบางคนคิดว่าเป็น environment ที่ทิ้งได้
  • directory-per-environment ให้แต่ละ environment มี backend ของตัวเองและ GCP project ของตัวเอง — isolate จริง ๆ — แต่ block backend, block provider, และการต่อสาย source กับ version ของ module จบลงด้วยการถูก copy-paste แทบทุกตัวอักษรเข้าไปในทุก environment directory ซึ่งคือความเสี่ยงจากการทำซ้ำมือแบบเดียวกับที่บทแรกของ module นี้เปิดเรื่องไว้เป๊ะ

Terraform เปล่า ๆ เองไม่มี primitive ตัวเดียวที่ให้ทั้ง isolation ที่แข็งแรงและ configuration ที่ไม่ซ้ำซ้อนข้าม environment พร้อมกัน คุณเลือกได้แค่ฝั่งใดฝั่งหนึ่งของความตึงสองอันข้างบน แต่หนีทั้งคู่พร้อมกันด้วยโค้ด .tf อย่างเดียวไม่ได้

Terragrunt คือ thin wrapper และ orchestrator ที่นั่งอยู่บน binary terraform (หรือ tofu) ตัวจริง ไม่ได้เพิ่ม resource syntax ใหม่ และไม่ได้ compile ไปเป็นภาษา infrastructure-as-code แบบอื่น โค้ด .tf ของ module ที่คุณเขียนอยู่แล้ว รวมถึงทุกอย่างใน directory modules/app จากบทที่แล้ว ยังคงเหมือนเดิมทุกประการและยังเป็น Terraform ธรรมดา ทุกคำสั่งของ Terragrunt สุดท้ายแล้ว shell out ไปเรียกคำสั่ง terraform (หรือ tofu) ตัวจริงข้างใต้

Terminal window
# What you type
terragrunt apply
# What actually eventually runs, per unit, with generated config already in place
terraform apply

สิ่งที่ Terragrunt เพิ่มเข้ามาคือ layer ที่จัดการ boilerplate ที่ซ้ำซ้อนที่คุณเพิ่งเห็นกองสูงขึ้นรอบ ๆ การเรียก Terraform หนึ่งครั้งต่อ environment

  • generate backend configuration จากคำนิยามศูนย์กลางเดียว แทนที่จะ copy-paste block backend "gcs" { ... } เดียวกันเข้าไปในทุก environment directory
  • generate provider configuration แบบเดียวกัน จากที่เดียว แทนที่จะมี block provider "google" { ... } ที่แทบเหมือนกันต่อ environment
  • ส่ง input variable ของแต่ละ environment เข้าไปใน shared module ทำให้ dev, staging, prod ยังเรียก source modules/app เดียวกันด้วยค่าที่ต่างกัน โดยไม่ต้องต่อสาย module block แยกมือต่อ environment
  • เข้าใจลำดับ dependency เมื่อ infrastructure ถูกแบ่งเป็นหลาย unit ที่ apply แยกกันเป็นอิสระ เช่น unit network ที่ unit gke ต้องพึ่งพา ทำให้ Terragrunt คิดออกว่า network ต้อง apply ก่อน gke แทนที่คุณต้อง track ลำดับนั้นเองด้วยมือข้าม terraform apply ที่รันแยกกัน

จุดสุดท้ายนี้สำคัญเป็นพิเศษ เพราะ directory-per-environment แบ่ง infrastructure เป็น state file ที่แยกกันจริงต่อ environment (และตามที่ module ถัดไปจะพูดถึง มักแยกต่อ component ด้วย) Terraform configuration เดียวมี dependency graph ภายในของตัวเองระหว่าง resource ใน state file เดียว แต่ไม่มี concept ของการเรียงลำดับข้าม configuration ที่แยกกันทั้งหมดและการเรียก terraform apply แยกกันเลย การเรียงลำดับข้าม configuration แบบนี้แหละคือปัญหาที่ Terragrunt ถูกสร้างมาเพื่อแก้ ซ้อนอยู่บน graph ของ Terraform เอง ไม่ใช่มาแทนที่

คุ้มค่าที่จะพูดให้แม่นยำตรง boundary นี้ เพราะพูดเกินจริงเกี่ยวกับสิ่งที่ Terragrunt ทำได้ง่ายมาก

  • Terragrunt ไม่ใช่ fork ของ Terraform และ ไม่ใช่ ตัวแทนของ Terraform การ plan และ apply resource จริง ๆ ยังทำโดย terraform หรือ tofu ทั้งหมด
  • Terragrunt ไม่ใช่ ภาษา configuration ใหม่สำหรับอธิบาย GCP resource resource google_compute_instance ถูก define แบบเดียวกันเป๊ะไม่ว่าจะมี Terragrunt เกี่ยวข้องหรือไม่
  • Terragrunt ทำให้การเปลี่ยนแปลง Terraform ที่ไม่ปลอดภัยกลายเป็นปลอดภัยเองไม่ได้ Terragrunt เอา boilerplate ที่ copy-paste ออกไปและจัดลำดับให้ แต่ logic ของ infrastructure จริง ๆ และความถูกต้องยังเป็นความรับผิดชอบของโค้ด module .tf ของคุณทั้งหมด

module ถัดไปเริ่มต่อจากตรงนี้เป๊ะ ด้วย building block จริงของ Terragrunt คือ root.hcl, remote_state, generate, และ terragrunt.hcl ของแต่ละ unit ที่เปลี่ยนตำแหน่งนี้ให้กลายเป็น setup ที่ใช้งานได้จริง

flowchart TD
  workspaces["Terraform workspaces: DRY, weak isolation"] --> gap["The gap: isolation AND zero duplication together"]
  directoryPerEnv["Directory-per-environment: isolated, duplicated boilerplate"] --> gap
  gap --> terragrunt["Terragrunt: generates backend, provider, inputs, manages unit ordering"]
  terragrunt -->|shells out to| terraformBinary["Real terraform / tofu binary"]
Workspace (DRY, isolation อ่อน) และ directory-per-environment (isolate ดี แต่ duplicate) ต่างทิ้งช่องว่างไว้ Terragrunt ปิดช่องว่างนั้นด้วยการห่อ Terraform ตัวจริง

Terraform เปล่า ๆ มีวิธีอ่าน output ของ state อื่นอยู่แล้ว ไม่ต้องมี Terragrunt เลย: terraform_remote_state พิสูจน์จริง two-unit network apply ก่อนด้วย local backend:

network/main.tf
resource "local_file" "network" {
filename = "${path.module}/network-id.txt"
content = "network-0123456789abcdef0\n"
}
output "network_id" {
value = "network-0123456789abcdef0"
}
gke/main.tf
data "terraform_remote_state" "network" {
backend = "local"
config = {
path = "../network/terraform.tfstate"
}
}
resource "local_file" "gke" {
filename = "${path.module}/gke-network-ref.txt"
content = "launched into ${data.terraform_remote_state.network.outputs.network_id}\n"
}

apply จริงใน gke/ หลังจาก network/ apply ไปแล้ว:

$ terraform apply -auto-approve
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
$ cat gke-network-ref.txt
launched into network-0123456789abcdef0

นี่ทำงานได้จริงโดยไม่มี Terragrunt เกี่ยวข้องเลย ความต่างจริง ๆ จาก dependency block ของ Terragrunt (module terragrunt-fundamentals) โผล่มาทันทีที่ upstream state ยังไม่มีอยู่ — gke/main.tf เดิม ลบ network/terraform.tfstate ทิ้งก่อน:

$ terraform plan
Error: Unable to find remote state
No stored state was found for the given workspace in the given backend.

เทียบกับ dependency + mock_outputs: สถานการณ์ “state ยังไม่มีอยู่” เดียวกันเป๊ะ แต่ที่นั่น plan สำเร็จได้ผ่าน placeholder ส่วน apply ปฏิเสธแยกต่างหากอย่างชัดเจน terraform_remote_state ไม่มีอะไรเทียบเท่าเลย — มัน hard-fail ทันที ทุกครั้ง

รักษาความตรงไปตรงมา: Terraform เปล่า ๆ ทำอะไรได้อยู่แล้วบ้าง

หัวข้อที่มีชื่อว่า “รักษาความตรงไปตรงมา: Terraform เปล่า ๆ ทำอะไรได้อยู่แล้วบ้าง”

-backend-config กับ -var-file ลด duplication ได้จริง พิสูจน์จริง main.tf ไฟล์เดียวใช้ซ้ำได้สอง environment:

# env/main.tf — the only .tf file, used for every environment
terraform {
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"
}
$ terraform init -backend-config=../dev.backend.tfvars
$ terraform apply -var-file=../dev.tfvars -auto-approve
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
$ cat app-dev.txt
environment=dev count=1

สิ่งที่วิธีนี้ ไม่ได้ ให้จริง ๆ คือ: การอ่าน output ข้าม unit แบบ dependency พร้อม mock_outputs (terraform_remote_state คือสิ่งที่ใกล้เคียงที่สุดของ Terraform เปล่า ๆ และมันไม่มี mock fallback เลย พิสูจน์ข้างบน), การจัดลำดับหลาย unit อัตโนมัติ (run --all), และจุดเดียวสำหรับเปลี่ยน รูปร่าง ของ logic การ generate backend/provider เอง

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

# gke/main.tf — applied as part of a brand-new environment's very first apply, before network exists
data "terraform_remote_state" "network" {
backend = "local"
config = {
path = "../network/terraform.tfstate"
}
}
resource "google_container_cluster" "app" {
network = data.terraform_remote_state.network.outputs.network_self_link
}
เฉลย

พิสูจน์ข้างบน terraform_remote_state ไม่มีกลไก mock-output เลย — ต่างจาก Terragrunt dependency block ไม่มีทางให้ plan นี้สำเร็จด้วย placeholder พร้อมส่งสัญญาณว่า apply ควรยังปฏิเสธอยู่ ผลลัพธ์จริงที่เลี่ยงไม่ได้ตรงนี้คือ Error: Unable to find remote state และทางแก้เดียวภายใน Terraform เปล่า ๆ คือเรียง apply ด้วยมือ

ช่องว่างที่ทั้ง Terraform workspace และ directory-per-environment ปิดไม่สนิทด้วยตัวเองคืออะไร
คำอธิบาย Terragrunt ที่แม่นยำที่สุดคือข้อไหน
Terragrunt แก้ปัญหาแบบไหนเพิ่มเติมที่ dependency graph ภายในของ Terraform configuration เดียวไม่ครอบคลุม
ข้อไหนคือสิ่งที่ Terragrunt ไม่ทำอย่างชัดเจน
plain Terraform gke/main.tf ใช้ data "terraform_remote_state" "network" อ่าน output ของ network บน environment ใหม่เอี่ยมที่ network ไม่เคยถูก apply เลย เกิดอะไรขึ้น พิสูจน์ข้างบน
พูดว่า Terraform เปล่า ๆ "ทำไม่ได้" เรื่องปรับ configuration ต่อ environment โดยไม่มี Terragrunt ถูกต้องไหม