Module Versioning and Registries
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”module source ที่ไม่ได้ pin version ไว้ สามารถ resolve ไปเป็น version ใหม่กว่าเดิมแบบเงียบ ๆ ในครั้งถัดไปที่มีคนรัน init ดังนั้นการ pin version constraint คือวินัยแบบเดียวกับที่คุณใช้กับ provider lock file อยู่แล้ว เพียงแค่เล็งไปที่ module code แทน provider code
ทำไม module version ที่ไม่ pin ถึงเป็นความเสี่ยงแบบเงียบ
หัวข้อที่มีชื่อว่า “ทำไม module version ที่ไม่ pin ถึงเป็นความเสี่ยงแบบเงียบ”module ที่มาจาก Registry หรืออ้างอิงผ่าน Git tag ไม่ได้ตายตัวเหมือน local relative path ถ้า module block ไม่มี version constraint การรัน terraform init -upgrade — หรือแม้แต่ init ครั้งแรกใน CI pipeline ที่ยังไม่เคยรันมาก่อน — ก็สามารถดึง version ใหม่กว่าของ module นั้นเข้ามาได้ โดยไม่มีใครได้อ่าน changelog ก่อนเลย การ upgrade ที่ดูเหมือนเล็กน้อยอาจ rename variable, เปลี่ยนค่า default หรือปรับโครงสร้าง resource ที่สร้างขึ้น และทั้งหมดนี้จะไม่โผล่ให้เห็นจนกว่า plan หรือ apply จะเริ่มทำงานต่างจากเมื่อวาน
# unpinned - always resolves to the latest version on init -upgrademodule "vpc" { source = "terraform-aws-modules/vpc/aws"}นี่คือ failure mode เดียวกับที่ provider lock file มีไว้ป้องกัน แค่ขยับขึ้นมาอีกชั้นหนึ่ง แทนที่ provider plugin จะเปลี่ยนไปโดยที่คุณไม่รู้ตัว กลายเป็น implementation ภายในของ module เองที่เปลี่ยนไปโดยที่คุณไม่รู้ตัวแทน
pessimistic constraint operator: version = ”~> 6.0”
หัวข้อที่มีชื่อว่า “pessimistic constraint operator: version = ”~> 6.0””การ pin module จาก Registry หรือ Git tag ด้วย version argument แก้ปัญหานี้ได้ pattern ที่ใช้กันทั่วไปคือ pessimistic constraint operator หรือ ~>
# pinned - allows 6.x patch and minor upgrades, blocks a 7.0 major bumpmodule "vpc" { source = "terraform-aws-modules/vpc/aws" version = "~> 6.0"}~> 6.0 แปลว่า “อนุญาต version ใดก็ได้ที่มากกว่าหรือเท่ากับ 6.0 ไปจนถึงแต่ไม่รวม 7.0” พูดอีกแบบคือ ยอมให้ Terraform ดึง patch และ minor release ภายในสาย 6.x ได้ — bug fix กับของใหม่ที่ backward-compatible — แต่ปฏิเสธการกระโดดไป 7.0 แบบเงียบ ๆ เพราะ major version bump คือสัญญาณที่คนเขียน module บอกว่าให้เตรียมรับ breaking change คุณยังเป็นคนเลือกเองว่าจะกระโดดตอนไหน และอ่าน changelog ของ module ก่อนได้เสมอ
พิสูจน์ ~> 6.0 ด้วยของจริง: terraform-aws-modules/vpc/aws, สถานะปัจจุบัน และ version ที่ resolve แล้วอยู่ที่ไหนจริง ๆ
หัวข้อที่มีชื่อว่า “พิสูจน์ ~> 6.0 ด้วยของจริง: terraform-aws-modules/vpc/aws, สถานะปัจจุบัน และ version ที่ resolve แล้วอยู่ที่ไหนจริง ๆ”terraform-aws-modules/vpc/aws — module ที่รู้จักกันดีและใช้กันแพร่หลายตัวเดียวกับที่พูดถึงก่อนหน้านี้ในคอร์สนี้ — ยัง maintain อยู่จริง ๆ ณ ตอนพิสูจน์นี้ ~> 6.0 resolve ไปเป็น release จริงและปัจจุบัน output จริงของ terraform init ไม่มีเลข version ที่กุขึ้นมาเลย
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"]}$ terraform initInitializing modules...Downloading registry.terraform.io/terraform-aws-modules/vpc/aws 6.7.3 for vpc...- vpc in .terraform/modules/vpc
Initializing provider plugins...- Finding hashicorp/aws versions matching "~> 6.0, >= 6.28.0"...- Installing hashicorp/aws v6.66.0...~> 6.0 resolve เป็น 6.7.3 — patch/minor release จริงและปัจจุบันภายในสาย 6.x พิสูจน์คำสัญญาของ constraint นี้เป๊ะ มันขยับเลยจาก 6.0.0 เพื่อรับ fix ใหม่ ๆ แต่ยังอยู่ในขอบเขต major version เดิม ด้วย mock AWS credential จริง (skip_credentials_validation ฯลฯ) plan ของ module นี้รันจบครบวงจรจริง
$ terraform plan...Plan: 17 to add, 0 to change, 0 to destroy.นี่คือรายละเอียดที่ควรแก้ไขให้ชัดเจน เพราะเผลอสันนิษฐานผิดได้ง่ายมากถ้าเทียบเคียงกับ provider version ของ module ที่ resolve แล้วไม่เคยถูกบันทึกใน .terraform.lock.hcl เลย ไฟล์นั้นมีไว้สำหรับ provider เท่านั้น พิสูจน์ — จาก init เดียวกันข้างบน เนื้อหา .terraform.lock.hcl (ตัดบางส่วน)
provider "registry.terraform.io/hashicorp/aws" { version = "6.66.0" constraints = "~> 6.0, >= 6.28.0" ...}ไม่มี module entry เลยที่ไหนในไฟล์นั้น version ของ module ที่ resolve แล้วไปอยู่ที่ .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-aws-modules/vpc/aws","Version":"6.7.3","Dir":".terraform/modules/vpc"}]}นั่นแปลว่า version = "~> 6.0" ในไฟล์ .tf ของคุณคือบันทึกถาวรและ commit ได้ ตัวเดียว ที่บอกว่า version ไหนของ module ที่ยอมรับได้ — ต่างจาก provider ที่ไม่มี lock file แยกต่างหากมา pin version ที่ resolve แล้วไว้สำหรับครั้งถัดไป ทุก init re-resolve constraint นั้นใหม่กับสิ่งที่ Registry publish ไว้ ณ ตอนนั้นเสมอ ซึ่งเป็นเหตุผลเป๊ะ ๆ ว่าทำไม constraint เอง ไม่ใช่ lock file ต่างหาก คือสิ่งที่ปกป้องคุณจากการกระโดดไป 7.0 โดยไม่มีใคร review
private module source กับการ publish module ของตัวเอง
หัวข้อที่มีชื่อว่า “private module source กับการ publish module ของตัวเอง”ไม่ใช่ทุก module ที่ควรอยู่บน public Registry module ที่เข้ารหัส networking layout, naming convention หรือ compliance requirement เฉพาะของบริษัทหนึ่ง ไม่ควรถูก publish ให้คนทั้งโลกเห็น สำหรับกรณีนี้ องค์กรมักใช้ private Git repository หรือ private module registry — private registry ของ HCP Terraform หรือตัวที่ self-host เอง — เพื่อให้ module ยังคงมี version และค้นหาได้ภายในองค์กร โดยไม่ต้อง public เลย
# private Git repositorymodule "internal_networking" {}
# private module registry (e.g. HCP Terraform)module "internal_networking_hcp" { source = "app.terraform.io/acme-corp/networking/aws" version = "~> 2.1"}ถ้าคุณต้องการ publish module ของตัวเองแบบ public จริง ๆ public Terraform Registry คาดหวัง naming convention ของ repository แบบเฉพาะ คือ terraform-<PROVIDER>-<NAME> เช่น terraform-aws-vpc Registry ใช้ชื่อนี้ในการแยกว่า module นี้เล็งไปที่ provider ไหนและควรเรียกว่าอะไร ดังนั้น repository ที่ไม่ตาม pattern นี้จะ publish ไม่ผ่าน
flowchart TB
subgraph pinned["Pinned version"]
p1["version = ~> 6.0"] --> p2["terraform init always resolves within 6.x"]
end
subgraph unpinned["Unpinned version"]
u1["no version constraint"] --> u2["terraform init -upgrade may resolve 7.0"]
u2 --> u3["breaking changes applied without changelog review"]
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 เลยจริง ๆ ความไม่สมมาตรนี้ไม่ใช่ bug ของ Terraform มันแปลว่า constraint string ในไฟล์ .tf ของคุณแบกน้ำหนักมากกว่า string เดียวกันของ provider เพราะไม่มี safety net ชั้นที่สองที่ commit ไว้คอยจับการ resolve ที่เซอร์ไพรส์แบบที่ lock file ทำให้ provider เขียน ~> 6.0 สำหรับ module คือบันทึกถาวรทั้งหมดของสิ่งที่ยอมรับได้ — เขียนผิดหรือลืมใส่ init ครั้งถัดไป resolve ไปที่ไหนก็ได้ที่คุณไม่ได้ review จริง
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ได้อะไร | เสียอะไร |
|---|---|---|
version = "~> 6.0" บน Registry module | อนุญาต patch/minor fix จริง (พิสูจน์ข้างบน resolve เป็น 6.7.3) ในขณะที่กันการกระโดดไป 7.0 โดยไม่มี review | ยังต้องเชื่อวินัย semantic versioning ของคนเขียน module — release ที่ “minor” ก็ยังมี breaking change ที่ไม่ตั้งใจได้ |
| ไม่มี 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-aws-modules/vpc/awsได้.terraform.lock.hclที่มีแค่provider "registry.terraform.io/hashicorp/aws"block — ไม่มี module entry เลยที่ไหน version ของ module ที่ resolve แล้วไปอยู่ที่.terraform/modules/modules.jsonแทน ซึ่งไม่ได้ตั้งใจให้ commit - “
terraform-aws-modules/vpc/awsเป็น community module เก่า ๆ ที่อาจถูกทิ้งไปแล้ว” เช็คตรง ๆ ข้างบน~> 6.0resolve เป็น6.7.3ซึ่งเป็น release จริงและปัจจุบัน และplanเต็มรูปแบบกับมัน (ด้วย mock credential) สำเร็จด้วย17 to add— module นี้ยัง maintain อยู่จริง ณ ตอนพิสูจน์นี้ - “เพราะ module ไม่มี lock file version constraint เลยไม่ค่อยสำคัญเท่าไหร่” พิสูจน์ข้างบนจากความไม่สมมาตรนั้นเอง เพราะไม่มี safety net ชั้นที่สองที่ commit ไว้สำหรับ module แบบที่ provider มี constraint string จึง สำคัญกว่า สำหรับ module ไม่ใช่น้อยกว่า มันคือสิ่งเดียวที่ยืนอยู่ระหว่างคุณกับการกระโดด major version โดยไม่มี review
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ตอน review module source ที่ generate มา เช็คสิ่งเหล่านี้
moduleblock จาก Registry หรือ Git tag ที่ไม่มีversion(หรือ?ref=) argument เลย — พิสูจน์ข้างบน นี่คือความเสี่ยงเรื่อง unpinned version ที่บทนี้เปิดไว้ตั้งแต่ต้น และมองข้ามได้ง่ายมากตอน review เร็ว ๆ เพราะ block ยัง “ดูครบ” อยู่- comment หรือ commit message ที่อ้างว่า version ที่ resolve แล้วของ module ถูก “lock” หรือ “pin ไว้ใน lock file” — พิสูจน์ข้างบน คำกล่าวอ้างนั้นจริงแค่สำหรับ provider เท่านั้น version ที่ resolve แล้วจริงของ module อยู่ใน
.terraform/modules/modules.jsonที่ gitignore ไว้ ไม่ใช่ที่ไหนที่ commit versionconstraint ที่ใช้=(pin ตายตัว) ทั้งที่ตั้งใจใช้~>หรือกลับกัน — pin ตายตัวกัน bug-fix release ด้วย ในขณะที่~>พิสูจน์ข้างบนแล้วว่าปล่อยให้ patch/minor fix จริงเข้ามาอัตโนมัติ- module เฉพาะของบริษัทที่ private ถูก publish ด้วยชื่อทั่วไปที่ไม่ตาม
terraform-<PROVIDER>-<NAME>— สิ่งนี้ทำให้ auto-detection ของ Registry สำหรับ module public พัง และควรระวังแม้กับ private registry ที่ naming discipline เดียวกันช่วยให้ module ค้นหาเจอ
🐛 หา bug ในโค้ดนี้
module "vpc" {source = "terraform-aws-modules/vpc/aws"version = "~> 6.0"}# ...six months later, deployed via CI...# "the module must have changed the lock file, let's check .terraform.lock.hcl"เฉลย
พิสูจน์ข้างบน ไม่มีอะไรให้เช็คใน .terraform.lock.hcl สำหรับ module นี้เลย — มันมีแค่ provider entry เท่านั้นเสมอ สมมติฐานใน comment คือความเข้าใจผิดที่บทนี้แก้ตรง ๆ version ที่ resolve แล้วของ module ไม่เคยถูกบันทึกใน lock file ที่ commit เหมือนของ provider ถ้า behavior ของ module นี้เปลี่ยนไปตลอดหกเดือน ที่จริงต้องดูคือ log output ของ terraform init เองจาก run ที่ทำให้มันเปลี่ยน (ดาวน์โหลด version ไหน) หรือ .terraform/modules/modules.json บนเครื่องนั้นเฉพาะเวลานั้น — และทางแก้ต่อไปคือทำให้ version constraint เองแม่นยำพอ (หรือเปลี่ยนเป็น = เพื่อ pin ตายตัว) ให้เหตุการณ์ “เรารันอยู่ version ไหนกันแน่” แบบนี้เกิดซ้ำไม่ได้อีก เพราะไม่มี lock file ให้พึ่งสำหรับ module แบบที่มีให้ provider