Writing a Reusable Module
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”module คือ directory ของไฟล์ .tf ธรรมดา — directory ที่คุณรัน terraform apply ตรง ๆ เรียกว่า root module ส่วน directory ไหนก็ตามที่ถูกเรียกผ่าน module block เรียกว่า child module ดังนั้นจึงไม่มีอะไรพิเศษเชิงโครงสร้างที่ทำให้ไฟล์กลุ่มหนึ่ง “เป็น module”
ไม่มี keyword module — มีแค่วิธีใช้งาน
หัวข้อที่มีชื่อว่า “ไม่มี keyword module — มีแค่วิธีใช้งาน”จุดนี้มักทำให้คนงงตอนได้ยินครั้งแรก Terraform ไม่มีไฟล์พิเศษ ไม่มี module.tf ไม่มี marker วิเศษที่เปลี่ยน directory ให้กลายเป็น module ทุก directory ของไฟล์ .tf ที่คุณเคยเขียนก็คือ module อยู่แล้ว directory ที่คุณรัน terraform init กับ terraform apply ตรง ๆ เรียกว่า root module ทันทีที่คุณอ้างอิง directory อื่นที่มีไฟล์ .tf ผ่าน module block directory นั้นก็กลายเป็น child module — แต่ไฟล์ .tf ข้างในก็หน้าตาเหมือนกับ root configuration ทุกประการ
# root main.tf — calling a child modulemodule "bucket" { source = "./modules/gcs-bucket" bucket_name = "acme-app-logs"}นั่นคือความต่างทั้งหมด child module คือ directory ของ Terraform configuration ที่ configuration อื่นอ้างอิงถึงผ่าน address ไม่มีอะไรใน .tf file ของ module ที่ต้องเปลี่ยนเพื่อให้เป็นจริงตามนี้ — directory เดียวกันนี้สามารถ apply เป็น root module ได้เหมือนกัน ถ้าคุณ cd เข้าไปแล้วรัน terraform apply เองตรงนั้น
ทำไม file layout ของ module ถึงเหมือน root config
หัวข้อที่มีชื่อว่า “ทำไม file layout ของ module ถึงเหมือน root config”เพราะ module ก็คือ Terraform configuration ตัวหนึ่ง จึงตาม convention สามไฟล์เดียวกับที่คุณใช้กับ root config อยู่แล้ว แค่เปลี่ยนกลุ่มเป้าหมายไป:
variables.tf— input ของ module นี่คือ public API surface ของ module คือสิ่งที่ caller ต้องหรืออาจส่งเข้ามาmain.tf— resource ที่ module นี้จัดการจริงoutputs.tf— ค่าที่ module นี้ส่งกลับไปให้ผู้เรียกใช้
ประเด็นนี้ควรจำให้ขึ้นใจ module ไม่ใช่ construct พิเศษสำหรับ templating ที่ซ้อนอยู่บน Terraform อีกที แต่คือ Terraform configuration ธรรมดา ที่บังเอิญรับ parameter ผ่าน variables.tf ตอนเข้า และส่งค่าออกผ่าน outputs.tf ตอนออก
variable "bucket_name" { description = "Name of the GCS bucket to create" type = string}
variable "project_id" { description = "ID of the GCP project that owns the bucket" type = string}resource "google_storage_bucket" "this" { name = var.bucket_name project = var.project_id location = "US"}output "url" { description = "GS URI of the created bucket" value = google_storage_bucket.this.url}caller เชื่อมต่อ module นี้ด้วย module block โดยใช้ bucket_name กับ project_id เป็น input และ module.<name>.url เป็น output ที่เอาไปอ้างอิงต่อที่อื่นได้:
# root main.tfmodule "logs_bucket" { source = "./modules/gcs-bucket" bucket_name = "acme-app-logs" project_id = "acme-app-prod"}
output "logs_bucket_url" { value = module.logs_bucket.url}ออกแบบ module ให้มี responsibility เดียวที่ชัดเจน
หัวข้อที่มีชื่อว่า “ออกแบบ module ให้มี responsibility เดียวที่ชัดเจน”ข้อผิดพลาดในการออกแบบ module ที่ใหญ่ที่สุดคือ scope creep — module ที่เริ่มจาก “VPC network” ตัวหนึ่ง ค่อย ๆ โตขึ้นมามี IAM binding ตรงนี้ มี Compute Engine instance ตรงนั้น มี Cloud Function ที่อื่นอีก จนไม่มีใครเอา module นี้ไปใช้ซ้ำกับงานอื่นได้เลยนอกจากงานเดิมที่สร้างมาเพื่อสิ่งนั้น ควรเล็งไปที่ module ที่ทำงานเดียวและ scope ชัดเจน เช่น “VPC network ที่มี public/private subnet” ไม่ใช่ “networking, IAM และ compute ทั้งหมดของ GCP เรา”
มีสองนิสัยเชิงปฏิบัติที่ช่วยให้ module reuse ได้จริง แทนที่จะเผลอ specific กับ caller คนเดียว:
- ตั้ง default ที่สมเหตุสมผลให้ input ที่เป็น optional ถ้า caller ส่วนใหญ่ต้องการค่าเดียวกันสำหรับ input หนึ่ง ให้ตั้ง
defaultไว้ในvariables.tfเพื่อให้ caller ธรรมดาไม่ต้องระบุเลย - อย่า over-parameterize ตั้งแต่วันแรก น่าเผลอทำมาก ที่จะ expose ทุก field ของ resource ให้เป็น variable ไว้ก่อน “เผื่อไว้” แต่ทุก variable ที่เพิ่มเข้ามาคือส่วนหนึ่งของ public API ของ module ที่ถาวร ที่คุณต้องเขียนเอกสารและดูแลต่อไป ควรเพิ่ม input ตอนที่ caller จริง ๆ ต้องการความยืดหยุ่นนั้น ไม่ใช่เพิ่มไว้ล่วงหน้าสำหรับ caller สมมติ
variable "bucket_name" { description = "Name of the GCS bucket to create" type = string}
variable "force_destroy" { description = "Allow bucket to be destroyed even if it still contains objects" type = bool default = false}ในตัวอย่างนี้ force_destroy มี default ที่สมเหตุสมผล — caller ส่วนใหญ่ไม่ต้องแตะเลย — ส่วน bucket_name ไม่มี default เพราะ caller แต่ละคนต้องการชื่อ bucket ที่ต่างกันจริง ๆ และไม่มี default ไหนที่เดาได้อย่างสมเหตุสมผล
flowchart LR
subgraph root["Root module"]
rmain["main.tf calls module.bucket"]
end
subgraph child["Child module: modules/gcs-bucket"]
vars["variables.tf (inputs)"] --> main["main.tf (resources)"]
main --> outs["outputs.tf (values exposed)"]
end
rmain -->|source = ./modules/gcs-bucket| vars
outs -->|module.bucket.url| rmain