Directory-per-Environment Pattern
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”directory-per-environment pattern ให้แต่ละ environment มี directory ของตัวเอง, backend ของตัวเอง, และ GCP project ของตัวเองเพื่อ isolation จริง โดยที่ทุก directory เรียก shared module ตัวเดียวกัน ทำให้ logic ของ infrastructure จริง ๆ อยู่ที่เดียวเป๊ะ
directory จริงต่อ environment ชี้ไปที่ project จริง
หัวข้อที่มีชื่อว่า “directory จริงต่อ environment ชี้ไปที่ project จริง”workspace ใช้ backend เดียวกัน, provider project เดียวกัน, และโค้ด .tf ชุดเดียวกันข้ามทุก environment ซึ่งคือ isolation ที่อ่อนแอตามที่บทก่อนหน้าชี้ไว้ pattern แบบ directory-per-environment แก้ตรงนี้ด้วยการให้แต่ละ environment มี root configuration ที่แยกกันจริง แต่ละอันมี backend block ของตัวเองและ provider "google" block ของตัวเองที่ชี้ไปที่ GCP project ของ environment นั้น
tree environmentsenvironments├── dev│ └── main.tf├── staging│ └── main.tf└── prod └── main.tfterraform { backend "gcs" { bucket = "acme-terraform-state-dev" prefix = "app" }}
provider "google" { project = "acme-dev-123456" region = "us-central1"}
module "app" { source = "../../modules/app" environment = "dev" machine_type = "e2-small" instance_count = 1}terraform { backend "gcs" { bucket = "acme-terraform-state-staging" prefix = "app" }}
provider "google" { project = "acme-staging-234567" region = "us-central1"}
module "app" { source = "../../modules/app" environment = "staging" machine_type = "e2-medium" instance_count = 2}terraform { backend "gcs" { bucket = "acme-terraform-state-prod" prefix = "app" }}
provider "google" { project = "acme-prod-345678" region = "us-central1"}
module "app" { source = "../../modules/app" environment = "prod" machine_type = "e2-standard-4" instance_count = 5}แต่ละ directory มี state bucket ของตัวเองและ project ของตัวเอง ทำให้ความผิดพลาดที่เกิดขึ้นตอน terraform apply รันอยู่ใน environments/dev ถูกจำกัดอยู่แค่ acme-dev-123456 เท่านั้น ความผิดพลาดนั้นไม่มีทางไปถึง acme-staging-234567 หรือ acme-prod-345678 เลย นี่คือ boundary แข็ง ๆ ที่ GCP IAM กับ billing บังคับไว้ ไม่ใช่แค่ข้อตกลงที่ต้องมีใครจำไว้เอง นั่นคือสิ่งที่ workspace ให้ไม่ได้พอดี
shared module ตัวเดียว caller สามตัวที่ต่างกัน
หัวข้อที่มีชื่อว่า “shared module ตัวเดียว caller สามตัวที่ต่างกัน”สังเกตว่าทั้งสาม directory เรียก source = "../../modules/app" เดียวกัน resource จริง ๆ ที่ประกอบเป็น “app” อยู่ที่เดียวเป๊ะ
resource "google_compute_instance" "app" { count = var.instance_count name = "app-${var.environment}-${count.index}" machine_type = var.machine_type zone = "us-central1-a"
boot_disk { initialize_params { image = "debian-cloud/debian-12" } }}bug fix, label ใหม่, หรือการทำ security-group ให้แน่นขึ้นที่ทำใน modules/app/main.tf จะมีผลกับ dev, staging, และ prod ในครั้งถัดไปที่แต่ละ environment รัน terraform apply มี resource logic แค่ชุดเดียวที่ต้องแก้ ซึ่งคือคุณสมบัติ DRY ที่การ copy-paste directory ตรง ๆ จากบทแรกไม่เคยมี
สิ่งที่ยังซ้ำซ้อนอยู่
หัวข้อที่มีชื่อว่า “สิ่งที่ยังซ้ำซ้อนอยู่”module เองยัง DRY อยู่ แต่ลองดูสามไฟล์ main.tf ข้างบนอีกครั้ง block terraform { backend "gcs" { ... } }, block provider "google" { ... }, และการต่อสาย module "app" { source = "../../modules/app" ... } คือ boilerplate ที่แทบเหมือนกันเป๊ะ ซ้ำทุกตัวอักษรในทุก environment directory มีแค่ชื่อ bucket, project ID, และ input value ของ module ไม่กี่ตัวที่ต่างกันจริง ๆ เพิ่ม environment ที่สี่เข้ามาแล้ว boilerplate ทั้งก้อนนั้นก็ถูก copy-paste เป็นครั้งที่สี่ เปลี่ยนวิธีจัดระเบียบ state เช่นเพิ่ม argument location เข้าไปใน backend block ทุกตัว การเปลี่ยนนั้นต้องเอาไปใส่มือใน environment directory ทุกอันที่มีอยู่ เป็นความเสี่ยงจากการทำซ้ำมือแบบเดียวกับที่บทแรกของ module นี้เตือนไว้เป๊ะ แค่ย้ายจาก module logic ไปอยู่ที่ backend กับ provider wiring รอบ ๆ แทน
flowchart TD shared["modules/app (shared source)"] devDir["environments/dev/main.tf"] -->|source| shared stagingDir["environments/staging/main.tf"] -->|source| shared prodDir["environments/prod/main.tf"] -->|source| shared devDir --> devProject["acme-dev-123456"] stagingDir --> stagingProject["acme-staging-234567"] prodDir --> prodProject["acme-prod-345678"] devDir -.->|nearly identical backend + provider block| stagingDir stagingDir -.->|nearly identical backend + provider block| prodDir