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

Production Checklist

การ ship Terraform กับ Terragrunt ขึ้น production บน GCP ไม่ใช่การตัดสินใจก้อนใหญ่ก้อนเดียว แต่เป็น checklist ของการตัดสินใจเล็ก ๆ หลายข้อ — แต่ละข้อครอบคลุมไปแล้วก่อนหน้านี้ในคอร์ส — ที่ต้องเป็นจริงพร้อมกันทั้งหมด

ไม่มีข้อไหนในนี้เป็นไอเดียใหม่เลยตอนนี้ สิ่งที่ใหม่คือการเห็นทั้งหมดเรียงกันเป็นลิสต์เดียว เพราะความพร้อมสำหรับ production คือทุกข้อเป็นจริงพร้อม ๆ กัน ไม่ใช่ข้อใดข้อหนึ่งทำได้ดีเป็นพิเศษ

  • remote GCS backend ที่มี locking ในตัวตั้งไว้ตั้งแต่วันแรก บล็อก backend "gcs" ตั้งไว้ก่อน apply ครั้งแรก ไม่ใช่มาเพิ่มทีหลังหลังจาก state file เสียหายจนต้องแก้ปัญหาแบบเร่งด่วน GCS ไม่ต้องมี lock-table resource แยกต่างหาก locking เป็นส่วนหนึ่งของ backend อยู่แล้ว ครอบคลุมไว้ใน State Management
  • provider กับ module version ต้อง pin ไว้ ไม่ปล่อยลอย .terraform.lock.hcl ที่ commit ไว้ pin provider version กับ checksum ไว้แน่นอน module source reference ใช้ tag หรือ version constraint ที่ระบุไว้ชัด ไม่ใช่ชื่อ branch ที่เลื่อนไปเรื่อย ๆ ใต้เท้าคุณ ครอบคลุมไว้ใน Foundations กับ Modules
  • ทุก plan ถูก review ใน CI ก่อนทุก apply โดย reviewer เฝ้าดู -/+ resource replacement ที่ไม่คาดคิดโดยเฉพาะ ไม่ใช่แค่สแกนผ่าน plan แบบ code diff ครอบคลุมไว้ในบทเรียน CI/CD ของ module นี้
  • ค่า sensitive ต้องไม่อยู่ใน variable และเท่าที่ทำได้ต้องไม่อยู่ใน state ดึงจาก Secret Manager ผ่าน data source แทนที่จะพิมพ์ใส่ไฟล์ tfvars โดยมี IAM จำกัดสิทธิ์อย่างแน่นหนาว่าใครอ่าน state bucket ได้บ้าง ครอบคลุมไว้ใน State Management กับบทเรียน secret ของ module นี้
  • tflint, security scanner, และถ้าเป็นไปได้ policy-as-code ต่อเข้ากับ CI เพื่อให้ pattern ที่รู้อยู่แล้วว่าแย่และกฎขององค์กรถูกบังคับใช้แบบเชิงกลไก ไม่ต้องพึ่งคนคอยจับทุกเคสด้วยตา ครอบคลุมไว้ในบทเรียน policy-as-code ของ module นี้
  • dependency block ของ Terragrunt เป็นตัวขับลำดับการ rollout ไม่ใช่คนคอยจำเองว่า VPC ต้อง apply ก่อน database ครอบคลุมไว้ใน Terragrunt Fundamentals
  • terraform test / .tftest.hcl ครอบคลุม module ไหนที่ทำอะไรที่ไม่ trivial เพื่อให้การ refactor ภายใน module ถูกจับได้ด้วย assertion แทนที่จะให้ downstream consumer มาเจอเอาเองแบบไม่ทันตั้งตัว ครอบคลุมไว้ใน Modules
# gke/terragrunt.hcl and cloudsql/terragrunt.hcl both use dependency blocks
# so rollout order is driven by Terragrunt, not a person's memory.
dependency "vpc" {
config_path = "../vpc"
mock_outputs = {
network_self_link = "mock-network-self-link"
private_subnet_self_links = ["mock-subnet-self-link"]
}
mock_outputs_allowed_terraform_commands = ["plan"]
}
inputs = {
network_self_link = dependency.vpc.outputs.network_self_link
subnetwork = dependency.vpc.outputs.private_subnet_self_links[0]
}
modules/cloudsql/tests/main.tftest.hcl
run "sets_expected_database_version" {
command = plan
variables {
instance_name = "app-db-test"
}
assert {
condition = google_sql_database_instance.this.database_version == "POSTGRES_15"
error_message = "Cloud SQL module did not default to the POSTGRES_15 engine"
}
}

