Module Versioning and Registries
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”version constraint บน module บอกว่า release ไหนของ Registry module หรือ Git-tagged module ที่ Terraform ได้รับอนุญาตให้ resolve ด้วยเหตุผลเดียวกับที่ version constraint ของ provider สำคัญ — ถ้าไม่มี terraform init -upgrade ธรรมดา ๆ ครั้งหนึ่งอาจดึง module release ใหม่ที่มี breaking change เข้ามาโดยไม่มีใคร review ก่อนเลย
ทำไมการ pin version ของ module ถึงสำคัญ
หัวข้อที่มีชื่อว่า “ทำไมการ pin version ของ module ถึงสำคัญ”นี่คือปัญหาเดียวกับที่ provider dependency lock file แก้ไข แค่ขยับขึ้นมาอีกชั้นหนึ่ง เมื่อ module block ดึง source มาจาก public Registry หรือ Git tag โดยไม่มี version constraint Terraform จะ resolve release ล่าสุดที่มีอยู่ทุกครั้งที่ init รันแบบ cold — clone ใหม่ใน CI, laptop เครื่องใหม่, cache ที่ถูกสร้างใหม่ ถ้าคนเขียน module ปล่อย breaking change มาใน release ใหม่นั้น — variable ที่เปลี่ยนชื่อ, default ที่ต่างไป, resource ที่ตอนนี้ต้อง replace แทนที่จะ update — การเปลี่ยนนั้นจะถูกดึงเข้ามาแล้ว apply โดยไม่มีใครในทีมได้อ่าน changelog ของ module นั้นก่อนเลย
module "vpc" { source = "terraform-google-modules/network/google" # no version constraint — always resolves to the latest release
project_id = var.project_id network_name = "app-network"}การ pin version เปลี่ยนเรื่องนี้ให้เป็นการตัดสินใจที่จงใจและ review ได้ แทนที่จะเป็นสิ่งที่เกิดขึ้นเองโดยบังเอิญ
module "vpc" { source = "terraform-google-modules/network/google" version = "~> 18.0"
project_id = var.project_id network_name = "app-network"}pessimistic constraint operator: ~>
หัวข้อที่มีชื่อว่า “pessimistic constraint operator: ~>”~> เรียกว่า pessimistic constraint operator ความหมายคืออนุญาตให้ upgrade ได้ แต่เฉพาะ upgrade ที่ไม่น่าจะทำให้อะไรพัง version = "~> 18.0" อนุญาตให้ Terraform resolve release ในสาย 18.x ตัวไหนก็ได้ — 18.1.0, 18.4.2, 18.12.0 — เพราะตาม semantic versioning ตัวเหล่านี้ควร backward compatible กัน แต่บล็อก 19.0.0 เอาไว้เด็ดขาด เพราะ major version bump คือจุดที่คนเขียน module ได้รับอนุญาตให้ใส่ breaking change เข้ามา
operator ตัวเดียวกันนี้ใช้ได้ละเอียดกว่านั้นอีกระดับ version = "~> 18.2.0" อนุญาต patch release อย่าง 18.2.1 กับ 18.2.7 แต่บล็อก 18.3.0 — มีประโยชน์ตอนที่คุณอยากกัน minor version bump ไว้ก่อนจนกว่าจะได้ review ก่อน ~> 18.0 ที่อนุญาตทั้งสาย 18.x คือ default ทั่วไปของทีมส่วนใหญ่ ส่วนรูปแบบที่แคบกว่าอย่าง ~> 18.2.0 เหมาะกับ module ที่เคยโดน minor version เปลี่ยน behavior มาก่อน
การ upgrade ข้ามขอบเขตที่ถูกบล็อกไว้ก็ยังทำได้ในคำสั่งเดียวเมื่อคุณพร้อม — แก้ version constraint ใน .tf file แล้วรัน terraform init -upgrade — constraint แค่ทำให้แน่ใจว่า upgrade นั้นเป็นสิ่งที่คนตัดสินใจทำเอง ไม่ใช่สิ่งที่เกิดขึ้นเงียบ ๆ ตอน init ครั้งถัดไป
พิสูจน์ ~> 18.0 ด้วยของจริง: terraform-google-modules/network/google และ argument subnets ที่มันต้องการจริง ๆ
หัวข้อที่มีชื่อว่า “พิสูจน์ ~> 18.0 ด้วยของจริง: terraform-google-modules/network/google และ argument subnets ที่มันต้องการจริง ๆ”terraform-google-modules/network/google เป็น module จริงและปัจจุบัน — ~> 18.0 resolve ได้จริง แต่ v18 ต้องการ argument ที่ตัวอย่างข้างบนไม่เคยแสดงเลย คือ subnets output จริงของ terraform validate พิสูจน์ว่าการไม่ใส่มันเป็น error จริง ไม่ใช่แค่เรื่องสไตล์
module "vpc" { source = "terraform-google-modules/network/google" version = "~> 18.0"
project_id = "acme-app-prod" network_name = "app-network"}$ terraform validateError: Missing required argument
on main.tf line 10, in module "vpc": 10: module "vpc" {
The argument "subnets" is required, but no definition was found.subnets ไม่ใช่ optional ใน v18 แม้แต่กับ VPC เล็ก ๆ นี่คือการเรียกที่แก้แล้วและ validate ผ่านจริง
module "vpc" { source = "terraform-google-modules/network/google" version = "~> 18.0"
project_id = "acme-app-prod" network_name = "app-network"
subnets = [ { subnet_name = "app-subnet-us-central1" subnet_ip = "10.0.1.0/24" subnet_region = "us-central1" } ]}$ terraform init...$ terraform validateSuccess! The configuration is valid.~> 18.0 resolve เป็น 18.3.0 จริงและปัจจุบัน — หลักฐานว่า module นี้ยัง maintain อยู่ ไม่ใช่ตัวอย่างเก่าที่ไม่ตรงกับความจริงอีกต่อไปที่คอร์สหนึ่งอาจเผลอพาต่อมาเรื่อย ๆ การเปลี่ยน required argument แบบนี้คือสิ่งที่ ~> ปกป้องคุณจากการข้ามไปแบบ ไม่มีใคร review ตรงขอบเขต major version (ในที่นี้คือ 17.x ไป 18.x) — แต่มันไม่ได้ปกป้องคุณจากตัวอย่างเก่าที่ copy-paste มาแล้วไม่เคย update ให้ตรงกับ version ที่ pin ไว้จริง
version ที่ resolve แล้วจริง ๆ อยู่ที่ไหน — ไม่ใช่ .terraform.lock.hcl
หัวข้อที่มีชื่อว่า “version ที่ resolve แล้วจริง ๆ อยู่ที่ไหน — ไม่ใช่ .terraform.lock.hcl”นี่คือรายละเอียดที่ควรแก้ไขให้ชัดเจน เพราะเผลอสันนิษฐานผิดได้ง่ายมากถ้าเทียบเคียงกับ provider version ของ module ที่ resolve แล้วไม่เคยถูกบันทึกใน .terraform.lock.hcl เลย ไฟล์นั้นมีไว้สำหรับ provider เท่านั้น พิสูจน์ — version ของ module ที่ resolve แล้วจาก init ข้างบนไปอยู่ที่ .terraform/modules/modules.json แทน ซึ่ง terraform init regenerate ใหม่ทุกครั้งและไม่ได้ตั้งใจให้ commit เข้า version control (มันอยู่ใน directory .terraform/ ที่ gitignore ไว้ คู่กับ module source ที่ดาวน์โหลดมาเอง)
$ cat .terraform/modules/modules.json{"Modules":[{"Key":"","Source":"","Dir":"."},{"Key":"vpc","Source":"registry.terraform.io/terraform-google-modules/network/google","Version":"18.3.0","Dir":".terraform/modules/vpc"}, ...]}ไม่มี module entry เลยที่ไหนใน .terraform.lock.hcl — มีแค่ provider block เท่านั้น นั่นแปลว่า version = "~> 18.0" ในไฟล์ .tf ของคุณคือบันทึกถาวรและ commit ได้ ตัวเดียว ที่บอกว่า version ไหนของ module ที่ยอมรับได้ — ต่างจาก provider ที่ไม่มี lock file แยกต่างหากมา pin version ที่ resolve แล้วไว้สำหรับครั้งถัดไป ทุก init re-resolve constraint นั้นใหม่กับสิ่งที่ Registry publish ไว้ ณ ตอนนั้นเสมอ ซึ่งเป็นเหตุผลเป๊ะ ๆ ว่าทำไม constraint เอง ไม่ใช่ lock file ต่างหาก คือสิ่งที่ปกป้องคุณจากการกระโดดไป 19.0 โดยไม่มีใคร review
private source และการ publish module ของตัวเอง
หัวข้อที่มีชื่อว่า “private source และการ publish module ของตัวเอง”ไม่ใช่ทุก module ที่ควรอยู่บน public Registry module ที่ encode security policy, naming convention หรือการเชื่อมต่อ service ภายในของบริษัทหนึ่งไม่มีเหตุผลที่ต้อง public และอาจไม่ควร public ด้วยซ้ำ Terraform รองรับ source mechanic แบบเดียวกันสำหรับกรณีนี้ — private Git repository (มักผ่าน SSH เพื่อให้ init authenticate ด้วย credential ที่มีอยู่แล้ว) หรือ private module registry เช่น private registry ของ HCP Terraform หรือ registry ที่ host เองภายในองค์กร
module "internal_service" { source = "git::ssh://[email protected]/acme-corp/terraform-modules.git//internal-service?ref=v1.4.0"
project_id = var.project_id}ถ้าคุณอยาก publish module ของตัวเองขึ้น public Terraform Registry Registry ต้องการ pattern ชื่อ repository แบบเฉพาะ คือ terraform-<PROVIDER>-<NAME> เช่น terraform-google-vpc สำหรับ module ที่ target provider google Registry จะ parse ชื่อนี้ตรง ๆ เป็น address ของ module (namespace/name/provider) ดังนั้น repository ที่ตั้งชื่อแบบอื่นจะไม่ถูกดึงเข้ามาเลย
flowchart LR
subgraph pinned["Pinned: version = ~> 18.0"]
p1["terraform init today"] --> p2["Resolves 18.4.2 (latest 18.x)"]
p2 --> p3["terraform init -upgrade later"]
p3 --> p4["Still resolves within 18.x, blocks 19.0.0"]
end
subgraph unpinned["Unpinned: no version constraint"]
u1["terraform init today"] --> u2["Resolves 18.4.2 (latest overall)"]
u2 --> u3["terraform init tomorrow"]
u3 --> u4["Silently resolves 19.0.0 with breaking changes"]
end Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”version ที่ resolve แล้วของ provider ถูก pin สองชั้น ชั้นแรกหลวม ๆ ด้วย version constraint ของคุณ ชั้นที่สองเป๊ะ ๆ ด้วย .terraform.lock.hcl version ที่ resolve แล้วของ module ถูก pin แค่ชั้นเดียว — ด้วย version constraint ของคุณเท่านั้น พิสูจน์ข้างบนด้วยการที่ .terraform.lock.hcl ไม่มี module entry เลยจริง ๆ ความไม่สมมาตรนี้แปลว่า constraint string ในไฟล์ .tf ของคุณแบกน้ำหนักมากกว่า string เดียวกันของ provider เพราะไม่มี safety net ชั้นที่สองที่ commit ไว้คอยจับการ resolve ที่เซอร์ไพรส์แบบที่ lock file ทำให้ provider และ ~> ปกป้องคุณแค่จากการกระโดด major version แบบ ไม่มีใคร review เท่านั้น — พิสูจน์ข้างบน มันไม่ได้ช่วยอะไรเลยถ้าตัวอย่างในคอร์ส (หรือ module call ของคุณเอง) ไม่เคย update ให้ตรงกับ required argument ที่ version ที่ pin ไว้ต้องการจริง ๆ
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ได้อะไร | เสียอะไร |
|---|---|---|
version = "~> 18.0" บน Registry module | อนุญาต patch/minor fix จริง (พิสูจน์ข้างบน resolve เป็น 18.3.0) ในขณะที่กันการกระโดดไป 19.0 โดยไม่มี review | ยังต้อง re-validate กับ version ที่ resolve แล้วจริง ๆ — พิสูจน์ข้างบน ~> 18.0 เพียงอย่างเดียวไม่จับว่า subnets กลายเป็น required แล้ว |
| ไม่มี lock file สำหรับ module (ต่างจาก provider) | มีไฟล์ที่ generate ให้ sync น้อยลงหนึ่งไฟล์ .terraform/modules/ regenerate สะอาดทุก init | version constraint string คือบันทึกที่ commit ได้เพียงตัวเดียวของ version ของ module ที่ยอมรับได้ — พิสูจน์ข้างบน ไม่มี .terraform.lock.hcl entry มา back up |
| private Git repo หรือ private registry สำหรับ module ภายในองค์กร | infrastructure detail เฉพาะของบริษัทไม่มีวัน public | ต้องมีวินัย versioning ของตัวเอง (Git tag จริง, private registry จริง) แทนที่จะพึ่ง convention ของ public Registry |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “version ที่ resolve แล้วของ module ถูกบันทึกใน
.terraform.lock.hclเหมือนกับของ provider” พิสูจน์ข้างบนinitจริงกับterraform-google-modules/network/googleได้.terraform.lock.hclที่มีแค่providerblock — ไม่มี module entry เลยที่ไหน version ของ module ที่ resolve แล้วไปอยู่ที่.terraform/modules/modules.jsonแทน ซึ่งไม่ได้ตั้งใจให้ commit - “ถ้า
versionconstraint ของ module ถูกต้อง โค้ดตัวอย่างที่เคยใช้ได้ก็จะยังใช้ได้อยู่” พิสูจน์ข้างบนด้วย error จริงsubnetsกลายเป็น required argument ที่จุดหนึ่งในสาย18.xและการเรียกแบบproject_id/network_nameเท่านั้นที่อาจเคย validate ผ่านกับ release เก่า ก็ fail กับ18.3.0ปัจจุบันด้วย “Missing required argument” - “เพราะ module ไม่มี lock file เลยไม่ค่อยสำคัญเท่าไหร่” พิสูจน์ข้างบนจากความไม่สมมาตรนั้นเอง เพราะไม่มี safety net ชั้นที่สองที่ commit ไว้สำหรับ module แบบที่ provider มี constraint string จึง สำคัญกว่า สำหรับ module ไม่ใช่น้อยกว่า
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ตอน review module source ที่ generate มา เช็คสิ่งเหล่านี้
moduleblock จาก Registry หรือ Git tag ที่ไม่มีversion(หรือ?ref=) argument เลย — พิสูจน์ข้างบน นี่คือความเสี่ยงเรื่อง unpinned version ที่บทนี้เปิดไว้ตั้งแต่ต้นterraform-google-modules/network/googleที่เรียกโดยไม่มีsubnetsargument — พิสูจน์ข้างบน นี่คือ error จริงและปัจจุบันตอน validate กับ~> 18.0ไม่ใช่สมมติฐาน- comment หรือ commit message ที่อ้างว่า version ที่ resolve แล้วของ module ถูก “lock” หรือ “pin ไว้ใน lock file” — พิสูจน์ข้างบน คำกล่าวอ้างนั้นจริงแค่สำหรับ provider เท่านั้น version ที่ resolve แล้วจริงของ module อยู่ใน
.terraform/modules/modules.jsonที่ gitignore ไว้ - module เฉพาะของบริษัทที่ private ถูก publish ด้วยชื่อทั่วไปที่ไม่ตาม
terraform-<PROVIDER>-<NAME>— สิ่งนี้ทำให้ auto-detection ของ Registry สำหรับ module public พัง และควรระวังแม้กับ private registry ที่ naming discipline เดียวกันช่วยให้ module ค้นหาเจอ
🐛 หา bug ในโค้ดนี้
module "vpc" {source = "terraform-google-modules/network/google"version = "~> 18.0"
project_id = "acme-app-prod"network_name = "app-network"}เฉลย
พิสูจน์ข้างบนด้วย error จริงของ terraform validate terraform-google-modules/network/google ต้องการ argument subnets จริงตามสาย ~> 18.0 ที่ pin ไว้ — “The argument `subnets` is required, but no definition was found” นี่คือสิ่งที่ version constraint ที่ถูกต้อง ไม่ ปกป้อง ~> 18.0 resolve เป็น 18.3.0 จริงและปัจจุบันได้ถูกต้อง constraint ทำหน้าที่ของมันแล้ว แต่ตัว module call เองไม่เคยถูก update ให้ตรงกับสิ่งที่ version นั้นต้องการจริง ๆ ทางแก้คือเพิ่ม subnets list จริง — พิสูจน์ข้างบนแล้วว่า validate ผ่านสะอาดทันทีที่มี subnet block อย่างน้อยหนึ่งตัว (subnet_name, subnet_ip, subnet_region)