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