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

Module Composition and Sources

source argument ใน module block บอกว่าโค้ดของ module นั้นอยู่ที่ไหน — local path, public Terraform Registry หรือ Git URL — และ infrastructure จริงถูกสร้างขึ้นด้วยการเอา output ของ module หนึ่งไปต่อเป็น variable ของอีก module ไม่ใช่เขียน configuration แบนราบก้อนเดียว

module block ทุกตัวต้องมี source argument และ Terraform รองรับรูปแบบหลักสามแบบ แบบง่ายที่สุดคือ local relative path ชี้ไปที่ directory ของไฟล์ .tf ที่อยู่ใน repository เดียวกับตัวที่เรียกใช้

module "vpc" {
source = "./modules/vpc"
project_id = var.project_id
network_name = "app-network"
}

local path เหมาะตอนที่ module ยังผูกกับ repository เดียวอยู่ หรือตอนที่คุณกำลังพัฒนาไปพร้อมกับโค้ดที่เรียกใช้ แต่พอ module reuse ได้จริงข้ามหลาย configuration คุณมักอยากเผยแพร่ไว้ที่ที่ทุก caller เข้าถึงได้เองโดยไม่ต้องผูกกับ repository ใดเป็นพิเศษ — ตรงนี้แหละที่ public Terraform Registry กับ Git source เข้ามามีบทบาท

public Terraform Registry เก็บ module จากชุมชนและ vendor ต่าง ๆ ภายใต้ address รูปแบบ namespace/name/provider ซึ่ง terraform init resolve ให้อัตโนมัติโดยไม่ต้องใช้ URL เลย

module "vpc" {
source = "terraform-google-modules/network/google"
version = "~> 9.0"
project_id = var.project_id
network_name = "app-network"
}

Git URL คือรูปแบบที่สาม เหมาะกับ module ส่วนตัวหรือภายในองค์กรที่ไม่ได้เผยแพร่ขึ้น registry ไหนเลย การต่อ ?ref= ท้าย URL คือการ pin source ไว้ที่ tag, branch หรือ commit ที่ระบุ เหมือนกับที่ version pin module จาก Registry

module "vpc" {
source = "git::https://github.com/acme-corp/terraform-modules.git//vpc?ref=v2.3.0"
project_id = var.project_id
network_name = "app-network"
}

สังเกต double slash ก่อนคำว่า vpc ใน Git URL — ทุกอย่างก่อนหน้านั้นคือ repository ที่จะ clone และทุกอย่างหลังจากนั้นคือ subdirectory ภายใน repository นั้นที่ module ตัวจริงอยู่ เรื่องนี้สำคัญเพราะ repository เดียวมักเก็บหลาย module ไว้ด้วยกัน ไม่ใช่แค่ตัวเดียว

infrastructure ที่ซับซ้อนจริง ๆ แทบไม่เคยเป็น configuration แบนราบก้อนเดียว แต่เป็น module เล็ก ๆ ที่ focus ชัดเจนหลายตัวต่อกัน โดย output ของ module หนึ่งกลายเป็น input variable ของ module ถัดไปโดยตรง ลองดู vpc module ที่สร้าง network กับ subnet ของตัวเอง และ compute-instance module ที่ต้องรู้ว่าจะ attach instance เข้ากับ network และ subnetwork ไหน

modules/vpc/outputs.tf
output "network_id" {
description = "Self link of the VPC network created by this module"
value = google_compute_network.this.id
}
output "subnet_ids" {
description = "Map of region to subnetwork ID"
value = { for s in google_compute_subnetwork.this : s.region => s.id }
}
modules/compute-instance/variables.tf
variable "network_id" {
description = "ID of the VPC network to attach the instance to"
type = string
}
variable "subnet_id" {
description = "ID of the subnetwork to attach the instance to"
type = string
}

root configuration instantiate module ทั้งสองตัวและส่ง output ของตัวแรกเข้าไปเป็น input ของตัวที่สองตรง ๆ โดยไม่ต้อง copy ID ด้วยมือระหว่างทาง

# root main.tf
module "vpc" {
source = "./modules/vpc"
project_id = var.project_id
network_name = "app-network"
}
module "compute_instance" {
source = "./modules/compute-instance"
project_id = var.project_id
network_id = module.vpc.network_id
subnet_id = module.vpc.subnet_ids["us-central1"]
}

Terraform resolve dependency ตรงนี้ให้อัตโนมัติ เพราะ module.compute_instance อ้างอิงถึง module.vpc.network_id Terraform จึงรู้ว่าต้องสร้าง network ก่อน instance โดยที่คุณไม่ต้องเขียน depends_on เพิ่มเลย

การเขียน vpc module เองตั้งแต่ศูนย์เป็นแบบฝึกหัดที่ดี แต่สำหรับงาน production มักไม่ใช่การใช้เวลาที่คุ้มค่าที่สุด องค์กร terraform-google-modules บน public Registry ดูแล module อย่าง terraform-google-modules/network/google ที่จัดการรายละเอียดยิบย่อยของ GCP networking ไว้แล้ว — custom subnet range, secondary range สำหรับ GKE, Shared VPC, Cloud NAT — ผ่านการใช้งานจริงจากชุมชนจำนวนมากที่เจอ edge case เดียวกับที่คุณจะเจอเองในที่สุด

แต่ module จาก Registry ก็ยังเป็นโค้ดของคนอื่นที่รันใน project ของคุณด้วย credential ของคุณ ก่อนจะพึ่งพาใน production ควรอ่านให้รู้ว่า module สร้างอะไรจริง ๆ — main.tf, ค่า default, IAM role หรือ API ที่เปิดใช้แทนคุณ module จาก Registry ที่ดูแลดีคือ productivity lever จริง ไม่ใช่ตัวแทนของการเข้าใจว่าอะไรถูก deploy เข้า GCP project ของคุณ

flowchart LR
  subgraph root["Root module"]
    rvpc["module.vpc"]
    rci["module.compute_instance"]
  end
  subgraph vpcmod["Child module: modules/vpc"]
    vout1["output: network_id"]
    vout2["output: subnet_ids"]
  end
  subgraph cimod["Child module: modules/compute-instance"]
    cin1["variable: network_id"]
    cin2["variable: subnet_id"]
  end
  rvpc --> vout1
  rvpc --> vout2
  vout1 -->|module.vpc.network_id| rci
  vout2 -->|module.vpc.subnet_ids| rci
  rci --> cin1
  rci --> cin2
Root configuration wiring vpc module outputs into compute-instance module inputs
source argument ของ module รองรับรูปแบบหลักสามแบบอะไรบ้าง
ในตัวอย่างการประกอบ vpc กับ compute-instance module compute-instance module รู้ได้อย่างไรว่าต้อง attach เข้ากับ network ไหน
query parameter ref บน Git module source ทำหน้าที่อะไร
เมื่อไหร่ที่การเลือกใช้ module จาก public Terraform Registry อย่าง terraform-google-modules/network/google เหมาะสมที่สุด