HCL, Provider และ Resource
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”configuration ของ Terraform ทุกส่วนคือ block เขียนด้วย HCL — terraform, provider, resource และ data คือสี่ตัวที่คุณจะเขียนบ่อยที่สุด รวมกันแล้วสี่ตัวนี้บอก Terraform ว่าต้องโหลด plugin อะไร คุยกับ Azure ยังไง ต้องสร้างอะไร และต้องอ่านอะไร
HCL block syntax
หัวข้อที่มีชื่อว่า “HCL block syntax”HCL (HashiCorp Configuration Language) สร้างจาก block รูปแบบทั่วไปคือ block type ตามด้วย label ที่ครอบด้วยเครื่องหมายคำพูดศูนย์ตัวขึ้นไป แล้วปิดท้ายด้วย body ของ argument ใน {}
block_type "label1" "label2" { argument = value}อย่าง resource block มี label สองตัว ส่วน provider block มี label ตัวเดียว comment ใช้ # หรือ // สำหรับบรรทัดเดียว และ /* */ สำหรับหลายบรรทัด string รองรับ interpolation ด้วย ${...} แต่ถ้าอ้างอิงค่าอื่นตรง ๆ (name = azurerm_resource_group.main.name) ไม่ต้องครอบด้วย ${} — syntax แบบนั้นใช้เฉพาะตอนอยู่ข้างใน string ที่ยาวกว่านั้น
# A single-line comment.// Also a single-line comment.
resource "azurerm_resource_group" "main" { name = "example-resources-${var.environment}" # interpolation inside a string location = "East US"}terraform block กับ provider block
หัวข้อที่มีชื่อว่า “terraform block กับ provider block”terraform block คือการตั้งค่า Terraform เอง ว่า Terraform เวอร์ชันไหนรัน configuration นี้ได้ และต้องใช้ provider อะไรบ้าง รวมถึงแต่ละ provider มาจากไหนและเวอร์ชันไหนที่รับได้
terraform { required_version = ">= 1.7.0"
required_providers { azurerm = { source = "hashicorp/azurerm" version = "~> 4.0" } }}source คือ registry address ของ provider (hashicorp/azurerm ชี้ไปที่ Azure provider ตัวจริงบน Terraform Registry) ส่วน version เป็น constraint ไม่ใช่การ pin ตายตัว — ~> 4.0 อนุญาตทุก release ในสาย 4.x
จากนั้น provider block ตั้งค่า instance หนึ่งของ provider ที่ประกาศไว้ด้านบน สำหรับ azurerm ตรงนี้คือที่อยู่ของ subscription — และมี quirk ที่รู้กันดีที่ควรพูดถึงตรง ๆ คือ features {} block เป็นสิ่งที่จำเป็นต้องมี แม้จะว่างเปล่าก็ตาม การไม่ใส่เลยเป็นหนึ่งใน error แรก ๆ ที่คนใหม่มักเจอกับ provider ตัวนี้
provider "azurerm" { features {}
subscription_id = "00000000-0000-0000-0000-000000000000"}resource block: สอง label ที่มีความหมาย และทำไม Azure ต้องมี resource group
หัวข้อที่มีชื่อว่า “resource block: สอง label ที่มีความหมาย และทำไม Azure ต้องมี resource group”resource block คือวิธีบอก Terraform ว่า “สิ่งนี้ควรมีอยู่” resource block มี label สองตัวเสมอ resource เกือบทุกตัวใน Azure ต้องอยู่ภายใน resource group — เป็น container/scoping construct ที่รวม resource ที่เกี่ยวข้องกันไว้ด้วยกัน คุม lifecycle ร่วมกันเป็นก้อนเดียว และไม่มี equivalent ตรง ๆ ใน AWS หรือ GCP (การจัดกลุ่มด้วย tag ใน AWS ไม่มีการบังคับใด ๆ อยู่เบื้องหลัง ส่วน GCP project ก็เป็น boundary ที่หนักกว่ามาก) นั่นทำให้ azurerm_resource_group เป็น resource แรกในเกือบทุก configuration ของ Azure
resource "azurerm_resource_group" "main" { name = "example-resources" location = "East US"}- label แรก
azurerm_resource_groupคือ type ของ resource ถูกกำหนดโดย provider และเป็นตัวตัดสินว่า argument ไหนใช้ได้ และ Azure API ตัวไหนที่ Terraform จะเรียก - label ที่สอง
mainคือ name ที่คุณเลือกเอง มีความหมายแค่ภายใน configuration นี้เท่านั้น
รวมกันแล้วทั้งสองตัวคือ address ของ resource นั้น คือ azurerm_resource_group.main ที่ใช้อ้างอิง attribute ของ resource นี้จากที่ไหนก็ได้ใน configuration ของคุณ
resource "azurerm_linux_virtual_machine" "web" { name = "web-vm" resource_group_name = azurerm_resource_group.main.name location = azurerm_resource_group.main.location size = "Standard_B1s" admin_username = "adminuser"
network_interface_ids = [ azurerm_network_interface.web.id, # defined elsewhere in this configuration ]
admin_ssh_key { username = "adminuser" public_key = file("~/.ssh/id_rsa.pub") }
os_disk { caching = "ReadWrite" storage_account_type = "Standard_LRS" }
source_image_reference { publisher = "Canonical" offer = "0001-com-ubuntu-server-jammy" sku = "22_04-lts" version = "latest" }}การอ้างอิง azurerm_resource_group.main.name และ azurerm_resource_group.main.location จาก VM resource คือ implicit dependency แบบเป๊ะ ๆ ที่สร้าง dependency graph ของ Terraform ตามที่จะพูดถึงด้านล่าง — ตอนนี้ Terraform รู้แล้วว่า resource group ต้องมีอยู่ก่อนถึงจะสร้าง VM ได้
data block: อ่านโดยไม่เป็นเจ้าของ
หัวข้อที่มีชื่อว่า “data block: อ่านโดยไม่เป็นเจ้าของ”data block อ่านข้อมูลของสิ่งที่มีอยู่แล้ว คือ infrastructure ที่ configuration นี้ไม่ได้จัดการและจะไม่มีวันสร้าง อัปเดต หรือลบ นี่คือ read-only lookup ของ Terraform
data "azurerm_resource_group" "existing" { name = "networking-shared"}
resource "azurerm_virtual_network" "app" { name = "app-vnet" address_space = ["10.0.0.0/16"] resource_group_name = data.azurerm_resource_group.existing.name location = data.azurerm_resource_group.existing.location}data.azurerm_resource_group.existing.location อ่านเหมือน resource address เป๊ะ ๆ แค่มี data. นำหน้า — label type กับ name ทำงานแบบเดียวกัน แต่ไม่มีอะไรจาก data block ที่จะโผล่เป็น create, update หรือ destroy ใน plan เลย เพราะมีหน้าที่อ่านอย่างเดียว
Dependency graph
หัวข้อที่มีชื่อว่า “Dependency graph”Terraform ไม่ได้ apply configuration ของคุณเรียงจากบนลงล่างตามที่เขียนไว้ แต่สร้าง DAG (directed acyclic graph) ของทุก resource และ data source แล้วใช้กราฟนั้นหาลำดับที่ถูกต้อง — พร้อมทั้งดูว่า resource ตัวไหนเป็นอิสระจากกันพอที่จะสร้าง พร้อมกัน ได้
edge ในกราฟนั้นมาจากสองที่
- Implicit dependency เมื่อไหร่ที่ argument ของ resource หนึ่งอ้างอิง attribute ของอีก resource หนึ่ง (เช่น VM อ้างอิง
azurerm_resource_group.main.nameหรือ subnet อ้างอิงชื่อของ virtual network) Terraform จะสรุปเองว่า resource ที่ถูกอ้างอิงต้องมีอยู่ก่อน - Explicit dependency คือ
depends_onบน resource ใช้ตอนที่ resource หนึ่งขึ้นกับอีกตัวจริง ๆ แต่ความสัมพันธ์นั้นไม่โผล่ผ่าน attribute reference ไหนเลย (เช่น role assignment ที่ต้องมีอยู่ก่อนแอปเริ่มพึ่งพา โดยไม่มี argument เชื่อมกันตรง ๆ)
resource "azurerm_resource_group" "main" { name = "example-resources" location = "East US"}
resource "azurerm_virtual_network" "main" { name = "example-vnet" address_space = ["10.0.0.0/16"] location = azurerm_resource_group.main.location # implicit dependency on azurerm_resource_group.main resource_group_name = azurerm_resource_group.main.name}
resource "azurerm_subnet" "app" { name = "app-subnet" resource_group_name = azurerm_resource_group.main.name virtual_network_name = azurerm_virtual_network.main.name # implicit dependency on azurerm_virtual_network.main address_prefixes = ["10.0.1.0/24"]}flowchart LR rg["azurerm_resource_group.main"] --> vnet["azurerm_virtual_network.main"] --> subnet["azurerm_subnet.app"] --> vm["azurerm_linux_virtual_machine.web"] storage["azurerm_storage_account.logs (unrelated, applies in parallel)"]