ข้ามไปยังเนื้อหา

ติดตั้ง Terraform และโครงสร้างโปรเจกต์

ติดตั้ง Terraform ผ่าน version manager แทนการใช้ binary เดียวที่ pin เวอร์ชันตายตัว เขียน Azure resource จริงตัวแรกของคุณ แล้วแบ่งออกเป็นไฟล์หลายไฟล์ตามชื่อที่เป็นธรรมเนียม — ซึ่งจริง ๆ แล้ว Terraform ไม่ได้บังคับสักไฟล์เดียว

ติดตั้ง Terraform (และทำไม version manager ถึงดีกว่า binary เดียว)

หัวข้อที่มีชื่อว่า “ติดตั้ง Terraform (และทำไม version manager ถึงดีกว่า binary เดียว)”

คุณดาวน์โหลด Terraform binary เดียวจาก HashiCorp แล้ววางไว้ใน PATH ได้ วิธีนี้ใช้งานได้ดีสำหรับการทดลองคนเดียว แต่ทันทีที่คุณทำงานมากกว่าหนึ่งโปรเจกต์ วิธีนี้จะเริ่มพัง repository หนึ่งเขียนไว้กับ Terraform 1.5 อีกอันต้องการ feature ของ 1.9 ส่วนอันที่สาม migrate ไป OpenTofu ไปแล้ว การคอย sync binary กลางตัวเดียวให้ตรงกับโปรเจกต์ที่คุณกำลังทำอยู่ตลอดเวลาเป็นงานจุกจิกที่ผิดพลาดได้ง่าย

version manager แก้ปัญหานี้ด้วยการติดตั้งหลายเวอร์ชันไว้เคียงข้างกัน แล้วสลับใช้ตามแต่ละโปรเจกต์ โดยปกติขับเคลื่อนด้วยไฟล์ระบุเวอร์ชันที่ commit เข้า repository tenv เป็นตัวเลือกที่ดีเพราะไม่ได้ผูกติดกับเครื่องมือตัวเดียว จัดการเวอร์ชันของ Terraform, OpenTofu, Terragrunt และ Atmos ได้ทั้งหมดจาก CLI ตัวเดียว ซึ่งสำคัญมากเมื่อทีมของคุณรัน Terragrunt คู่กับ Terraform (จะพูดถึงทีหลังในคอร์สนี้)

Terminal window
# install tenv (macOS, via Homebrew)
brew install tenv
# install and pin a specific Terraform version for the current directory
tenv tf install 1.9.0
tenv tf use 1.9.0
# tenv reads a .terraform-version file (or the terraform block's
# required_version constraint) to pick the right version automatically
terraform version

ไม่ว่าคุณจะใช้วิธีติดตั้งแบบไหน คำสั่งที่ใช้งานกับ terraform binary เองในแต่ละวันจะเหมือนกันทุกประการ — version manager เปลี่ยนแค่วิธีที่ binary นั้นมาอยู่บนเครื่องคุณเท่านั้น

โปรเจกต์ Terraform ก็แค่ directory ที่มีไฟล์ .tf หนึ่งไฟล์หรือมากกว่านั้น ไฟล์ที่เล็กที่สุดที่ใช้งานได้จริงจะประกาศ provider, resource group และ resource หนึ่งตัวข้างใน

