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

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> with terragrunt run --all <command>. The old form still works but is deprecated — this course uses run --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 แค่ตัวนั้น

ไม่ใช่ stub — network module ของจริง (ถึงจะ minimal) ที่ Module 4 จะขยายเป็น interface สาม cloud เต็ม module นี้สร้าง VPC และสอง subnet แล้ว expose output ที่เป็น contract:

modules/aws/network/main.tf
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 ได้อย่างปลอดภัย ระหว่างคุณเรียน

สำหรับบทนี้ เก็บ state เป็น local ไว้ (block remote_state เป็นงานของบทหน้า) แล้วโฟกัสที่ชัยชนะ DRY: provider ที่ generate ตัวเดียวสำหรับทุก unit

live/aws/root.hcl
generate "provider" {
path = "provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOF
provider "aws" {
region = "us-east-1"
}
EOF
}

unit คือสาม block: ดึง root เข้ามา, ชี้ไปที่ module, ส่ง input

live/aws/network/terragrunt.hcl
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

unit ต่อสายถึงกันผ่าน dependency อ่าน output ของ unit หนึ่งเป็น input ของอีก unit cluster unit (Module 5 สร้าง module) จะหน้าตาแบบนี้:

# live/aws/cluster/terragrunt.hcl — preview of the pattern
include "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 เดี่ยว ๆ ก่อน:

Terminal window
cd live/aws/network
terragrunt init
terragrunt 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:

Terminal window
cd live/aws
terragrunt run --all plan
Group 1
- Unit ./network
The stack at /.../live/aws will be processed in the following order...

รื้อทิ้งเมื่อสำรวจเสร็จ:

Terminal window
cd live/aws/network
terragrunt destroy # Destroy complete! Resources: 3 destroyed.

คุณเสร็จเมื่อ unit เดียว apply ได้สะอาด, provider.tf ที่ generate ออกมาโผล่โดยคุณไม่ได้เขียน, และ terragrunt run --all plan ค้นพบ tree

ตรวจสอบความเข้าใจ:

  1. สาม block ใน Terragrunt unit คืออะไร และแต่ละอันให้อะไร?
  2. provider.tf ใน working directory ของ unit มาจากไหน และทำไมจึงดีกว่าการเขียนหนึ่งตัวต่อ unit?
  3. dependency "network" ทำอะไรให้ terragrunt run --all และทำไมการต่อสายถึงเหมือนกันได้บนทั้งสาม cloud?
  4. เมื่อไรคุณควร 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 →