การแยก environment dev/staging/prod เป็นแนวปฏิบัติพื้นฐานของทีม DevOps ที่จริงจังกับ reliability — workshop นี้สาธิตวิธีจัดโครงสร้าง Terraform ให้รองรับหลาย environment โดยไม่ต้อง copy-paste code และยังคงแยก state file ของแต่ละ environment อย่างชัดเจน
จะเปรียบเทียบ 3 approach หลัก ได้แก่ (1) directory-per-environment, (2) workspaces และ (3) Terragrunt-style — แล้วเลือก approach (1) ทำตามทั้ง workshop เพราะเหมาะกับองค์กรทั่วไปที่ต้องการความชัดเจนและ blast radius แคบ
โครงสร้างโปรเจกต์
multi-env-workshop/
├── modules/
│ └── web_server/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── envs/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── backend.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ │ ├── main.tf
│ │ ├── backend.tf
│ │ └── terraform.tfvars
│ └── prod/
│ ├── main.tf
│ ├── backend.tf
│ └── terraform.tfvars
└── README.md
ข้อดีของ layout นี้คือ apply แต่ละ environment ต้อง cd envs/dev && terraform apply ทำให้ลืม apply ผิด environment ได้ยาก state file แยกขาดโดยธรรมชาติ และ code re-use ผ่าน modules/
ขั้นที่ 1 — Reusable Module
# modules/web_server/variables.tf
variable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment ต้องเป็น dev, staging หรือ prod"
}
}
variable "instance_count" {
type = number
default = 1
}
variable "instance_size" {
type = string
default = "s-1vcpu-1gb"
}
variable "region" {
type = string
default = "sgp1"
}
# modules/web_server/main.tf
resource "digitalocean_droplet" "web" {
count = var.instance_count
name = "${var.environment}-web-${count.index + 1}"
region = var.region
size = var.instance_size
image = "ubuntu-22-04-x64"
tags = [
var.environment,
"web",
"managed-by-terraform",
]
}
resource "digitalocean_tag" "env" {
name = var.environment
}
# modules/web_server/outputs.tf
output "droplet_ips" {
value = digitalocean_droplet.web[*].ipv4_address
}
output "droplet_ids" {
value = digitalocean_droplet.web[*].id
}
ขั้นที่ 2 — Environment: dev
# envs/dev/backend.tf
terraform {
required_version = ">= 1.5"
backend "s3" {
bucket = "my-tfstate-bucket"
key = "envs/dev/terraform.tfstate"
region = "ap-southeast-1"
dynamodb_table = "tfstate-lock"
encrypt = true
}
required_providers {
digitalocean = {
source = "digitalocean/digitalocean"
version = "~> 2.0"
}
}
}
# envs/dev/main.tf
provider "digitalocean" {}
module "web" {
source = "../../modules/web_server"
environment = "dev"
instance_count = 1
instance_size = "s-1vcpu-1gb"
region = "sgp1"
}
output "dev_ips" {
value = module.web.droplet_ips
}
# envs/dev/terraform.tfvars
# ค่า override (ถ้ามี)
# module นี้ไม่ต้องมี tfvars เพิ่ม เพราะใช้ default ของ module
ขั้นที่ 3 — Environment: staging
# envs/staging/main.tf
provider "digitalocean" {}
module "web" {
source = "../../modules/web_server"
environment = "staging"
instance_count = 2
instance_size = "s-2vcpu-2gb"
region = "sgp1"
}
backend.tf ของ staging ใช้ key = "envs/staging/terraform.tfstate" คนละ key กับ dev เพื่อแยก state file
ขั้นที่ 4 — Environment: prod
# envs/prod/main.tf
provider "digitalocean" {}
module "web" {
source = "../../modules/web_server"
environment = "prod"
instance_count = 3
instance_size = "s-4vcpu-8gb"
region = "sgp1"
}
resource "digitalocean_loadbalancer" "web" {
name = "prod-web-lb"
region = "sgp1"
forwarding_rule {
entry_port = 443
entry_protocol = "https"
target_port = 80
target_protocol = "http"
certificate_name = "prod-cert"
}
healthcheck {
port = 80
protocol = "http"
path = "/health"
}
droplet_ids = module.web.droplet_ids
}
prod มี resource เพิ่มเติม เช่น load balancer ที่ไม่จำเป็นใน dev/staging — เขียนตรงในแต่ละ environment ไม่ต้องยัดเข้า module เพื่อคง flexibility
ขั้นที่ 5 — Workflow การใช้งาน
# Deploy dev
cd envs/dev
terraform init
terraform plan
terraform apply
# Deploy staging (ต่อจาก dev ผ่าน test)
cd ../staging
terraform init
terraform plan
terraform apply
# Deploy prod (require approval)
cd ../prod
terraform init
terraform plan -out=prod.tfplan
# ให้ senior review prod.tfplan ก่อน apply
terraform apply prod.tfplan
ขั้นที่ 6 — Shared Variables ระหว่าง Environment
สำหรับค่าที่ใช้ทุก environment (เช่น region, organization tags) สามารถสร้างไฟล์ common.tfvars แล้วส่งผ่าน CLI
# envs/_common.tfvars
region = "sgp1"
org_name = "my-company"
cost_center = "engineering"
# Apply:
terraform apply -var-file=../_common.tfvars
ขั้นที่ 7 — CI/CD Integration
# .github/workflows/tf-envs.yml
name: Terraform
on:
pull_request:
paths: ['envs/**', 'modules/**']
jobs:
plan:
strategy:
matrix:
env: [dev, staging, prod]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- name: Plan ${{ matrix.env }}
working-directory: envs/${{ matrix.env }}
run: |
terraform init
terraform plan -out=tfplan
- name: Comment plan
# post plan to PR comment
pipeline pattern นี้รัน plan ของทุก environment ขนานกัน — เห็น impact ของ PR ต่อทุก env พร้อมกันบน review
เปรียบเทียบ 3 Approach
- Directory-per-env (ใน workshop นี้) — เข้าใจง่าย, blast radius แคบ, flexibility สูง แต่ต้องคัดลอก backend config — เหมาะองค์กรทั่วไป
- Terraform Workspaces — ใช้ state เดียวแต่แยก namespace ด้วย workspace — เขียนโค้ดน้อยกว่าแต่เสี่ยงถ้ามี typo ชื่อ workspace จะ apply ผิด env
- Terragrunt / Atlantis — framework ข้าง ๆ สำหรับลด DRY — ลด config ซ้ำได้มากแต่เพิ่ม learning curve ควรใช้เมื่อมีหลาย team
ข้อควรระวัง
- ต้องแยก state file ทุก environment — ห้ามใช้ state เดียวกัน ไม่งั้น apply dev อาจกระทบ prod
- backend S3 bucket ต้องเปิด versioning และ MFA delete เพื่อป้องกัน state หาย
- ห้าม hardcode secret ใน tfvars ของ prod — ใช้ Vault, AWS Secrets Manager หรือ SOPS encrypted file
- ให้ prod apply ผ่าน CI เท่านั้น ห้าม engineer รันจากเครื่องตัวเอง เพื่อให้ audit log ครบ
- version pin ทั้ง module version, provider version — ไม่ให้เด้งเวอร์ชันไปเองเมื่อ prod apply
สรุป
การจัดโครงสร้างหลาย environment ด้วย directory-per-env + shared module ให้ทั้งความชัดเจนและ code reuse ไปพร้อมกัน — เหมาะกับทีมที่ต้องการ onboarding engineer ใหม่ได้เร็ว โดยเห็นจากโครงสร้าง directory ว่ามี env ใดและ module ใดถูก shared บ้าง
เมื่อโปรเจกต์โตถึงระดับหลาย service พร้อมกัน ค่อยพิจารณา Terragrunt หรือ Atlantis เพื่อลด config ที่ซ้ำกันและทำให้ CI เรียบ — แต่สำหรับ workshop และ organization ขนาดเล็ก/กลาง pattern นี้เพียงพอและอ่านง่ายที่สุด

