Terraform Workspaces
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”workspace ของ Terraform ทำให้ configuration เดียวและ backend เดียวสร้าง state file ที่แยกจากกันได้หลายชุด สลับด้วยชื่อผ่าน CLI โดยไม่ต้องทำโค้ด .tf ซ้ำเลยสักบรรทัด
สร้างและสลับ workspace
หัวข้อที่มีชื่อว่า “สร้างและสลับ workspace”ทุก Terraform configuration เริ่มต้นด้วย workspace เดียวชื่อ default สร้างและสลับไปมาระหว่าง workspace ที่ตั้งชื่อเพิ่มได้ด้วยสามคำสั่งนี้
# Create a new workspace and switch to it immediatelyterraform workspace new dev
# List every workspace; the active one is marked with an asteriskterraform workspace list# default# * dev# staging# prod
# Switch to an existing workspaceterraform workspace select stagingแต่ละ workspace ได้ state file ของตัวเอง แต่ทุก workspace ยังอ่านไฟล์ .tf ชุดเดียวกันเป๊ะ และชี้ไปที่ backend configuration เดียวกันเป๊ะ สั่ง terraform plan ใน workspace dev จะมองแค่ state ของ dev เท่านั้น สลับไป staging แล้ว Terraform จะทำตัวเหมือนไม่เคยเห็น resource ของ staging มาก่อน เพราะในมุมของ state file นั้น ไม่เคยเห็นจริง ๆ
ปรับพฤติกรรมด้วย terraform.workspace
หัวข้อที่มีชื่อว่า “ปรับพฤติกรรมด้วย terraform.workspace”ชื่อ workspace ที่ active อยู่เข้าถึงได้ในตัว configuration ผ่านค่า built-in terraform.workspace การ interpolate ค่านี้ทำให้ไฟล์ .tf ชุดเดียวทำงานต่างกันไปตาม workspace ที่ select อยู่
locals { instance_size = { dev = "t3.micro" staging = "t3.small" prod = "m5.large" }}
resource "aws_instance" "app" { ami = data.aws_ami.app.id instance_type = local.instance_size[terraform.workspace]
tags = { Name = "app-${terraform.workspace}" }}ตรงนี้ DRY จริง ๆ มี aws_instance block เดียว มีจุดเดียวสำหรับแก้ bug ในนั้น แต่ instance size กับชื่อยังออกมาต่างกันตาม workspace ไม่มี directory copy-paste ไม่มี resource definition ซ้ำ
สิ่งที่ workspace ไม่ isolate
หัวข้อที่มีชื่อว่า “สิ่งที่ workspace ไม่ isolate”นี่คือข้อจำกัดที่ต้องจำไว้ workspace isolate แค่ state เท่านั้น ไม่เปลี่ยนอย่างอื่นเลย ทุก workspace ที่สร้างจาก configuration เดียวกันแชร์สิ่งเหล่านี้ร่วมกัน
- backend configuration เดียวกันเป๊ะ S3 bucket เดียวกัน, region เดียวกัน, account เดียวกัน
- provider configuration เดียวกันเป๊ะ AWS credential เดียวกัน เว้นแต่จะเขียน logic แบบ conditional เพื่อสลับเอง
- โค้ด
.tfเดียวกันเป๊ะ ไม่มี hard boundary ใด ๆ กัน workspacedevไม่ให้มี resource block ที่ ถ้า config ผิด ก็แตะ infrastructure ระดับ production ได้เท่ากับ workspace อื่น
นี่ทำให้ workspace เหมาะกับความต่างแบบเบา ๆ ที่โครงสร้างเหมือนกันทุกอย่าง เช่น preview environment อายุสั้นต่อ feature branch ที่แค่ต้องการ state ของตัวเองกับ instance size ที่เล็กกว่า เป็น use case ที่ดี แต่เป็นตัวเลือกที่แย่ทันทีที่ environment หนึ่งต้องการ AWS account จริง ๆ ที่ต่างกัน, region ต่างกัน, หรือ credential ต่างกันจากที่อื่น เพราะ workspace แสดงความต่างแบบนั้นไม่ได้เลย เป็นแค่ชื่อ
ความเสี่ยงที่คมกว่าและเกิดขึ้นในชีวิตจริงคือเรื่องคน ไม่ใช่เรื่อง architecture active workspace คือ state ชิ้นหนึ่งที่ CLI เลือกไว้ ซึ่งอยู่นอกไฟล์ .tf ไปเลย ไม่มีอะไรกัน engineer ที่คิดว่าตัวเองอยู่ใน dev สั่ง terraform apply แล้วมาพบทีหลังว่า terminal นั้นยังค้างอยู่ใน prod จากชั่วโมงก่อนหน้า ไม่มี credential แยก ไม่มี account boundary แยก ไม่มี confirmation prompt ที่ผูกกับชื่อ workspace เลย มีแค่ string ที่ลืมง่าย
flowchart LR cfg["One .tf config + one backend"] --> dev["workspace: dev (own state file)"] cfg --> staging["workspace: staging (own state file)"] cfg --> prod["workspace: prod (own state file)"]
พิสูจน์: state path จริง, terraform.tfstate.d/, และค่าที่สลับจริง
หัวข้อที่มีชื่อว่า “พิสูจน์: state path จริง, terraform.tfstate.d/, และค่าที่สลับจริง”ทั้งหมดข้างบนพิสูจน์ได้แบบ offline ด้วย local_file แทนที่ aws_instance (กลไก terraform.workspace เดียวกันเป๊ะ ไม่ต้องมี cloud เพื่อเห็นมัน):
locals { instance_size = { default = "t3.micro" dev = "t3.micro" staging = "t3.small" prod = "m5.large" }}
resource "local_file" "app" { filename = "${path.module}/app-${terraform.workspace}.txt" content = "workspace=${terraform.workspace} size=${local.instance_size[terraform.workspace]}\n"}apply จริงใน workspace default แล้ว terraform workspace new staging และ apply อีกครั้งด้วย config เดียวกันเป๊ะ:
$ terraform apply -auto-approve # default workspaceApply complete! Resources: 1 added, 0 changed, 0 destroyed.
$ terraform workspace new stagingCreated and switched to workspace "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=t3.micro$ cat app-staging.txtworkspace=staging size=t3.smallและ state layout จริงบน disk — นี่คือรูปร่างจริงของ terraform.tfstate.d/WORKSPACE/ พิสูจน์คำกล่าวอ้าง “state file ของตัวเอง” ข้างบนด้วย find จริง ไม่ใช่แค่พูดลอย ๆ:
$ find . -iname "*.tfstate*" -not -path "./.terraform/*"./terraform.tfstate./terraform.tfstate.d./terraform.tfstate.d/staging/terraform.tfstateworkspace default คือกรณีพิเศษหนึ่งเดียว state ของมันอยู่ที่ path terraform.tfstate ธรรมดา ตำแหน่งเดียวกับที่ state จะอยู่ถ้าไม่เคยใช้ workspace เลย — ทุก workspace อื่นได้ subdirectory ใต้ terraform.tfstate.d/ terraform workspace list ยืนยันว่ามีทั้งคู่จริง และตัวไหน active:
$ terraform workspace list default* stagingทำไม directory ถึงดีกว่า workspace สำหรับ isolation ของ environment จริง ๆ
หัวข้อที่มีชื่อว่า “ทำไม directory ถึงดีกว่า workspace สำหรับ isolation ของ environment จริง ๆ”layout terraform.tfstate.d/ ข้างบนคือข้อโต้แย้งในตัวมันเอง: state file ของ default กับ staging อยู่ ใน working directory เดียวกัน อ่านด้วย ไฟล์ .tf ชุดเดียวกันเป๊ะ ผ่าน backend configuration เดียวกันเป๊ะ ไม่มี directory boundary ไม่มี credential แยก และไม่มีอะไรบน disk ที่กันเชิงโครงสร้างไม่ให้คำสั่งที่รันผิด workspace ไปแตะ environment ผิดได้ สิ่งเดียวที่แยก dev จาก prod คือชื่อไหน active อยู่ใน terminal ตอนที่ apply รัน บทถัดไปเรื่อง directory-per-environment pattern แลก DRY-ness ที่พิสูจน์ข้างบน (resource block เดียว จุดเดียวสำหรับแก้ bug) กับการรับประกันเชิงโครงสร้างที่ workspace ให้ไม่ได้: prod/ คือ cd คนละที่ มักเป็น credential ชุดต่างกัน และที่สำคัญที่สุด ความผิดพลาดที่พิมพ์ตอนยืนอยู่ใน dev/ ไม่มีทางไปถึง prod/ เลย ในขณะที่ความผิดพลาดที่พิมพ์ตอน terminal บังเอิญอยู่ workspace prod มีทางไปถึงมันได้ทุกทาง
อ่านโค้ดที่ AI สร้าง
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้าง”ตอนรีวิว config ที่ใช้ workspace ที่ AI สร้าง ให้เช็คสิ่งเหล่านี้:
- lookup map
local.instance_size(หรือคล้าย ๆ กัน) ที่ขาด key ของ workspace ที่ใช้งานจริงอยู่ตัวใดตัวหนึ่ง — พิสูจน์ข้างบนterraform.workspaceevaluate เป็น string อะไรก็ตามที่ active อยู่ตอนนั้น การ index map ด้วย key ที่ไม่มี คือ error จริงของ Terraform ไม่ใช่ fallback เงียบ ๆ ไปหาค่า default - สคริปต์หรือ CI pipeline ไหนก็ตามที่รัน
terraform applyโดยไม่เช็ค (terraform workspace show) ก่อนว่า workspace ไหน active อยู่ — พิสูจน์ข้างบน workspace selection คือ state ที่อยู่ใน process นั้น ๆ ต่อเนื่องข้ามคำสั่ง ไม่เกี่ยวอะไรเลยกับ branch หรือ environment ที่ CI job เข้าใจว่ากำลัง deploy อยู่ terraform.workspaceที่ใช้ปรับอะไรเชิงโครงสร้าง —regionของ provider,bucketของ backend ที่ต่างไปเลย — แทนที่จะเป็นค่าระดับ resource อย่าง size หรือ tag พิสูจน์ข้างบน workspace แชร์ backend เดียวและ provider config เดียวกันข้ามทุก workspace โค้ดที่พยายามทำให้สิ่งพวกนั้นต่างกันต่อ workspace คือการฝืนกลไก
🐛 หา bug ในโค้ดนี้
locals {instance_size = { dev = "t3.micro" staging = "t3.small"}}
resource "aws_instance" "app" {instance_type = local.instance_size[terraform.workspace]}เฉลย
lookup map มีแค่ key dev กับ staging เท่านั้น พิสูจน์ข้างบน terraform.workspace evaluate เป็น workspace อะไรก็ตามที่ active จริง ทันทีที่มีใครสร้างและสลับไป workspace prod (การกระทำที่สมเหตุสมผลและคาดหวังได้เต็มที่ เมื่อดูจากหัวข้อทั้ง module นี้) local.instance_size[terraform.workspace] จะ fail ด้วย error จริง “key does not exist” ตอน plan ทางแก้คือทำ map ให้ครบทุก workspace จริง หรือใช้ fallback ผ่าน lookup(local.instance_size, terraform.workspace, "t3.micro")
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”workspace คือ pointer ไม่ใช่ boundary — พิสูจน์ข้างบน terraform.tfstate.d/NAME/ คือกลไกทั้งหมด เป็นแค่ subdirectory ที่ Terraform เลือกจาก string ตัวเดียว อ่านกับไฟล์ .tf ชุดเดียวที่ไม่เปลี่ยนและ backend เดียวที่ไม่เปลี่ยน “isolation” ทุกอย่างที่ workspace ให้ มาจาก string ตัวเดียวนั้นถูกต้องตอนที่คำสั่งรัน — พิสูจน์ข้างบนด้วย state-file layout จริง ไม่มีกำแพงเชิงโครงสร้างเพิ่มเติมที่ไหนเลย
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ทางเลือก | ได้อะไร | เสียอะไร |
|---|---|---|
| Workspace | DRY จริง ๆ — resource block เดียว พิสูจน์ข้างบนว่าให้ค่าต่างกันต่อ workspace โดยไม่ต้องทำซ้ำเลย | ไม่ isolate backend/provider/โค้ด .tf เลย พิสูจน์ข้างบน สิ่งเดียวที่แยก environment คือชื่อไหน active อยู่ตอนนั้น |
| Directory-per-environment (บทถัดไป) | กำแพงเชิงโครงสร้าง — ไม่มีทางที่ความผิดพลาดใน dev/ จะไปถึง prod/ | เอาความเสี่ยง copy-paste-ต่อ-directory จากสองบทก่อนกลับมา เว้นแต่จะจับคู่กับ shared module |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “แต่ละ workspace มี provider region หรือ backend bucket ของตัวเองได้” พิสูจน์ข้างบน: ทุก workspace อ่าน backend configuration และ provider configuration เดียวกันเป๊ะ มีแค่ state file เท่านั้นที่ต่างกัน
- “default workspace ถูกเก็บแบบเดียวกับ workspace อื่นทุกตัว แค่ชื่อต่างกัน” พิสูจน์ข้างบนด้วย
findจริง: state ของdefaultอยู่ที่ pathterraform.tfstateธรรมดา ไม่มี subdirectoryterraform.tfstate.d/เลย — workspace อื่นทุกตัวได้ subdirectory นั้นจริง - “สลับ workspace ปลอดภัย เพราะ Terraform จะเตือนถ้าฉันกำลังจะ apply ผิด workspace” พิสูจน์ข้างบนโดยการที่ไม่มี: ไม่มีคำสั่งไหนที่รันข้างบนแสดง warning แบบนั้นเลย
terraform workspace show/listเป็นวิธีเดียวที่เช็คได้ และไม่มีอะไรบังคับให้รันก่อนapply