Module Composition and Sources
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”source argument บน module block บอก Terraform ว่าจะไปเอา code ของ module นั้นมาจากไหน และ infrastructure จริงที่ไม่เล็กเกิดจากการเอา module เล็ก ๆ หลายตัวมาประกอบกัน — output ของ module หนึ่งถูกส่งตรงเข้าไปเป็น input variable ของอีก module หนึ่ง
source มาจากไหนบ้าง
หัวข้อที่มีชื่อว่า “source มาจากไหนบ้าง”ทุก module block ต้องมี source และมีสามแบบหลักที่คุณจะเจอบ่อยมาก local relative path ชี้ไปที่ directory ภายใน repository ของคุณเอง public Terraform Registry resolve address สั้น ๆ แบบ <namespace>/<name>/<provider> ไปเป็น module ที่ publish ไว้และมี version Git URL ชี้ตรงไปที่ repository โดยจะ pin ไว้ที่ tag, branch หรือ commit ไหนก็ได้ผ่าน ?ref=
# local relative pathmodule "bucket" { source = "./modules/s3-bucket"}
# public Terraform Registrymodule "vpc" { source = "terraform-aws-modules/vpc/aws" version = "~> 6.0"}
# Git URL pinned to a specific tagmodule "vpc_from_git" { source = "git::https://github.com/acme/terraform-modules.git//vpc?ref=v1.4.0"}ทั้งสามแบบคือคำตอบที่ต่างกันของคำถามเดียวกัน คือ Terraform จะไปเอาไฟล์ .tf ของ child module นี้มาจากไหน ไม่มีอะไรในวิธีเขียน variables.tf, main.tf หรือ outputs.tf ข้างใน module ที่ต้องเปลี่ยนตาม source แบบไหนที่ caller เลือกใช้
ประกอบ module: output ของตัวหนึ่งกลายเป็น input ของอีกตัว
หัวข้อที่มีชื่อว่า “ประกอบ module: output ของตัวหนึ่งกลายเป็น input ของอีกตัว”configuration แบบแบนราบตัวเดียวที่มี resource เป็นร้อยตัวนั้นอ่านยากและ reuse ไม่ได้เลย infrastructure จริงถูกสร้างจาก module เล็ก ๆ ที่ focus งานเดียว แล้วต่อกันผ่าน output กับ input vpc module หนึ่งเปิด identifier ที่ module อื่นต้องการ
# Illustrative: modules/vpc/outputs.tf (the vpc module's own resources are omitted here)output "vpc_id" { description = "ID of the created VPC" value = aws_vpc.this.id}
output "private_subnet_ids" { description = "IDs of the private subnets" value = aws_subnet.private[*].id}ec2-instance module ประกาศ input ที่ต้องการเพื่อ launch เข้าไปใน network นั้น
variable "vpc_id" { description = "ID of the VPC to launch the instance into" type = string}
variable "subnet_id" { description = "ID of the subnet to launch the instance into" type = string}root configuration คือจุดที่ต่อทั้งสอง module เข้าด้วยกันจริง โดยส่ง output ของ module หนึ่งตรงเข้าไปเป็น input ของอีก module หนึ่ง
# root main.tfmodule "vpc" { source = "./modules/vpc" cidr_block = "10.0.0.0/16"}
module "web_server" { source = "./modules/ec2-instance"
vpc_id = module.vpc.vpc_id subnet_id = module.vpc.private_subnet_ids[0]}ทั้งสอง module ไม่จำเป็นต้องรู้จักกันเลย vpc module ไม่รู้ด้วยซ้ำว่าจะมี ec2-instance module มา consume output ของตัวเองในอนาคต และ ec2-instance module ก็ไม่รู้ว่า vpc_id ที่ได้รับมาจาก module หรือแค่ string ที่ hardcode ไว้ root configuration เป็นจุดเดียวที่รู้จักทั้งสอง module และเป็นคนเชื่อมทั้งสองเข้าด้วยกัน
public Terraform Registry คือตัวเร่ง productivity ตัวจริง
หัวข้อที่มีชื่อว่า “public Terraform Registry คือตัวเร่ง productivity ตัวจริง”pattern ทั่วไปอย่าง “VPC ที่มี public/private subnet” ถูกแก้ปัญหามาแล้วหลายรอบ public Terraform Registry มี community module ที่ดูแลดีและถูกใช้กันแพร่หลาย — terraform-aws-modules/vpc/aws เป็นหนึ่งในจุดเริ่มต้นที่พบบ่อยที่สุดสำหรับ AWS networking — และการหยิบตัวนี้มาใช้มักคุ้มค่ากับเวลาของคุณมากกว่าการเขียนสิ่งเดียวกันขึ้นมาเองตั้งแต่ศูนย์
module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "~> 6.0"
name = "acme-app-vpc" cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b"] private_subnets = ["10.0.1.0/24", "10.0.2.0/24"] public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]}อย่างไรก็ตาม “ใช้กันแพร่หลาย” ไม่เหมือนกับ “เชื่อได้แบบไม่ต้องตรวจ” ก่อนสร้าง production infrastructure บน Registry module ตัวไหน ควรอ่านให้แน่ใจว่า module นั้น create อะไรจริง ๆ บ้าง — resource, ค่า default และ IAM permission ที่สร้างในนามคุณ Registry module ก็ยังเป็น code ของคนอื่นที่รันด้วย AWS credential ของคุณอยู่ดี
ประกอบข้าม configuration แยกกันด้วย terraform_remote_state
หัวข้อที่มีชื่อว่า “ประกอบข้าม configuration แยกกันด้วย terraform_remote_state”ทุกอย่างข้างบนต่อ module เข้าด้วยกัน ภายใน configuration เดียว ที่ root มีทั้ง module.vpc และ module.web_server ใน terraform apply ครั้งเดียวกัน infrastructure จริงมักถูกแยกเป็น configuration ต่างหากจริง ๆ ที่มี state ของตัวเอง — configuration networking ของทีม platform ที่ apply ตามตารางของตัวเอง configuration ของทีม application ที่ apply แยกต่างหาก terraform_remote_state คือวิธีเชื่อมทั้งสองแบบ native ไม่ต้องใช้ Terragrunt เลย เป็น data source ที่อ่าน output ของ configuration อื่นตรง ๆ จาก state ของมัน ไม่ว่า state นั้นจะอยู่บน backend ไหนก็ตาม
# network/main.tf — a separate configuration, applied on its ownresource "aws_vpc" "this" { cidr_block = "10.0.0.0/16"}
output "vpc_id" { value = aws_vpc.this.id}# app/main.tf — a different configuration, reading network's statedata "terraform_remote_state" "network" { backend = "s3" config = { bucket = "my-company-terraform-state" key = "network/terraform.tfstate" region = "us-east-1" }}
resource "aws_instance" "web" { ami = "ami-0abcdef1234567890" instance_type = "t3.micro" subnet_id = data.terraform_remote_state.network.outputs.vpc_id}พิสูจน์จริงโดยไม่มี cloud credential เลย — configuration สอง config ที่ใช้ local backend แทน network กับ app โดย terraform_remote_state อ่าน state file จริงข้ามขอบเขตกัน
$ cd network && terraform apply -auto-approveOutputs:
vpc_id = "vpc-0123456789abcdef0"
$ cd ../app && terraform plan + content = <<-EOT vpc_id=vpc-0123456789abcdef0 EOToutput ของ plan แสดงค่าจริงที่อ่านมาจาก state file ของ network — data.terraform_remote_state.network.outputs.vpc_id resolve ออกมาเป็น string เดียวกันเป๊ะกับที่ apply ของ network เองสร้างไว้ โดยไม่มี dependency ตรงระหว่าง source code ของทั้งสอง configuration เลย ไม่มี configuration ไหน import อีกตัว app แค่รู้ shape ของ output ของ network และรู้ว่า state ของมันอยู่ที่ไหน
นี่คือกลไกที่ Terragrunt dependency block (อยู่ใน terragrunt-fundamentals/dependencies-between-units.mdx) สร้างต่อยอดขึ้นมาและมาแทนที่เป็นส่วนใหญ่สำหรับ setup multi-environment — terraform_remote_state คือตัวที่ทำงานกับ Terraform ธรรมดา ไม่ต้องมี Terragrunt เลย ซึ่งเป็นเหตุผลที่มันยังโผล่บ่อยมากใน codebase ที่มีมาก่อน Terragrunt หรือที่ตั้งใจไม่ใช้ Terragrunt เลย
moved ข้ามขอบเขต module refactor composition โดยไม่มี downtime
หัวข้อที่มีชื่อว่า “moved ข้ามขอบเขต module refactor composition โดยไม่มี downtime”composition ไม่ได้หยุดนิ่ง — resource ที่เริ่มอยู่ที่ root มักถูกดึงเข้าไปใน module ทีหลัง ทันทีที่ caller ตัวที่สองต้องการสิ่งเดียวกัน ถ้าปล่อยไว้เฉย ๆ refactor แบบนี้ Terraform จะอ่านว่า “address เก่าหายไป มี address ใหม่โผล่มา” ซึ่ง plan เป็น destroy-แล้ว-create moved block ที่ข้ามขอบเขต module บอก Terraform ว่านี่คือ object จริงตัวเดียวกัน
# root main.tfmoved { from = aws_instance.web to = module.web_server.aws_instance.this}
module "web_server" { source = "./modules/ec2-instance" # ...}พิสูจน์จริง ใช้ pattern local_file แบบไม่มี credential ที่คอร์สนี้ใช้ตลอดสำหรับกลไก state — resource ที่เคยอยู่ที่ root ถูกดึงเข้า module โดยมี moved ข้ามขอบเขต
$ terraform planlocal_file.config: Refreshing state... [id=5d54098470458257525a27f6f0b6de89e278569d]
Terraform will perform the following actions:
# local_file.config has moved to module.config.local_file.this resource "local_file" "this" { id = "5d54098470458257525a27f6f0b6de89e278569d" # (10 unchanged attributes hidden) }
Plan: 0 to add, 0 to change, 0 to destroy.Plan: 0 to add, 0 to change, 0 to destroy คือหลักฐานที่สำคัญ การดึง resource เข้า module ซึ่งเป็นหนึ่งใน refactor ที่พบบ่อยที่สุดใน configuration ที่กำลังโต ไม่จำเป็นต้องแปลว่าต้อง recreate AWS resource จริง — ตราบใดที่ moved block เดินทางไปพร้อมกับ refactor ใน commit เดียวกัน
flowchart LR
subgraph root["Root configuration"]
vpcmod["module vpc"]
ec2mod["module web_server"]
end
subgraph vpc["Child module: vpc"]
vpcout1["output vpc_id"]
vpcout2["output private_subnet_ids"]
end
subgraph ec2["Child module: ec2-instance"]
ec2in1["var.vpc_id"]
ec2in2["var.subnet_id"]
end
vpcmod --> vpcout1
vpcmod --> vpcout2
vpcout1 -->|module.vpc.vpc_id| ec2in1
vpcout2 -->|module.vpc.private_subnet_ids[0]| ec2in2
ec2in1 --> ec2mod
ec2in2 --> ec2mod Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”composition ในบทนี้มีสองรูปแบบที่ต่างกันมาก และการแยกให้ออกสำคัญมาก module block ประกอบกันภายใน configuration เดียวคือการต่อสายแบบ compile-time-ish — root configuration เห็น graph เต็มของทั้งสอง module และ plan พร้อมกันใน apply ครั้งเดียว terraform_remote_state ประกอบข้าม configuration คือการต่อสายแบบ runtime ต่อ snapshot — app อ่านสิ่งที่ network เขียนลง state ล่าสุด ซึ่งอาจ stale ทันทีที่ network เปลี่ยนอีกครั้ง โดยไม่มีการ re-plan อัตโนมัติฝั่ง app ทั้งสองแบบใช้ได้จริงทั้งคู่ ความต่างคือ blast radius กับความ stale ไม่ใช่ความถูกต้อง moved ข้ามขอบเขต module คือชิ้นที่สาม มันคือสิ่งที่ทำให้คุณ migrate ระหว่าง สองรูปแบบนี้ได้ (ดึง resource ที่ root เข้า module หรือกลับกัน) โดยไม่ให้ Terraform อ่าน refactor นี้เป็นการเปลี่ยนแปลง infrastructure จริง
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ได้อะไร | เสียอะไร |
|---|---|---|
ประกอบด้วย module (apply ครั้งเดียว) | root config เห็นภาพเต็ม plan เดียวแสดงผลกระทบทั้งหมด | ทุก module ใน graph ถูก plan พร้อมกัน — vpc เปลี่ยนนิดเดียวอาจขยาย plan ให้กว้างไปทุกอย่างที่ต่อกับมัน |
terraform_remote_state ข้าม config | lifecycle apply แยกกันจริง — network เปลี่ยนไม่บังคับให้ app plan ตาม | อ่าน snapshot ไม่ใช่ state สด — พิสูจน์ข้างบน app เห็นแค่สิ่งที่ network apply ล่าสุดเขียนไว้ อาจ drift จนกว่าจะ re-plan |
moved ข้ามขอบเขต module | refactor resource เข้า (หรือออกจาก) module โดยไม่ destroy/recreate — พิสูจน์ข้างบน 0 to add, 0 to change, 0 to destroy | ต้องเขียนและ commit ใน change เดียวกับ refactor เป๊ะ ไม่งั้นช่วงเวลาระหว่างนั้น plan เป็น destroy/recreate จริง |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
terraform_remote_stateให้การเชื่อมต่อแบบ live กับ state ปัจจุบันของ configuration อื่น” พิสูจน์ข้างบน มันอ่านสิ่งที่ถูกเขียนลง state object ล่าสุด — ถ้าnetworkเปลี่ยนและ apply ใหม่หลังจากappplan ไปแล้ว plan ครั้งถัดไปของapp(ไม่ใช่ครั้งที่รันไปแล้ว) เท่านั้นที่จะเห็นค่าใหม่ - “ต้องมี Terragrunt ถึงจะประกอบ Terraform configuration แยกกันได้” พิสูจน์ข้างบน
terraform_remote_stateคือdatasource ของ Terraform ธรรมดา ไม่ต้องมี Terragrunt เลย — มันมีมาก่อนdependencyblock ของ Terragrunt เองด้วยซ้ำ และยังโผล่บ่อยมากใน codebase ที่ไม่ใช้ Terragrunt เลย - “การดึง resource เข้า module เป็นเรื่อง disruptive โดยธรรมชาติ ต้อง destroy แล้ว recreate เสมอ” พิสูจน์ข้างบนด้วยตัวอย่าง
local_fileจริง ด้วยmovedblock ที่ข้ามขอบเขต module refactor แบบเดียวกันเป๊ะ plan ออกมาเป็น0 to add, 0 to change, 0 to destroy - “
versionconstraint ของ Registry module รับประกันว่า behavior ของ module ไม่เปลี่ยนเลย” ไม่ได้พิสูจน์หรืออ้างแบบนี้ในบทนี้ —~>ยังยอม patch/minor update ภายใน constraint ได้ (พูดถึงลึกในmodule-versioning-and-registries.mdx) ดังนั้นการ pin version แค่ลดขอบเขตของ drift ไม่ได้กำจัดมันทั้งหมด
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ตอน review การประกอบ module หรือการต่อสายข้าม configuration ที่ generate มา เช็คสิ่งเหล่านี้
terraform_remote_statedata source ที่สันนิษฐานว่าสะท้อน state ปัจจุบัน ของ configuration อื่นเสมอ — พิสูจน์ข้างบน มันสะท้อนแค่applyล่าสุดของ configuration นั้น ไม่ใช่การอ่านแบบ live- resource ที่ถูกย้ายเข้า module ในโค้ดที่ generate มา โดยไม่มี
movedblock คู่กัน — พิสูจน์ข้างบน สิ่งนี้อ่านเป็น destroy-แล้ว-create ถ้าไม่มี block นั้น ความเสี่ยงเดียวกับที่state-management/drift-detection-and-import.mdxพูดถึงสำหรับ refactor ภายใน configuration เดียวกัน - สอง Terraform configuration ที่แก้ resource จริงตัวเดียวกันผ่านคนละทาง (ตัวหนึ่งผ่าน
moduleอีกตัวผ่านterraform_remote_stateบวก resource block) แทนที่จะให้ตัวหนึ่งเป็นเจ้าของและอีกตัวแค่อ่าน output - Registry module source ที่ pin ไว้โดยไม่มี
versionconstraint เลย ซึ่งหัวข้อถัดไปเรื่อง module versioning พูดถึงเป็นความเสี่ยงจริงที่พิสูจน์ได้
🐛 หา bug ในโค้ดนี้
# app/main.tf — refactored so the instance now launches from a module,# migrated from a bare resource block in the same commitmodule "web_server" {source = "./modules/ec2-instance"subnet_id = data.terraform_remote_state.network.outputs.vpc_id}เฉลย
พิสูจน์ข้างบนด้วย plan จริง (local_file.config -> module.config.local_file.this) การดึง resource เข้า module โดยไม่มี moved { from = ... to = ... } block คู่กันเลย ไม่ได้อ่านเป็น refactor สำหรับ Terraform เลย — resource ที่เคยเป็น aws_instance.web ที่ root หายไป และ module.web_server.aws_instance.this โผล่มาเป็นตัวใหม่ ซึ่ง plan ออกมาเป็น ทำลาย EC2 instance ที่รันอยู่ launch ตัวใหม่ สำหรับอะไรก็ตามที่รับ traffic ผ่าน instance นั้น นั่นคือ downtime จริงและ instance ID ใหม่ ทั้งที่ผู้เขียนตั้งใจแค่จะจัดระเบียบโค้ด ทางแก้คือ moved block ใน commit เดียวกับ refactor — พิสูจน์ข้างบนแล้วว่าให้ Plan: 0 to add, 0 to change, 0 to destroy พร้อม note has moved to ที่ข้ามขอบเขต module แทน