DRY with Terragrunt
สิ่งที่จะสร้าง
หัวข้อที่มีชื่อว่า “สิ่งที่จะสร้าง”slice จริงชิ้นแรกของ tree live/: AWS network module เล็ก ๆ แต่ของจริง ขับด้วย Terragrunt unit ที่ป้อน input เข้าไป โดยมี root.hcl ที่ใช้ร่วมกันคอย generate provider ครั้งเดียวให้ทุก unit จากนั้นเราจะได้เจอ terragrunt run --all — apply ทั้ง tree ของ cloud ตามลำดับ dependency ด้วยคำสั่งเดียว
นี่คือจุดที่ความซ้ำจาก Module 2 หายไป ปัญหา provider block สามชุดถูกแก้ด้วยการเขียน provider ครั้งเดียว แล้ว generate ออกไปทุกที่
Terraform เปล่าบังคับให้คุณเขียนซ้ำข้าม directory: provider block เดียวกัน, backend เดียวกัน, ในทุก folder ที่เก็บ state Terragrunt ตัดความซ้ำนั้นออก โดยให้แต่ละ unit include root.hcl ที่ใช้ร่วมกัน และให้ terraform { source } ชี้ไปที่ module ที่อื่นใน repo unit หดเหลือแค่สิ่งเดียวที่ unique จริง — inputs ของตัวเอง
terraform { source } คือหมัดเด็ด แทนที่จะ copy ไฟล์ของ module ลงใน live/aws/network unit อ้างอิง modules/aws/network แล้วส่ง input เข้าไป module เดียว ผู้เรียกหลายราย แต่ละรายมี state ของตัวเอง และเพราะ Terragrunt เข้าใจ link dependency ระหว่าง unit run --all จึง plan หรือ apply ทั้ง tree ตามลำดับที่ถูกต้องได้
Recent Terragrunt replaced the old
terragrunt run-all <command>withterragrunt run --all <command>. The old form still works but is deprecated — this course usesrun --all.
ข้อดีข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีข้อเสีย”Terragrunt include + generate เทียบกับ copy-paste provider และ backend block:
- Pros: เขียน provider และ backend ครั้งเดียวใน
root.hclทุก unit สืบทอดไป เปลี่ยน region หรือ state bucket ที่เดียวแล้วทุก unit ตามไป ไฟล์ของ unit ยังเล็กและ diff ง่าย - Cons: ชิ้นส่วนเคลื่อนไหวมากขึ้น —
provider.tfที่ generate ออกมาโผล่ในแต่ละ unit ตอน apply ซึ่งทำให้คนแปลกใจครั้งแรกที่เห็น คุณแลกความ “ไฟล์นี้มาจากไหน?” นิดหน่อยกับ duplication ที่น้อยลงมาก
terragrunt run --all เทียบกับ apply แต่ละ unit ด้วยมือ:
- Pros: คำสั่งเดียวสร้างทั้ง cloud ตามลำดับ dependency — network, แล้ว cluster, แล้ว data — resolve link
dependencyอัตโนมัติ เหมาะสำหรับการยืนหรือรื้อทั้ง platform - Cons: blast radius ใหญ่กว่า
run --all applyแตะทุกอย่างพร้อมกัน การเปลี่ยนที่แย่จึงลงกว้างกว่า ใช้อย่างตั้งใจ; สำหรับการปรับจุดเดียวcdเข้า unit เดียวแล้ว apply แค่ตัวนั้น
ติดตั้ง
หัวข้อที่มีชื่อว่า “ติดตั้ง”1. modules/aws/network — a small, real module
หัวข้อที่มีชื่อว่า “1. modules/aws/network — a small, real module”ไม่ใช่ stub — network module ของจริง (ถึงจะ minimal) ที่ Module 4 จะขยายเป็น interface สาม cloud เต็ม module นี้สร้าง VPC และสอง subnet แล้ว expose output ที่เป็น contract:
variable "name" { type = string }variable "cidr" { type = string default = "10.0.0.0/16"}
resource "aws_vpc" "this" { cidr_block = var.cidr tags = { Name = var.name }}
resource "aws_subnet" "private" { count = 2 vpc_id = aws_vpc.this.id cidr_block = cidrsubnet(var.cidr, 8, count.index) availability_zone = data.aws_availability_zones.available.names[count.index] tags = { Name = "${var.name}-private-${count.index}" }}
data "aws_availability_zones" "available" { state = "available"}
output "network_id" { value = aws_vpc.this.id }output "private_subnet_ids" { value = aws_subnet.private[*].id }VPC และ subnet ไม่มีค่าใช้จ่ายในการรัน จึงปล่อยไว้แบบ applied ได้อย่างปลอดภัย ระหว่างคุณเรียน
2. live/aws/root.hcl — generate the provider once
หัวข้อที่มีชื่อว่า “2. live/aws/root.hcl — generate the provider once”สำหรับบทนี้ เก็บ state เป็น local ไว้ (block remote_state เป็นงานของบทหน้า) แล้วโฟกัสที่ชัยชนะ DRY: provider ที่ generate ตัวเดียวสำหรับทุก unit
generate "provider" { path = "provider.tf" if_exists = "overwrite_terragrunt" contents = <<EOFprovider "aws" { region = "us-east-1"}EOF}3. live/aws/network/terragrunt.hcl — the first unit
หัวข้อที่มีชื่อว่า “3. live/aws/network/terragrunt.hcl — the first unit”unit คือสาม block: ดึง root เข้ามา, ชี้ไปที่ module, ส่ง input
include "root" { path = find_in_parent_folders("root.hcl")}
terraform { source = "../../../modules/aws/network"}
inputs = { name = "clouddeploy" cidr = "10.0.0.0/16"}นั่นคือ configuration ต่อ cloud ทั้งหมดของ network — ไม่มี provider, ไม่มี backend, ไม่มีโค้ด resource ทั้งหมดมาจาก root และ module
4. The dependency pattern (preview)
หัวข้อที่มีชื่อว่า “4. The dependency pattern (preview)”unit ต่อสายถึงกันผ่าน dependency อ่าน output ของ unit หนึ่งเป็น input ของอีก unit cluster unit (Module 5 สร้าง module) จะหน้าตาแบบนี้:
# live/aws/cluster/terragrunt.hcl — preview of the patterninclude "root" { path = find_in_parent_folders("root.hcl")}
terraform { source = "../../../modules/aws/cluster"}
dependency "network" { config_path = "../network"}
inputs = { name = "clouddeploy" network_id = dependency.network.outputs.network_id subnet_ids = dependency.network.outputs.private_subnet_ids node_count = 2 node_size = "small"}dependency "network" คือสิ่งที่บอก run --all ให้สร้าง network ก่อน cluster การต่อสายเดียวกันบนทุก cloud เพราะ interface เหมือนกัน
ตรวจสอบผล
หัวข้อที่มีชื่อว่า “ตรวจสอบผล”apply network unit เดี่ยว ๆ ก่อน:
cd live/aws/networkterragrunt initterragrunt plan # Plan: 3 to add (vpc + 2 subnets), 0 to change, 0 to destroy.terragrunt apply # Apply complete! Resources: 3 added.สังเกตว่า Terragrunt เขียน provider.tf ลงใน working directory จาก block generate ของ root — คุณไม่เคยเขียนไฟล์นี้เอง ตอนนี้รันทั้ง tree จาก cloud root:
cd live/awsterragrunt run --all planGroup 1- Unit ./network
The stack at /.../live/aws will be processed in the following order...รื้อทิ้งเมื่อสำรวจเสร็จ:
cd live/aws/networkterragrunt destroy # Destroy complete! Resources: 3 destroyed.คุณเสร็จเมื่อ unit เดียว apply ได้สะอาด, provider.tf ที่ generate ออกมาโผล่โดยคุณไม่ได้เขียน, และ terragrunt run --all plan ค้นพบ tree
ตรวจสอบความเข้าใจ:
- สาม block ใน Terragrunt unit คืออะไร และแต่ละอันให้อะไร?
provider.tfใน working directory ของ unit มาจากไหน และทำไมจึงดีกว่าการเขียนหนึ่งตัวต่อ unit?dependency "network"ทำอะไรให้terragrunt run --allและทำไมการต่อสายถึงเหมือนกันได้บนทั้งสาม cloud?- เมื่อไรคุณควร
cdเข้า unit เดียวแล้ว apply แค่ตัวนั้น แทนที่จะคว้าrun --all?
คุณเปลี่ยน module ที่ reusable ให้เป็น deployment จริงด้วย Terragrunt unit สามบล็อก, generate provider ครั้งเดียวสำหรับทั้ง cloud, และ apply ทั้ง tree ด้วย run --all ไฟล์ของ unit เล็กเพราะทุกอย่างที่แชร์อยู่ใน root.hcl — ทุกอย่างยกเว้นสิ่งเดียวที่เราจงใจปล่อยเป็น local: state ต่อไปเราจะย้าย state ไปที่ remote backend ของแต่ละ cloud แล้วเพิ่ม locking
Next: Remote state & locking →