The Multi-Environment Problem
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”setup จริงของ infrastructure ทุกอันต้องมีอย่างน้อย dev, staging, prod เป็น environment ที่แยกจากกันและ isolate กัน แล้วการเอา logic เดียวกันของ Terraform มาใช้ซ้ำในทุก environment โดยไม่ปล่อยให้ configuration แต่ละอัน drift ออกจากกันเงียบ ๆ คือปัญหาที่ Terraform ไม่ได้แก้ให้อัตโนมัติ
environment มีมากกว่าที่คิด
หัวข้อที่มีชื่อว่า “environment มีมากกว่าที่คิด”ตัวอย่าง Terraform ง่าย ๆ ทั่วไปมักจัดการแค่ environment เดียว แต่ setup จริงไม่เคยเป็นแบบนั้น อย่างน้อยต้องมี
- dev ราคาถูก ทิ้งได้ พังได้อย่างปลอดภัย
- staging จำลอง prod ให้ใกล้เคียงที่สุด ใช้ดักปัญหาก่อนจะปล่อยจริง
- prod ตัวที่รับ traffic จริงและลูกค้าจริง
หลายทีมมี subdivision มากกว่านั้นอีก เช่น copy แยกต่อ GCP region, copy แยกต่อลูกค้าสำหรับ B2B deployment ขนาดใหญ่ หรือ preview environment อายุสั้นที่ spin up ต่อ feature branch แล้วถูกทำลายทิ้งเมื่อ pull request merge แต่ละอันมักต่างจากกันตรง ๆ อย่างเป็นรูปธรรม เช่น Compute Engine machine type เล็กกว่าใน dev, Cloud SQL read replica น้อยกว่าใน staging, ปิด autoscaling ใน dev เพื่อประหยัดเงิน และที่สำคัญ prod มักถูกวางไว้ใน GCP project ที่แยกออกไปต่างหากจากที่เหลือทั้งหมด เพื่อให้ความผิดพลาดใน dev ไปไม่ถึงอะไรที่สำคัญ GCP project คือ boundary แข็ง ๆ ของ billing, IAM, และ quota ซึ่งเล่นบทบาท isolation แบบเดียวกับที่ AWS account แยกต่างหากทำให้ทีมที่ใช้ AWS ได้ จุดหลังนี้เรียกว่า blast-radius isolation คือการจำกัดว่าความผิดพลาดจาก apply หรือ credential ที่หลุดออกไปจะไปได้ไกลแค่ไหน
ทางแก้แบบไร้เดียงสา: copy-paste directory ต่อ environment
หัวข้อที่มีชื่อว่า “ทางแก้แบบไร้เดียงสา: copy-paste directory ต่อ environment”สัญชาตญาณแรกที่แทบทุกคนใช้คือสร้าง environment เดียวให้ทำงานได้ก่อน แล้ว copy ทั้ง directory ไปเป็น environment ที่เหลือ
# Three near-identical copies, created once by handtree environmentsenvironments├── dev│ └── main.tf├── staging│ └── main.tf└── prod └── main.tfวันแรก ทั้งสามไฟล์เหมือนกันทุกตัวอักษร ยกเว้นค่า hardcode ไม่กี่จุด เช่น machine type ตรงนี้ ชื่อ bucket ตรงนั้น ทุกอย่างใช้งานได้ และรู้สึกเร็วดี ไม่ต้องออกแบบ shared module ไม่ต้องคิด abstraction ให้ถูกต้อง แค่สาม folder ของโค้ด .tf ธรรมดา
# Day one: dev/main.tf, staging/main.tf, and prod/main.tf are all this rule, verbatimresource "google_compute_firewall" "allow_app_port" { name = "allow-app-port" network = "default"
allow { protocol = "tcp" ports = ["8080"] }
source_ranges = ["10.0.0.0/16"]}หกเดือนถัดมา security review flag ว่า rule นั้นใน prod หลวมเกินไป แล้วมีคนไปแก้ให้แน่นขึ้นตรงใน prod/main.tf โดยตรง
# Six months later: prod/main.tf was patched during a security review.# staging/main.tf and dev/main.tf still say 10.0.0.0/16 — nobody remembered to port the fix.resource "google_compute_firewall" "allow_app_port" { name = "allow-app-port" network = "default"
allow { protocol = "tcp" ports = ["8080"] }
source_ranges = ["10.0.5.0/24"]}ไม่มีใครทำอะไรร้ายหรือแม้แต่สะเพร่าตรงนี้เลย การแก้ rule ตรงที่ security review กำลังมองอยู่คือเรื่องสมเหตุสมผลในตอนนั้น แต่ตอนนี้สาม directory กลายเป็นสาม configuration ที่ต่างกันแบบเล็ก ๆ และไม่มีเอกสารบันทึกไว้เลยว่าต่างกันตรงไหน สั่ง diff ระหว่าง dev/ กับ prod/ หกเดือนต่อมาจะเจอความต่างเล็ก ๆ เป็นสิบจุด และไม่มีใครจำได้จากความทรงจำว่าจุดไหนตั้งใจให้ต่าง จุดไหนแค่ลืม port fix ตาม
ความตึงที่ module นี้จะพาไปดู
หัวข้อที่มีชื่อว่า “ความตึงที่ module นี้จะพาไปดู”ปัญหาลึก ๆ คือการ copy-paste directory ทั้ง environment optimize เพื่อ isolation คือแต่ละ environment ไม่กระทบกันจริง ๆ เพราะเป็นไฟล์คนละชุดกันเลย แต่แลกมาด้วยการ reuse ที่หายไปทั้งหมด bug fix ทุกตัว, label ที่เพิ่มเข้าไปเพื่อ track cost ทุกอัน, module version ที่ bump ทุกครั้ง ต้องไปทำซ้ำมือใน copy ทุกชุด และการทำซ้ำมือคือขั้นตอนแบบที่มักจะเงียบ ๆ หยุดเกิดขึ้นไปเอง
ที่เหลือของ module นี้คือการแก้ความตึงนั้น จะ reuse logic เดียวกันข้ามทุก environment ยังไง (ให้มีจุดเดียวสำหรับแก้ bug ไม่ใช่สามหรือห้าจุด) พร้อมกับยังคงเก็บ state, credential, และ blast radius ของแต่ละ environment ให้ isolate จากกันเต็มที่ Terraform เองมีคำตอบบางส่วนให้สองแบบ คือ workspace กับ directory-per-environment pattern และแต่ละแบบ ตามที่สองบทถัดไปจะแสดงให้เห็น แก้ปัญหาได้แค่ครึ่งเดียว แล้วเปิดช่องโหว่ให้อีกครึ่งที่เหลือ
flowchart LR origin["Working config, copied once"] -->|copy-paste| dev["dev/ (copy)"] origin -->|copy-paste| staging["staging/ (copy)"] origin -->|copy-paste| prod["prod/ (copy)"] prod -->|"security rule tightened here only"| prodDrift["prod/ (drifted)"] staging -->|"instance size bumped here only"| stagingDrift["staging/ (drifted)"] dev -->|"never touched again"| devDrift["dev/ (stale)"]
ทั้งสอง google_compute_firewall fence ข้างบนเป็น HCL จริงที่ valid — ยืนยันด้วย terraform fmt -check กับทั้งคู่ — drift ที่โชว์ระหว่างสองอันไม่ใช่ความต่างของ syntax เลย เป็นความต่างของค่า (source_ranges) เท่านั้น
อ่านโค้ดที่ AI สร้าง
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้าง”- สาม (หรือมากกว่า) environment directory ที่เหมือนกันเป๊ะ byte-ต่อ-byte ยกเว้นค่าไม่กี่ตัวที่ hardcode ไว้ — pattern copy-paste เป๊ะ ๆ ที่บทนี้อธิบาย
- ค่า hardcode (firewall CIDR, machine type, label) ที่ต่างกันระหว่าง environment directory โดยไม่มี comment อธิบายว่า ทำไม — แยกไม่ออกเลยระหว่าง drift โดยไม่ตั้งใจ กับตัวเลือกที่ตั้งใจให้ต่างกันตาม environment
- ไม่มีการพูดถึง GCP project แยกต่อ environment เลยในตัวอย่าง multi-environment ที่ generate มา — argument เรื่อง blast-radius ของ GCP โดยเฉพาะต้องอาศัยการแยกนั้นจริง ๆ
🐛 หา bug ในโค้ดนี้
# Generated "multi-environment setup" — three near-identical files# environments/dev/main.tf, environments/staging/main.tf, environments/prod/main.tfresource "google_compute_instance" "app" {name = "app-vm"machine_type = "e2-micro"labels = { environment = "prod"}}เฉลย
resource block นี้เหมือนกันเป๊ะ ถูก generate ออกมาให้ทั้งสาม directory — รวมทั้ง dev/main.tf และ staging/main.tf ที่ hardcode environment = "prod" ไว้ใน labels ด้วย นี่คือ artifact แบบ copy-paste ที่สมจริง block นี้ถูกเขียนครั้งเดียวสำหรับ prod แล้ว copy ไปอีกสอง directory โดยไม่มีใครอัปเดตค่าเดียวที่ควรต่างกันตาม environment cost report กับ resource filter ทุกตัวที่ key ด้วย label นั้นตอนนี้ misattribute spend ของ dev กับ staging ไปเป็น prod เงียบ ๆ และไม่มีอะไรใน Terraform เองจับได้เลย — config นี้เป็น HCL ที่ valid สมบูรณ์แบบและ apply ผ่านสะอาดทั้งสามที่
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”การ copy-paste directory คือการ optimize เพื่อคุณสมบัติหนึ่ง (isolation) โดยแลกกับอีกอัน (single source of truth) — ไม่มีของฟรีตรงนี้ มีแค่ trade ที่คุณเลือกทำอย่างตั้งใจ หรือสะดุดเข้าไปโดยไม่รู้ตัว
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ทางเลือก | ได้อะไร | เสียอะไร |
|---|---|---|
| copy-paste directory ต่อ environment | isolation เต็มที่ตั้งแต่วันแรก ไม่ต้อง design หรือเรียนรู้ abstraction เลย | ทุก fix ต้องเอาไปใส่มือทุกที่ พิสูจน์ข้างบนว่า drift เงียบ ๆ ได้ภายในหกเดือน |
| shared config เดียวไม่มีการแยก environment เลย | DRY สูงสุด มีจุดเดียวที่แก้อะไรก็ได้ | ไม่มี isolation เลย |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “drift ระหว่าง environment copy แปลว่ามีใครสักคนประมาท” พิสูจน์ข้างบน: การแก้ prod เป็นสิ่งที่สมเหตุสมผลและถูกต้องในตอนนั้น ปัญหาอยู่ที่โครงสร้าง
- “พอ copy environment เสร็จแล้วใช้งานได้ ความเสี่ยงก็จบแล้ว” ความเสี่ยงสะสมทบกันไปทุกครั้งที่มีการแก้ต่อจากนั้น — ไม่ใช่ต้นทุนครั้งเดียว แต่เป็นภาษีต่อเนื่อง