Directory-per-Environment Pattern
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”directory-per-environment ให้แต่ละ environment มี directory ที่แยกจากกันจริง ๆ พร้อม backend configuration ของตัวเอง โดยที่ root config ของทุก environment เรียก shared module เดียวกัน ด้วยค่า input ที่ต่างกัน
directory จริง, backend จริง, ต่อ environment
หัวข้อที่มีชื่อว่า “directory จริง, backend จริง, ต่อ environment”แทนที่จะเป็น state file เดียวที่เลือกด้วย workspace pattern นี้ใช้ directory จริงหนึ่งชุดต่อ environment แต่ละชุดมีไฟล์ .tf ระดับ root ของตัวเอง
environments/├── dev/│ └── main.tf├── staging/│ └── main.tf└── prod/ └── main.tfmodules/└── app/ └── main.tfความต่างสำคัญจากการ copy-paste environment ทั้งชุดคือสิ่งที่อยู่ข้างใน main.tf แต่ละไฟล์ ไม่ใช่ copy เต็มของ resource ของ application แต่เป็น root configuration เล็ก ๆ ที่มี backend block ของตัวเอง กับ module call เดียวเข้าไปยัง shared module ตัวเดียว
terraform { backend "s3" { bucket = "acme-terraform-state-dev" key = "app/terraform.tfstate" region = "us-east-1" use_lockfile = true }}
provider "aws" { region = "us-east-1" profile = "acme-dev"}
module "app" { source = "../../modules/app"
environment = "dev" instance_type = "t3.micro" instance_count = 1}terraform { backend "s3" { bucket = "acme-terraform-state-prod" key = "app/terraform.tfstate" region = "us-east-1" use_lockfile = true }}
provider "aws" { region = "us-east-1" profile = "acme-prod"}
module "app" { source = "../../modules/app"
environment = "prod" instance_type = "m5.large" instance_count = 3}สังเกตว่า backend แต่ละอันชี้ไปยัง S3 bucket ที่แยกกันจริง ผูกกับ profile แยก (และในของจริง มักเป็น AWS account ที่แยกกันจริง ๆ ด้วย) dev กับ prod แชร์ state กันโดยไม่ตั้งใจไม่ได้ เพราะไม่มี state ร่วมกันให้ไปแตะโดยพลาด resource จริงของ application อยู่ที่เดียวคือใน modules/app และทุก environment แค่เรียก module นั้นด้วยค่า instance_type กับ instance_count ที่ต่างกัน
สิ่งที่ pattern นี้แก้ได้จริง
หัวข้อที่มีชื่อว่า “สิ่งที่ pattern นี้แก้ได้จริง”ตรงนี้แก้ปัญหา isolation ที่อ่อนแอของ workspace ได้ตรง ๆ แต่ละ environment directory ชี้ไปยัง AWS account ของตัวเอง, region ของตัวเอง, และ backend bucket ของตัวเองได้ โดยไม่มีอะไรผูกมัดกันในระดับ Terraform เลย ไม่มี state file ร่วมให้ชื่อ workspace ที่พิมพ์ผิดไปแตะโดยพลาด และไม่มี provider block ร่วมให้ credential ผิดหลุดเข้าไปได้ ถ้า dev กับ prod ต้องอยู่คนละ AWS account จริง ๆ เพื่อ blast-radius isolation pattern นี้ทำได้ ส่วน workspace ทำไม่ได้
ปัญหา DRY ที่ pattern นี้พาเอากลับมา
หัวข้อที่มีชื่อว่า “ปัญหา DRY ที่ pattern นี้พาเอากลับมา”ลองดูสองไฟล์ main.tf นั้นอีกครั้ง input instance_type, instance_count, และ environment เป็นบรรทัดเดียวที่ต่างกันอย่างมีความหมายระหว่างสองไฟล์นี้ คือสามบรรทัดจากทั้งหมดประมาณสิบห้าบรรทัด ที่เหลือทั้งหมด ทั้งรูปของ backend "s3" block, provider "aws" block, และ argument source ของ module "app" block คือ boilerplate ที่ซ้ำกันแทบทุกตัวอักษรในทุก environment directory bump module ไปเวอร์ชันใหม่, เพิ่ม provider argument ที่จำเป็นตัวใหม่, หรือเปลี่ยน convention ของ state key แล้วความเปลี่ยนแปลงนั้นต้องเอาไปใส่มือใน dev/main.tf, staging/main.tf, และ prod/main.tf ทีละไฟล์ เป็น failure mode แบบเดียวกับการทำซ้ำมือจากวิธี copy-paste directory ทั้งชุด เพียงแต่แคบลงเหลือแค่ส่วนหนึ่งของแต่ละไฟล์ที่เล็กกว่าเดิม แต่ก็ยังเป็นปัญหาจริง
flowchart LR mod["modules/app (shared source)"] devEnv["environments/dev/main.tf"] -->|module call with dev inputs| mod stagingEnv["environments/staging/main.tf"] -->|module call with staging inputs| mod prodEnv["environments/prod/main.tf"] -->|module call with prod inputs| mod devEnv -.->|near-identical backend + provider + source blocks| stagingEnv stagingEnv -.->|near-identical backend + provider + source blocks| prodEnv