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

Terraform Workspaces

Terraform workspace ทำให้ configuration เดียวสร้าง state file แยกกันได้หลายอันโดยสลับด้วยชื่อ แต่ทุก workspace ยังรันผ่าน backend เดียวกัน provider configuration เดียวกัน และโค้ด .tf เดียวกันเป๊ะ

แทนที่จะ copy-paste ทั้ง directory ต่อ environment workspace ให้คุณเก็บชุดไฟล์ .tf ไว้ชุดเดียว แล้วแค่สลับว่า Terraform จะอ่านและเขียน state file ชื่อไหน

Terminal window
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod
terraform workspace list
# default
# * dev
# staging
# prod
terraform workspace select staging
terraform apply

terraform workspace new NAME สร้าง workspace แล้วสลับไปใช้ทันที terraform workspace select NAME สลับ workspace ที่ active อยู่โดยไม่สร้างใหม่ terraform workspace list แสดง workspace ทั้งหมดที่มีอยู่ พร้อมเครื่องหมาย asterisk บอกว่าตัวไหน active อยู่ workspace ที่ active ถูก track ไว้ในเครื่องของคุณเองที่ directory .terraform และทุก plan หรือ apply ที่คุณรันจะแตะแค่ state ของ workspace นั้นเท่านั้น state ของ dev กับ state ของ prod คือคนละไฟล์กันเลย ถึงแม้ทั้งคู่จะมาจาก main.tf เดียวกัน

Terraform expose ชื่อ workspace ที่ active อยู่เป็นค่า built-in ชื่อ terraform.workspace ซึ่งคุณ interpolate เข้าไปในชื่อ resource, label, หรือ lookup map ได้โดยตรง เพื่อเปลี่ยน behavior ต่อ workspace โดยไม่ต้องเขียนโค้ดแยกกันสำหรับแต่ละอัน

locals {
machine_types = {
dev = "e2-small"
staging = "e2-medium"
prod = "e2-standard-4"
}
}
resource "google_compute_instance" "app" {
name = "app-${terraform.workspace}"
machine_type = lookup(local.machine_types, terraform.workspace, "e2-small")
zone = "us-central1-a"
boot_disk {
initialize_params {
image = "debian-cloud/debian-12"
}
}
network_interface {
network = "default"
}
}

สลับไปที่ workspace prod แล้ว resource block เดียวกันนี้จะตั้งชื่อ instance เป็น app-prod และเลือกขนาดเป็น e2-standard-4 สลับไปที่ dev แล้วโค้ดเดียวกันเป๊ะจะได้ app-dev ขนาด e2-small ไม่มีอะไรในไฟล์ .tf เองเปลี่ยนระหว่าง environment เลย มีแค่ workspace ที่ active เท่านั้นที่เปลี่ยน

ตรงนี้คือจุดที่พลาดง่าย block backend, block provider "google", และโค้ด .tf ทุกบรรทัดที่เหลือ ถูกใช้ร่วมกันโดยไม่เปลี่ยนเลยข้ามทุก workspace

terraform {
backend "gcs" {
bucket = "acme-terraform-state"
prefix = "app"
}
}
provider "google" {
project = "acme-shared-project"
region = "us-central1"
}

Terraform จัดการแยก state object ของแต่ละ workspace ไว้ในเครื่องเดียวกันโดยอัตโนมัติ ทำให้ dev, staging, prod ไม่ทับ state file กันเอง แต่ bucket เอง, project ใน block provider "google", และ credential ที่ Terraform ใช้ authenticate เหมือนกันเป๊ะไม่ว่า workspace ไหนจะ active อยู่ ตรงนี้ไม่มี boundary แข็ง ๆ เลยสักนิด ไม่มีอะไรกันไม่ให้คนที่อยู่ใน workspace dev คิดว่ากำลังจะแก้ instance ทดสอบที่ทิ้งได้ ทั้งที่จริง ๆ ทุก workspace ใช้ GCP project acme-shared-project เดียวกันและ resource ที่ใกล้เคียง prod จริงที่อยู่ในนั้นร่วมกัน การเลือก terraform workspace select ผิดตามด้วย apply คือความผิดพลาดที่เกิดขึ้นได้ง่ายมาก และ workspace เองไม่มีทางป้องกันให้คุณเลย

flowchart TD
  config["Single main.tf + backend + provider google.project = acme-shared-project"]
  config -->|terraform workspace select dev| devState["dev state file"]
  config -->|terraform workspace select staging| stagingState["staging state file"]
  config -->|terraform workspace select prod| prodState["prod state file"]
  devState -.->|same backend, same project| shared["acme-shared-project"]
  stagingState -.->|same backend, same project| shared
  prodState -.->|same backend, same project| shared
Terraform configuration และ backend เดียว สาม workspace สาม state file GCP project เดียวที่ใช้ร่วมกัน

พิสูจน์ได้แบบ offline ด้วย local_file แทนที่ google_compute_instance (กลไก terraform.workspace เดียวกัน):