เอาทุกอย่างมารวมกันแล้วคอร์สนี้ก็รวมเป็นรูปทรงเดียวที่ชัดเจน ไม่ใช่ลิสต์ข้อเท็จจริงที่แยกกันเดี่ยว ๆ แต่เป็น repo หนึ่งอันที่หน้าตาแบบนี้จริง ๆ

infra/
├── root.hcl
├── vpc/
│ └── terragrunt.hcl
├── gke/
│ └── terragrunt.hcl
└── cloudsql/
└── terragrunt.hcl

root.hcl อยู่บนสุดถือ configuration ของ backend และการ generate provider ที่ใช้ร่วมกัน ถูก include โดยทุก unit ข้างล่าง

infra/root.hcl
remote_state {
backend = "gcs"
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
config = {
bucket = "acme-terraform-state"
prefix = "${path_relative_to_include()}/terraform.tfstate"
project = "acme-prod-123456"
location = "US"
}
}
infra/cloudsql/terragrunt.hcl
include "root" {
path = find_in_parent_folders("root.hcl")
}
terraform {
source = "../../modules/cloudsql"
}
dependency "vpc" {
config_path = "../vpc"
mock_outputs = {
network_self_link = "mock-network-self-link"
private_subnet_self_links = ["mock-subnet-self-link"]
}
mock_outputs_allowed_terraform_commands = ["plan"]
}
inputs = {
network_self_link = dependency.vpc.outputs.network_self_link
subnetwork = dependency.vpc.outputs.private_subnet_self_links[0]
}

ทุกชิ้นส่วนตรงนี้ย้อนกลับไปหาบทเรียนก่อนหน้าได้หมด GCS locking ในตัวคือเรื่อง locking ของ module State Management, root.hcl คู่กับ include คือ DRY backend pattern ของ Terragrunt Fundamentals, dependency block คือสิ่งที่ให้ cloudsql reference output จริงของ vpc ได้โดยไม่ต้องมีคนจำว่า unit ไหนต้อง apply ก่อน และ source ../../modules/cloudsql ที่ pin ไว้กับ tag ใน repo จริง ไม่ปล่อยลอย คือวินัยเรื่อง versioning ของ Modules

ทั้ง tree นี้ถูก ship ผ่าน CI/CD shape เดียวกับที่ผ่านมาใน module นี้เป๊ะ terragrunt run --all plan รันทุก pull request ที่แตะ infra/ โพสต์ output ให้ review และ terragrunt run --all apply รันแค่หลัง merge เข้า main authenticate ผ่าน Workload Identity Federation ตัวเดียวกัน ไม่ใช่ key ที่ดาวน์โหลดมา ไม่มีอะไรตรงนี้เป็นกลไกใหม่เลย ทั้งหมดคือชิ้นส่วนเดียวกันจากทุก module ก่อนหน้า เอามาต่อกันเป็นระบบเดียวที่ deploy vpc แล้วตามด้วย gke กับ cloudsql ตามลำดับที่ dependency block บอกไว้ gate ด้วย plan ที่มีคนอ่านจริง ๆ

flowchart TB
  root["root.hcl: shared GCS backend, built-in locking"]
  root --> vpc["vpc unit"]
  root --> gke["gke unit"]
  root --> cloudsql["cloudsql unit"]
  vpc -->|dependency block: network_self_link, subnet links| gke
  vpc -->|dependency block: network_self_link, subnet links| cloudsql
  pr["Pull request"] -->|terragrunt run --all plan| review["Reviewed for -/+ replacements"]
  review -->|merge to main| apply["terragrunt run --all apply"]
  apply --> vpc
  apply --> gke
  apply --> cloudsql
root.hcl บวกกับสาม unit ที่พึ่งพากัน ship ผ่าน CI pipeline ที่ gate ทั้ง plan และ apply
ทำไมการ pin module version ไว้กับ tag ที่ระบุถึงสำคัญ แทนที่จะชี้ source ไปที่ branch ที่ลอยไปเรื่อย ๆ
ทำไม remote GCS backend ที่มี locking ในตัวควรตั้งไว้ตั้งแต่วันแรก แทนที่จะมาเพิ่มทีหลัง
ทำไม dependency block ของ Terragrunt ถึงสำคัญสำหรับการ rollout แบบหลาย unit แทนที่จะพึ่ง engineer apply unit ตามลำดับที่ถูกต้องด้วยมือ
ทำไมการ review plan ใน CI เพื่อหา -/+ replacement โดยเฉพาะถึงสำคัญในฐานะส่วนหนึ่งของ checklist นี้