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 ไม่ได้ถูก freeze ไว้กับที่เหมือน local relative path ถ้า module block ไม่มี version constraint การรัน terraform init -upgrade — หรือแม้แต่ init ใหม่ใน CI pipeline ที่ไม่เคยรันมาก่อน — สามารถดึง release ใหม่กว่าของ module นั้นเข้ามาโดยไม่มีใคร review changelog ก่อน upgrade ที่ดูเหมือนเล็กน้อยสามารถเปลี่ยนชื่อ variable เปลี่ยน default หรือปรับโครงสร้าง resource ที่สร้างขึ้นใหม่ทั้งหมด และไม่มีอะไรแสดงให้เห็นจนกว่า plan หรือ apply จะเริ่มทำงานต่างจากเมื่อวาน
# unpinned - always resolves to the latest version on init -upgrademodule "vnet" { source = "Azure/avm-res-network-virtualnetwork/azurerm"}นี่คือ failure mode เดียวกันเป๊ะกับที่ provider lock file มีไว้เพื่อป้องกัน แค่ขยับขึ้นไปอีกระดับหนึ่ง: แทนที่จะเป็น provider plugin ที่เปลี่ยนไปโดยที่คุณไม่รู้ตัว กลายเป็น implementation ภายในของ module เองที่เปลี่ยนไปโดยที่คุณไม่รู้ตัวแทน
pessimistic constraint operator พิสูจน์ด้วยของจริง: version = ”~> 0.2”
หัวข้อที่มีชื่อว่า “pessimistic constraint operator พิสูจน์ด้วยของจริง: version = ”~> 0.2””การ pin module จาก Registry หรือ Git tag ด้วย version argument แก้ปัญหานี้ได้ pattern ที่ใช้กันทั่วไปคือ pessimistic constraint operator, ~> หัวข้อนี้ใช้ Azure Verified Modules (AVM) แทน community module เก่าอย่าง Azure/vnet/azurerm ที่ tutorial บางตัวยังอ้างถึง — เช็คตรงกับ GitHub แล้ว repository นั้นมี banner “This repository was archived by the owner” จริง และชี้ผู้ใช้ไปยัง AVM เป็นตัวแทน คอร์สนี้จึงสอน module ที่เป็นปัจจุบันจริง ๆ
module "vnet" { source = "Azure/avm-res-network-virtualnetwork/azurerm" version = "~> 0.2"
location = "eastus" parent_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/acme-rg" address_space = ["10.0.0.0/16"] name = "acme-vnet" enable_telemetry = false}output จริงของ terraform init — resolve จริง ไม่มีเลข version ที่กุขึ้นมาเลย — กับ azurerm ~> 5.0 ที่ pin ไว้ของคอร์สนี้เอง
$ terraform initInitializing modules...Downloading registry.terraform.io/Azure/avm-res-network-virtualnetwork/azurerm 0.22.2 for vnet...
Initializing provider plugins...- Finding hashicorp/random versions matching "~> 3.5, ~> 3.6"...- Finding hashicorp/azurerm versions matching "~> 5.0"...- Finding azure/azapi versions matching "~> 2.4, ~> 2.12"...- Finding azure/modtm versions matching "~> 0.3"...- Installing hashicorp/random v3.9.1...- Installing hashicorp/azurerm v5.7.0...- Installing azure/azapi v2.13.0...- Installing azure/modtm v0.4.0...$ terraform validateSuccess! The configuration is valid.~> 0.2 resolve เป็น 0.22.2 — release จริงและปัจจุบัน สังเกตสิ่งที่ init ทำให้เองโดยไม่ต้องสั่ง: AVM module นี้ต้องการ azure/azapi (resource ของมัน implement ผ่าน type azapi_resource แบบทั่วไป ไม่ใช่เรียก azurerm_virtual_network ตรง ๆ) และ azure/modtm (telemetry การใช้งานแบบไม่ระบุตัวตน ปิดไว้ตรงนี้ด้วย enable_telemetry = false) เป็น provider เพิ่มเติม — ทั้งสองตัวไม่ได้ถูกประกาศไว้ใน required_providers ของคอร์สนี้เอง แต่ init ดึงเข้ามาให้อัตโนมัติเพราะ manifest ของ module เองต้องการมัน ~> 5.0 แปลว่า “อนุญาต version ใด ๆ ที่มากกว่าหรือเท่ากับ 5.0 จนถึงแต่ไม่รวม 6.0” สำหรับ azurerm provider โดยเฉพาะ ส่วน module version constraint ~> 0.2 ตาม pattern เดียวกันเป๊ะ อีกระดับหนึ่ง สำหรับ release line ของ module เอง
version ที่ resolve แล้วจริง ๆ อยู่ที่ไหน — ไม่ใช่ .terraform.lock.hcl
หัวข้อที่มีชื่อว่า “version ที่ resolve แล้วจริง ๆ อยู่ที่ไหน — ไม่ใช่ .terraform.lock.hcl”นี่คือรายละเอียดที่ควรแก้ไขให้ชัดเจน เพราะเผลอสันนิษฐานผิดได้ง่ายมากถ้าเทียบเคียงกับ provider version ของ module ที่ resolve แล้วไม่เคยถูกบันทึกใน .terraform.lock.hcl เลย ไฟล์นั้นมีไว้สำหรับ provider เท่านั้น พิสูจน์ — จาก init เดียวกันข้างบน เนื้อหา .terraform.lock.hcl (ตัดบางส่วน)
provider "registry.terraform.io/hashicorp/azurerm" { version = "5.7.0" constraints = "~> 5.0" ...}ไม่มี module entry เลยที่ไหนในไฟล์นั้น — มีแต่ provider (azurerm, random, azapi, modtm ทั้งที่ดึงเข้ามาตรง ๆ หรือดึงมาโดย module เอง) 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":"vnet","Source":"registry.terraform.io/Azure/avm-res-network-virtualnetwork/azurerm","Version":"0.22.2","Dir":".terraform/modules/vnet"}, ...]}นั่นแปลว่า version = "~> 0.2" ในไฟล์ .tf ของคุณคือบันทึกถาวรและ commit ได้ ตัวเดียว ที่บอกว่า version ไหนของ module ที่ยอมรับได้ — ต่างจาก provider ที่ไม่มี lock file แยกต่างหากมา pin version ที่ resolve แล้วไว้สำหรับครั้งถัดไป ทุก init re-resolve constraint นั้นใหม่กับสิ่งที่ Registry publish ไว้ ณ ตอนนั้นเสมอ ซึ่งเป็นเหตุผลเป๊ะ ๆ ว่าทำไม constraint เอง ไม่ใช่ lock file ต่างหาก คือสิ่งที่ปกป้องคุณจากการกระโดดไป 1.0 (หรือไกลกว่านั้น) โดยไม่มีใคร review
private module source และการ publish module ของตัวเอง
หัวข้อที่มีชื่อว่า “private module source และการ publish module ของตัวเอง”ไม่ใช่ทุก module ที่ควรอยู่บน public Registry module ที่ encode networking layout, naming convention หรือ compliance requirement เฉพาะขององค์กรหนึ่งไม่ควรถูก publish ให้คนทั้งโลกเห็น สำหรับกรณีแบบนี้ องค์กรมักใช้ private Git repository หรือ private module registry — private registry ของ HCP Terraform หรือของที่ self-hosted เอง — เพื่อให้ module ยังมี version และค้นหาได้ภายในองค์กร โดยไม่ต้องกลายเป็น public
# private Git repositorymodule "internal_networking" {}
# private module registry (e.g. HCP Terraform)module "internal_networking" { source = "app.terraform.io/acme-corp/networking/azurerm" version = "~> 2.1"}ถ้าคุณต้องการ publish module ออกสู่สาธารณะ public Terraform Registry ต้องการ repository naming convention เฉพาะแบบนี้: terraform-<PROVIDER>-<NAME> AVM เองก็ตาม convention นี้เป๊ะ — module ที่พิสูจน์ตลอดบทนี้อยู่ใน GitHub repository ที่ชื่อ terraform-azurerm-avm-res-network-virtualnetwork ตรง ๆ Registry parse ชื่อนี้เพื่อดูว่า module เล็งไปที่ provider ไหนและควรเรียกว่าอะไร ดังนั้น repository ที่ไม่ตาม pattern นี้จะ publish ไม่สำเร็จ
flowchart TB
subgraph pinned["Pinned version"]
p1["version = ~> 5.0"] --> p2["terraform init always resolves within 5.x"]
end
subgraph unpinned["Unpinned version"]
u1["no version constraint"] --> u2["terraform init -upgrade may resolve 6.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 เลยจริง ๆ ความไม่สมมาตรนี้แปลว่า constraint string ในไฟล์ .tf ของคุณแบกน้ำหนักมากกว่า string เดียวกันของ provider เพราะไม่มี safety net ชั้นที่สองที่ commit ไว้คอยจับการ resolve ที่เซอร์ไพรส์แบบที่ lock file ทำให้ provider และพิสูจน์ข้างบน รูปร่างของ constraint เองก็สำคัญกว่าที่ดูภายนอก ~> 0.2 กับ ~> 0.2.0 ไม่ใช่คำสัญญาเดียวกันเลยสำหรับ module ที่ยังไม่ถึง 1.0
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ได้อะไร | เสียอะไร |
|---|---|---|
version = "~> 0.2" (สองส่วน) บน Registry module | ตาม release จริงอัตโนมัติภายในสาย major เดียวกัน — พิสูจน์ข้างบน resolve เป็น 0.22.2 จริงและปัจจุบัน | สำหรับ module ที่ยังไม่ถึง 1.0 “สาย major เดียวกัน” แปลว่าทั้งหมดของ 0.x — พิสูจน์ข้างบน กว้างกว่ารูปแบบสามส่วนมาก และยังอาจได้ interface ที่ต่างกันจริงในแต่ละ release |
version = "~> 0.2.0" (สามส่วน) | ล็อกไว้แค่ patch update ภายใน 0.2.x — พิสูจน์ข้างบน resolve เป็น 0.2.4 ที่เก่ากว่ามาก | พลาด fix และ feature จริงที่ publish ใน minor release ใหม่กว่าของ 0.x สำหรับ module ที่ยังพัฒนาอยู่และไม่ถึง 1.0 นี่อาจแปลว่าติดอยู่กับ release เก่าตลอดไป |
| ไม่มี 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 |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
Azure/vnet/azurermยังเป็นจุดเริ่มต้นมาตรฐานสำหรับ module networking ของ Azure” เช็คตรง ๆ กับ GitHub repository นั้นมี banner “This repository was archived by the owner” จริง และชี้ผู้ใช้ไปยัง Azure Verified Modules — พิสูจน์ข้างบนAzure/avm-res-network-virtualnetwork/azurermคือคำตอบปัจจุบันที่ยัง maintain อยู่ และมัน resolve กับ validate ผ่านจริงกับazurerm ~> 5.0ที่ pin ไว้ของคอร์สนี้ - “version ที่ resolve แล้วของ module ถูกบันทึกใน
.terraform.lock.hclเหมือนกับของ provider” พิสูจน์ข้างบนinitจริงกับ AVM vnet module ได้.terraform.lock.hclที่มีแค่providerblock เท่านั้น — ไม่มี module entry เลยที่ไหน version ของ module ที่ resolve แล้วไปอยู่ที่.terraform/modules/modules.jsonแทน ซึ่งไม่ได้ตั้งใจให้ commit - “
~> 0.2กับ~> 0.2.0แปลว่าประมาณเดียวกัน แค่ความละเอียดต่างกัน” พิสูจน์ข้างบนด้วยการรันทั้งสองแบบกับ Registry จริง~> 0.2resolve เป็น0.22.2(release0.xไหนก็ได้ที่>= 0.2) ในขณะที่~> 0.2.0resolve เป็น0.2.4(patch release ภายใน0.2.xเท่านั้น) — สำหรับ module ที่ยังไม่ถึง 1.0 การตัด segment ที่สามออกเปลี่ยนว่า component ไหนที่ pessimistic operator ได้รับอนุญาตให้เพิ่ม ไม่ใช่แค่ความแน่นของการ pin - “เพราะ 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 ที่บทนี้เปิดไว้ตั้งแต่ต้นmoduleblock ที่ยัง source จากAzure/vnet/azurermหรือAzure/naming/azurerm— เช็คข้างบนแล้วว่าทั้งสองถูก archive คำตอบปัจจุบันคือ Azure Verified Module- constraint แบบสองส่วน
~>(เช่น~> 0.2) ที่ใช้กับ module ที่ยังไม่ถึง 1.0 โดยดูเหมือนตั้งใจ pin ให้แน่นในสาย minor — พิสูจน์ข้างบน สิ่งนี้จริง ๆ อนุญาตช่วงกว้างกว่าที่ผู้เขียนน่าจะตั้งใจมาก~> 0.2.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>— พิสูจน์ข้างบน แม้แต่ AVM เองก็ตาม convention นี้เป๊ะ
🐛 หา bug ในโค้ดนี้
module "vnet" {source = "Azure/vnet/azurerm"version = "~> 5.0"
resource_group_name = azurerm_resource_group.main.namevnet_name = "acme-app-vnet"address_space = ["10.0.0.0/16"]}เฉลย
พิสูจน์ข้างบนด้วยการดึงข้อมูลจริงจาก GitHub repository Azure/vnet/azurerm มี banner “This repository was archived by the owner” จริง และ README ชี้ผู้ใช้ไปยัง Azure Verified Modules เป็นตัวแทน — module นี้ไม่ได้รับ update, security fix หรืองาน compatibility กับ azurerm provider เวอร์ชันใหม่เลย ไม่ว่า version constraint จะเขียนยังไง module call ที่ generate มาถูกต้องตามทุก convention ในบทนี้ (มี ~> constraint จริง, Registry address จริง) ก็ยังชี้ไปที่ทางตันได้ถ้า address เองเก่าไปแล้ว ทางแก้คือ module ที่พิสูจน์ตลอดบทนี้แทน source = "Azure/avm-res-network-virtualnetwork/azurerm", version = "~> 0.2" พร้อม interface ของ AVM (parent_id, address_space, name, enable_telemetry) แทนที่จะเป็นของ module เก่าที่ archive แล้ว (resource_group_name, vnet_name) — พิสูจน์ข้างบนแล้วว่า init และ validate ผ่านสะอาดกับ azurerm ~> 5.0 ที่ pin ไว้ของคอร์สนี้เอง