main.tf
locals {
machine_types = {
default = "e2-small"
dev = "e2-small"
staging = "e2-medium"
prod = "e2-standard-4"
}
}
resource "local_file" "app" {
filename = "${path.module}/app-${terraform.workspace}.txt"
content = "workspace=${terraform.workspace} machine_type=${lookup(local.machine_types, terraform.workspace, "e2-small")}\n"
}

apply จริงใน default แล้ว terraform workspace new staging และ apply อีกครั้งด้วย config เดียวกัน:

$ terraform apply -auto-approve # default workspace
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
$ terraform workspace new staging
$ terraform apply -auto-approve # staging workspace, same .tf files
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

ไฟล์ที่ generate มาสองไฟล์พิสูจน์ว่า terraform.workspace สลับจริงตาม workspace:

$ cat app-default.txt
workspace=default machine_type=e2-small
$ cat app-staging.txt
workspace=staging machine_type=e2-medium

state layout จริงบน disk:

$ find . -iname "*.tfstate*" -not -path "./.terraform/*"
./terraform.tfstate
./terraform.tfstate.d
./terraform.tfstate.d/staging/terraform.tfstate

default คือกรณีพิเศษหนึ่งเดียว — state ของมันอยู่ที่ path terraform.tfstate ธรรมดา workspace อื่นทุกตัวได้ subdirectory terraform.tfstate.d/

อ่านโค้ดที่ AI สร้าง และบั๊กจริงที่คอร์สนี้เองเคยมี

หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้าง และบั๊กจริงที่คอร์สนี้เองเคยมี”

google_compute_instance resource ที่ไม่มี network_interface block ไม่ใช่แค่ omission เชิง style — เป็น error จริงที่ provider บังคับ พิสูจน์ รัน terraform validate กับ google_compute_instance block ตัวเดียวกันเป๊ะในบทนี้ ก่อนที่จะแก้:

$ terraform validate
Error: Insufficient network_interface blocks
At least 1 "network_interface" blocks are required.

ตัวอย่างเดิมของบทนี้เองมีบั๊กนี้เป๊ะ ๆ — google_compute_instance ที่มี boot_disk แต่ไม่มี network_interface เป็นสิ่งที่ตัวอย่าง Compute Engine ที่ AI generate มักพลาดบ่อยมาก เพราะ resource ดูเหมือน จะครบแล้วด้วย image กับ machine type ทางแก้ ยืนยันด้วย terraform validate ที่ผ่านจริงหลังแก้ คือ network_interface { network = "default" } block ที่โชว์ข้างบนตอนนี้

🐛 หา bug ในโค้ดนี้

resource "google_compute_instance" "app" {
name = "app-${terraform.workspace}"
machine_type = "e2-small"
zone = "us-central1-a"
boot_disk {
initialize_params {
image = "debian-cloud/debian-12"
}
}
}
เฉลย

พิสูจน์ข้างบนด้วย terraform validate จริง: resource นี้ขาด network_interface block ที่จำเป็น google_compute_instance ต้องมีอย่างน้อยหนึ่งอัน provider ปฏิเสธ config ตั้งแต่ตอน validate ด้วย “Insufficient network_interface blocks” — ไม่ใช่เรื่องเซอร์ไพรส์ตอน plan หรือ apply แต่เป็นตอน validate เลย นี่คือบั๊กเดียวกันเป๊ะที่ตัวอย่างเดิมของบทนี้เองเคยมี

workspace คือ pointer ไม่ใช่ boundary — พิสูจน์ข้างบน terraform.tfstate.d/NAME/ คือกลไกทั้งหมด

ทางเลือกได้อะไรเสียอะไร
WorkspaceDRY จริง ๆ — resource block เดียว พิสูจน์ข้างบนว่าให้ค่าต่างกันต่อ workspaceไม่ isolate backend/provider/project เลย พิสูจน์ข้างบน
  • “แต่ละ workspace มี GCP project ของตัวเองได้” พิสูจน์ข้างบน: ทุก workspace อ่าน backend และ provider configuration เดียวกันเป๊ะ
  • “resource block ที่ reference machine_type กับ boot_disk ถูกต้องแล้วต้องครบสมบูรณ์” พิสูจน์ข้างบน: google_compute_instance block ที่ดู syntax สมเหตุสมผลก็ยัง fail terraform validate จริงเพราะขาด network_interface block — ความครบต้องเช็คกับ provider schema ไม่ใช่มองด้วยตา
Terraform workspace ให้สิ่งที่แยกกันจริงคืออะไร
terraform.workspace มักถูกใช้ทำอะไร
ความเสี่ยงจริงในโลกจริงที่ Terraform workspace สร้างขึ้นมาโดยเฉพาะคืออะไร
สิ่งไหนที่เหมือนกันเป๊ะข้ามทุก workspace ใน configuration เดียว
google_compute_instance resource มี boot_disk, machine_type, name ตั้งไว้ครบ แต่ไม่มี network_interface block terraform validate จริงโชว์อะไร พิสูจน์ข้างบน
state file ของ default workspace มี path จริงบน disk ต่างจาก workspace อื่นทุกตัวตรงไหน ตามที่พิสูจน์ข้างบน