Terraform Workspaces
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”Terraform workspace ทำให้ configuration เดียวเก็บ state file แยกกันได้หลายอัน แต่ละอันมีชื่อของตัวเอง เป็นวิธีถูก ๆ ที่จำลอง dev, staging, prod แต่ workspace ไม่ได้เปลี่ยนอะไรเกี่ยวกับ backend, provider, หรือ Azure subscription ที่ configuration นั้นคุยด้วยจริง ๆ เลย
configuration เดียว หลาย state file
หัวข้อที่มีชื่อว่า “configuration เดียว หลาย state file”terraform workspace คือชุดคำสั่งในตัวสำหรับจัดการ state ที่มีชื่อ ภายใน working directory เดียว สร้าง workspace หนึ่งอันต่อ environment แล้ว Terraform จะเก็บ state file แยกกันโดยสมบูรณ์ให้แต่ละอัน
terraform workspace new devterraform workspace new stagingterraform workspace new prod
terraform workspace list# dev# staging# * prod
terraform workspace select devworkspace ที่ active อยู่คือตัวชี้เฉย ๆ ว่า workspace ไหนถูกเลือกอยู่จะเป็นตัวกำหนดว่า plan หรือ apply ครั้งถัดไปจะอ่านและเขียน state file ไหน ถ้าใช้ local backend แบบ default Terraform จะเก็บไว้ใต้ terraform.tfstate.d แยก subdirectory ต่อ workspace หนึ่งอัน
tree terraform.tfstate.dterraform.tfstate.d├── dev│ └── terraform.tfstate├── staging│ └── terraform.tfstate└── prod └── terraform.tfstateถ้าใช้ remote backend อย่าง azurerm แนวคิดเดียวกันนี้ยังใช้ได้ Terraform จะแยก namespace ของ state blob ต่อ workspace แทนที่จะแยกต่อ local directory แต่ผลลัพธ์เหมือนกันทุกประการ คือ dev มองไม่เห็นและเขียนทับ resource ของ prod ไม่ได้ เพราะอยู่คนละ state กันโดยสิ้นเชิง
การปรับพฤติกรรมด้วย terraform.workspace
หัวข้อที่มีชื่อว่า “การปรับพฤติกรรมด้วย terraform.workspace”Terraform เปิดชื่อของ workspace ที่กำลัง select อยู่ให้ใช้ผ่านค่า built-in ชื่อ terraform.workspace ซึ่ง interpolate ได้ทุกที่ที่ต้องการ string การใช้งานที่พบบ่อยที่สุดคือเลือกขนาดหรือชื่อต่อ environment จาก lookup map
locals { vm_size = { dev = "Standard_B1s" staging = "Standard_B2s" prod = "Standard_D2s_v5" }}
resource "azurerm_linux_virtual_machine" "app" { name = "app-${terraform.workspace}" resource_group_name = azurerm_resource_group.app.name location = azurerm_resource_group.app.location size = local.vm_size[terraform.workspace]
admin_username = "azureuser" # ...}select dev แล้ว VM จะได้ชื่อ app-dev และขนาด Standard_B1s select prod ไฟล์ .tf เดียวกันนี้จะได้ app-prod ขนาด Standard_D2s_v5 โดยไม่มีโค้ดซ้ำที่ไหนเลย นี่คือจุดขายของ workspace คือมีชุด .tf เดียว มีที่แก้ bug จุดเดียว และความต่างต่อ environment ขับเคลื่อนด้วย workspace ไหนกำลัง active อยู่ล้วน ๆ
สิ่งที่ workspace ไม่ isolate
หัวข้อที่มีชื่อว่า “สิ่งที่ workspace ไม่ isolate”นี่คือจุดที่พลาดง่าย terraform workspace new สร้างแค่ state file ใหม่เท่านั้น ไม่ได้สร้าง backend configuration ใหม่ ไม่ได้สร้าง provider configuration ใหม่ และไม่ได้สร้าง Azure subscription ใหม่ ทั้งหมดนั้นอยู่ใน .tf code เดียวกัน แชร์กันทุกตัวอักษรข้ามทุก workspace
terraform { backend "azurerm" { resource_group_name = "tfstate-rg" storage_account_name = "tfstateacct001" container_name = "tfstate" key = "app.terraform.tfstate" }}
provider "azurerm" { features {} subscription_id = "00000000-0000-0000-0000-000000000000"}ทุก workspace ทั้ง dev, staging, และ prod อ่านเขียนผ่าน backend block เดียวกันนั้น และ authenticate ผ่าน provider block เดียวกันนั้น กับ subscription_id เดียวกันนั้น ไม่มี credential ที่ scope ต่อ workspace ไม่มี subscription ที่ scope ต่อ workspace และไม่มี copy ของ .tf code ที่ scope ต่อ workspace สิ่งเดียวที่ต่างกันระหว่าง workspace คือ state file ไหนถูกแตะ กับอะไรก็ตามที่เลือก key ไว้ด้วย terraform.workspace
นั่นทำให้ workspace เข้ากันไม่ดีกับ environment ที่ต้องการ Azure subscription, region, หรือ credential ที่แยกกันจริง ๆ และสร้างความเสี่ยงจริงที่เฉพาะเจาะจงขึ้นมาอันหนึ่ง คือไม่มีอะไรใน tooling หยุดใครสักคนไม่ให้อยู่บน prod workspace ทั้งที่คิดว่ายังอยู่บน dev แล้ว apply การเปลี่ยนแปลงที่ตั้งใจไว้สำหรับ environment เล็ก ๆ ตรงเข้าไปที่ production ได้ เพราะ backend, provider, และ subscription เบื้องหลังไม่เคยเปลี่ยนเลยสักนิด
flowchart LR config["One Terraform config, one backend block"] -->|terraform workspace new dev| devWs["dev workspace"] config -->|terraform workspace new staging| stagingWs["staging workspace"] config -->|terraform workspace new prod| prodWs["prod workspace"] devWs --> devState["dev state file"] stagingWs --> stagingState["staging state file"] prodWs --> prodState["prod state file"] devWs -.-> shared["Same subscription_id, same backend, same .tf code"] stagingWs -.-> shared prodWs -.-> shared
พิสูจน์: state path จริง, terraform.tfstate.d/, และค่าที่สลับจริง
หัวข้อที่มีชื่อว่า “พิสูจน์: state path จริง, terraform.tfstate.d/, และค่าที่สลับจริง”พิสูจน์ได้แบบ offline ด้วย local_file แทนที่ azurerm_linux_virtual_machine (กลไก terraform.workspace เดียวกัน):
locals { vm_size = { default = "Standard_B1s" dev = "Standard_B1s" staging = "Standard_B2s" prod = "Standard_D2s_v3" }}
resource "local_file" "app" { filename = "${path.module}/app-${terraform.workspace}.txt" content = "workspace=${terraform.workspace} size=${local.vm_size[terraform.workspace]}\n"}apply จริงใน default แล้ว terraform workspace new staging และ apply อีกครั้งด้วย config เดียวกัน:
$ terraform apply -auto-approve # default workspaceApply complete! Resources: 1 added, 0 changed, 0 destroyed.
$ terraform workspace new staging$ terraform apply -auto-approve # staging workspace, same .tf filesApply complete! Resources: 1 added, 0 changed, 0 destroyed.ไฟล์ที่ generate มาสองไฟล์พิสูจน์ว่า terraform.workspace สลับจริงตาม workspace มาจาก resource block เดียวกัน:
$ cat app-default.txtworkspace=default size=Standard_B1s$ cat app-staging.txtworkspace=staging size=Standard_B2sstate layout จริงบน disk — รูปร่างจริงของ terraform.tfstate.d/WORKSPACE/:
$ find . -iname "*.tfstate*" -not -path "./.terraform/*"./terraform.tfstate./terraform.tfstate.d./terraform.tfstate.d/staging/terraform.tfstatedefault คือกรณีพิเศษหนึ่งเดียว — state ของมันอยู่ที่ path terraform.tfstate ธรรมดา ตำแหน่งเดียวกับที่ state จะอยู่ถ้าไม่เคยใช้ workspace เลย workspace อื่นทุกตัวได้ subdirectory terraform.tfstate.d/ terraform workspace list ยืนยันว่ามีทั้งคู่จริง:
$ terraform workspace list default* stagingอ่านโค้ดที่ AI สร้าง
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้าง”- lookup map
local.vm_sizeที่ขาด key ของ workspace ที่ใช้งานจริงอยู่ —terraform.workspaceevaluate เป็น string อะไรก็ตามที่ active อยู่ การ index map ด้วย key ที่ไม่มี คือ error จริง ไม่ใช่ fallback เงียบ ๆ - CI script ไหนก็ตามที่รัน
terraform applyโดยไม่เช็คterraform workspace showก่อน — workspace selection คือ state ที่อยู่ใน process นั้น ๆ ไม่เกี่ยวอะไรกับ branch หรือ environment ที่ CI เข้าใจว่ากำลัง deploy
🐛 หา bug ในโค้ดนี้
locals {vm_size = { dev = "Standard_B1s" staging = "Standard_B2s"}}
resource "azurerm_linux_virtual_machine" "app" {size = local.vm_size[terraform.workspace]}เฉลย
lookup map มีแค่ key dev กับ staging เท่านั้น พิสูจน์ข้างบน terraform.workspace evaluate เป็น workspace อะไรก็ตามที่ active จริง ทันทีที่มีใครสร้างและสลับไป workspace prod local.vm_size[terraform.workspace] จะ fail ด้วย error จริง “key does not exist” ตอน plan
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”workspace คือ pointer ไม่ใช่ boundary — พิสูจน์ข้างบน terraform.tfstate.d/NAME/ คือกลไกทั้งหมด เป็นแค่ subdirectory ที่ Terraform เลือกจาก string ตัวเดียว อ่านกับไฟล์ .tf ชุดเดียวและ backend/provider/subscription_id เดียวที่ไม่เปลี่ยน
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ทางเลือก | ได้อะไร | เสียอะไร |
|---|---|---|
| Workspace | DRY จริง ๆ — resource block เดียว พิสูจน์ข้างบนว่าให้ค่าต่างกันต่อ workspace | ไม่ isolate backend/provider/subscription_id เลย พิสูจน์ข้างบน สิ่งเดียวที่แยก environment คือชื่อไหน active อยู่ตอนนั้น |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “แต่ละ workspace มี subscription_id หรือ backend bucket ของตัวเองได้” พิสูจน์ข้างบน: ทุก workspace อ่าน backend และ provider configuration เดียวกันเป๊ะ
- “default workspace ถูกเก็บแบบเดียวกับ workspace อื่นทุกตัว” พิสูจน์ข้างบนด้วย
findจริง: state ของdefaultไม่มี subdirectoryterraform.tfstate.d/เลย