terraform {
required_version = ">= 1.7.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
provider "azurerm" {
features {}
}
resource "azurerm_resource_group" "main" {
name = "example-resources"
location = "East US"
}
resource "azurerm_storage_account" "reports" {
name = "acmemonthlyreports001"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
account_tier = "Standard"
account_replication_type = "LRS"
}

นี่คือ configuration ที่สมบูรณ์และ apply ได้จริง ไม่มี virtual network ไม่มี compute ไม่มี prerequisite อะไรนอกจาก credential ของ Azure ใน environment ของคุณ azurerm_storage_account เป็น resource แรกที่ดีด้วยเหตุผลนี้เป๊ะ ๆ — เพราะไม่ต้องพึ่งพาอะไรให้มีอยู่ก่อน นอกจาก resource group ของตัวเอง มีจุดแปลกอันหนึ่งที่ควรรู้ไว้ตั้งแต่ต้น คือ name ของ storage account ต้อง unique ทั่วทั้ง Azure ไม่ใช่แค่ใน subscription หรือ resource group ของคุณ — ใช้ได้แค่ตัวอักษรพิมพ์เล็กกับตัวเลข ห้ามมีขีด (dash) นั่นคือเหตุผลที่โปรเจกต์จริงมักจะ generate suffix แบบสุ่มหรือ hash แทนที่จะ hardcode ชื่ออย่างที่เห็นด้านบน

โปรเจกต์จริงแบ่งไฟล์เดียวนั้นออกเป็นหลายไฟล์ ตามธรรมเนียม

providers.tf
terraform {
required_version = ">= 1.7.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
provider "azurerm" {
features {}
}
variables.tf
variable "storage_account_name" {
description = "Globally unique name of the storage account to create"
type = string
default = "acmemonthlyreports001"
}
main.tf
resource "azurerm_resource_group" "main" {
name = "example-resources"
location = "East US"
}
resource "azurerm_storage_account" "reports" {
name = var.storage_account_name
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
account_tier = "Standard"
account_replication_type = "LRS"
}
outputs.tf
output "storage_account_id" {
description = "Resource ID of the created storage account"
value = azurerm_storage_account.reports.id
}

ชื่อ main.tf, variables.tf, outputs.tf, providers.tf และ versions.tf เป็น ธรรมเนียม ไม่ใช่ข้อบังคับของ Terraform Terraform ไม่ได้ parse ชื่อไฟล์เลยแม้แต่น้อย แต่โหลดไฟล์ .tf ทุกไฟล์ใน directory มารวมกันเป็น configuration เดียว และไม่สนใจว่าคุณจะตั้งชื่อไฟล์ว่า main.tf, zzz.tf หรือ whatever.tf resource ใน outputs.tf กับ variable ที่อ้างถึงใน main.tf ทำงานเหมือนกันทุกประการกับตอนที่ทุกอย่างอยู่ในไฟล์เดียว — Terraform สร้าง dependency graph แบบเดียวกันไม่ว่าจะแบ่งไฟล์แบบไหน การแบ่งตามหน้าที่ (provider, variable, resource, output) มีไว้เพื่อให้คนที่เปิด directory รู้ว่าต้องดูตรงไหน ไม่ใช่เพราะ Terraform ต้องการเส้นแบ่งนั้น

flowchart TB
  subgraph dir["Project directory"]
    main["main.tf"]
    vars["variables.tf"]
    outs["outputs.tf"]
    prov["providers.tf"]
    ver["versions.tf"]
  end
  main --> combined["One combined Terraform configuration"]
  vars --> combined
  outs --> combined
  prov --> combined
  ver --> combined
Terraform โหลดไฟล์ .tf ทุกไฟล์ใน directory มารวมเป็น configuration เดียว

มีสองคำสั่งที่ควรรันก่อน commit ทุกครั้ง และไม่มีต้นทุนอะไรเลยแม้จะรันบ่อยแค่ไหน

terraform fmt เขียนไฟล์ใหม่ในที่เดิมให้เป็น format มาตรฐานของ Terraform — indentation สม่ำเสมอ เครื่องหมาย = จัดแนวตรงกัน spacing สม่ำเสมอ แก้แค่ style ไม่แก้ logic

Terminal window
terraform fmt

terraform validate ตรวจสอบว่า configuration สอดคล้องกันเองภายใน คือ HCL syntax ถูกต้อง argument มี type ตรงกัน variable ที่ reference ถึงมีอยู่จริง จับ typo และความผิดพลาดเชิงโครงสร้างได้ก่อนที่คุณจะติดต่อ Azure ด้วยซ้ำ

Terminal window
terraform validate
Success! The configuration is valid.

สิ่งที่ validate ไม่ทำ คือตรวจสอบอะไรกับ Azure API จริง validate ไม่รู้เลยว่า subscription ของคุณชน service quota หรือเปล่า ชื่อ storage account ที่คุณเลือกถูกใช้ไปแล้วทั่วโลกหรือไม่ หรือ credential ของคุณมีสิทธิ์สร้างจริงหรือเปล่า — ทั้งหมดนี้จะโผล่มาให้เห็นตอนที่คุณรัน terraform plan เท่านั้น เพราะเป็นคำสั่งแรกที่ติดต่อกับ Azure จริง ๆ configuration หนึ่งอาจผ่าน validate ได้อย่างไม่มีปัญหา แต่ยังพังตอน plan หรือ apply ได้

Terraform สนใจไหมว่า resource ถูกประกาศใน main.tf หรือไฟล์อื่น
terraform fmt ทำอะไร
terraform validate ตรวจสอบอะไร
ทำไมการผ่าน terraform validate ถึงไม่การันตีว่าจะไม่เจอปัญหาฝั่ง Azure จริง