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

Directory-per-Environment Pattern

directory-per-environment ให้แต่ละ environment มี directory ที่แยกจากกันจริง ๆ พร้อม backend configuration ของตัวเอง โดยที่ root config ของทุก environment เรียก shared module เดียวกัน ด้วยค่า input ที่ต่างกัน

แทนที่จะเป็น state file เดียวที่เลือกด้วย workspace pattern นี้ใช้ directory จริงหนึ่งชุดต่อ environment แต่ละชุดมีไฟล์ .tf ระดับ root ของตัวเอง

Terminal window
environments/
├── dev/
└── main.tf
├── staging/
└── main.tf
└── prod/
└── main.tf
modules/
└── app/
└── main.tf

ความต่างสำคัญจากการ copy-paste environment ทั้งชุดคือสิ่งที่อยู่ข้างใน main.tf แต่ละไฟล์ ไม่ใช่ copy เต็มของ resource ของ application แต่เป็น root configuration เล็ก ๆ ที่มี backend block ของตัวเอง กับ module call เดียวเข้าไปยัง shared module ตัวเดียว

environments/dev/main.tf
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
}
environments/prod/main.tf
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 ที่ต่างกัน

ตรงนี้แก้ปัญหา 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 ทำไม่ได้

ลองดูสองไฟล์ 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
Three isolated environment directories, one shared module, but near-identical boilerplate repeated in every directory
directory-per-environment pattern แก้ปัญหาอะไรที่ Terraform workspace แก้ไม่ได้
สิ่งไหนยังคง DRY (เขียนครั้งเดียว ใช้ซ้ำได้ทุกที่) ใน directory-per-environment pattern
อะไรที่ยังถูก duplicate ข้าม environment directory ใน pattern นี้ ทั้งที่แก้ปัญหา isolation ได้แล้ว
ทำไม environments/dev กับ environments/prod ถึงใช้ AWS account ที่ต่างกันโดยสิ้นเชิงได้อย่างปลอดภัยใน pattern นี้