Production Checklist
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”การส่ง Terraform กับ Terragrunt ขึ้น production บน GCP ไม่ใช่การตัดสินใจใหญ่ครั้งเดียว แต่เป็น checklist ของการตัดสินใจเล็ก ๆ หลายข้อ — แต่ละข้อคุยไปแล้วก่อนหน้านี้ในคอร์ส — ที่ต้องเป็นจริงพร้อมกันทั้งหมด
Checklist
หัวข้อที่มีชื่อว่า “Checklist”ไม่มีข้อไหนเป็นไอเดียใหม่ ณ จุดนี้แล้ว สิ่งที่ใหม่คือการเห็นทุกข้อเรียงกันเป็น list เดียว เพราะความพร้อมสำหรับ production ก็คือทุกข้อเป็นจริงพร้อมกัน ไม่ใช่ข้อใดข้อหนึ่งทำได้ดีเป็นพิเศษ
- Remote GCS backend พร้อม locking ในตัวตั้งแต่วันแรก
backend "gcs"block ตั้งไว้ก่อนapplyครั้งแรก ไม่ใช่ย้อนกลับมาใส่ทีหลังหลังจาก state file เสียหายจนบังคับให้ต้องทำ GCS ไม่ต้องมี lock-table resource แยกต่างหาก — locking เป็นส่วนหนึ่งของ backend เองอยู่แล้ว คุยไปแล้วใน State Management - Provider กับ module version ถูก pin ไว้ ไม่ใช่ลอย
.terraform.lock.hclที่ commit ไว้ pin provider version กับ checksum แน่นอน modulesourcereference ใช้ tag หรือ version constraint เจาะจง ไม่ใช่ชื่อ branch ที่เคลื่อนไหวได้ใต้เท้าเรา คุยไปแล้วใน Foundations กับ Modules planทุกอันถูก review ใน CI ก่อนapplyทุกครั้ง โดย reviewer เฝ้าดู-/+resource replacement ที่ไม่คาดคิดโดยเฉพาะ ไม่ใช่อ่านผ่าน ๆ แบบ code diff คุยไปแล้วในบท CI/CD ของ module นี้- ค่า sensitive อยู่นอก variable และนอก state ให้มากที่สุดเท่าที่ทำได้ ดึงจาก Secret Manager ผ่าน
datasource แทนที่จะพิมพ์ลงไฟล์tfvarsพร้อม IAM จำกัดคนอ่าน state bucket เข้มงวด คุยไปแล้วใน State Management และบท secrets ของ module นี้ tflint, security scanner, และ policy-as-code (ถ้าทำได้) wired เข้า CI เพื่อให้ pattern ที่รู้อยู่แล้วว่าไม่ดีและ rule ขององค์กรถูก enforce แบบ mechanical แทนที่จะพึ่งคนคอยจับด้วยตาทุกครั้ง คุยไปแล้วในบท policy-as-code ของ module นี้- Terragrunt
dependencyblock ขับเคลื่อนลำดับ rollout ไม่ใช่คนคอยจำว่า VPC ต้อง apply ก่อน database คุยไปแล้วใน Terragrunt Fundamentals terraform test/.tftest.hclcoverage สำหรับ module ที่ทำอะไรไม่ธรรมดา เพื่อให้การ refactor internals ของ module ถูกจับได้ด้วย assertion แทนที่จะไปเจอกับ downstream consumer ที่งงว่าเกิดอะไรขึ้น คุยไปแล้วใน Moduleslifecycle.prevent_destroyบน resource ที่แทนที่ไม่ได้จริง ๆ และdeletion_protection/deletion_policyบน GCP resource ที่มีของตัวเอง เพื่อให้ config change ที่จะทำลายข้อมูล production fail plan แบบดัง ๆ แทนที่จะสำเร็จแบบเงียบ ๆ พิสูจน์ข้างล่างpreconditionblock ที่ validate สมมติฐานก่อนจะกลายเป็น resource จับ input ที่ผิดตอน plan time ด้วย message ที่บอกปัญหาจริง แทนที่จะเป็น provider error ลึกลับกลาง apply พิสูจน์ข้างล่างforce-unlockrunbook ที่ซ้อมมาแล้ว และนิสัย backup state ที่ทำจริง เพื่อให้ lock ที่ค้างหรือ apply ที่พังมี recovery path ที่ซ้อมมาแล้ว แทนที่จะกลายเป็น incident ที่มีคนต้อง improvise กับ production state เป็นครั้งแรก พิสูจน์ข้างล่าง
Guardrail ที่ enforce จริง: prevent_destroy, precondition, และ deletion_protection/deletion_policy ของ GCP เอง
หัวข้อที่มีชื่อว่า “Guardrail ที่ enforce จริง: prevent_destroy, precondition, และ deletion_protection/deletion_policy ของ GCP เอง”prevent_destroy = true ทำให้ Terraform ปฏิเสธ plan ที่จะทำลาย resource นั้น จบ ไม่มี flag ไหน bypass ได้นอกจากลบ lifecycle block ทิ้งเอง พิสูจน์จริง โดยใช้ local_file แทน resource ไหนก็ได้ที่ความผิดพลาดอาจทำลายได้ (กลไกเป็นของ Terraform-core เหมือนกันไม่ว่า provider ไหน):
resource "local_file" "critical" { filename = "${path.module}/generated/critical.conf" content = "important=true\n"
lifecycle { prevent_destroy = true }}$ terraform destroy -input=false -auto-approve...Plan: 0 to add, 0 to change, 1 to destroy.
Error: Instance cannot be destroyed
on main.tf line 3: 3: resource "local_file" "critical" {
Resource local_file.critical has lifecycle.prevent_destroy set, but theplan calls for this resource to be destroyed. To avoid this error andcontinue with the plan, either disable lifecycle.prevent_destroy or reducethe scope of the plan using the -target option.ปฏิเสธจริง exit code จริงเป็น 1 precondition คือ guardrail ที่เสริมกัน validate สมมติฐานเกี่ยวกับ input ก่อนที่ Terraform จะ commit เข้า resource เลย:
variable "environment" { type = string default = "prod"}
resource "local_file" "config" { filename = "${path.module}/generated/app.conf" content = "env=${var.environment}\n"
lifecycle { precondition { condition = contains(["dev", "staging", "prod"], var.environment) error_message = "environment must be one of dev, staging, prod — got \"${var.environment}\"." } }}$ terraform plan -var="environment=bogus"...Error: Resource precondition failed
on main.tf line 21, in resource "local_file" "config": 21: condition = contains(["dev", "staging", "prod"], var.environment) ├──────────────── │ var.environment is "bogus"
environment must be one of dev, staging, prod — got "bogus".fail จริง และสำเร็จจริงเมื่อ environment ถูกต้อง (exit 0) — พิสูจน์ทั้งสองฝั่งของ condition เดียวกัน ไม่ใช่แค่ฝั่ง fail
GCP มี guard เฉพาะ resource ซ้อนอยู่บน prevent_destroy อีกชั้น: resource ที่ stateful หลายตัว ship deletion guard ของตัวเอง อยู่ใน provider เลย แยกจาก lifecycle ของ Terraform-core google_sql_database_instance คือตัวอย่างจริงที่มี document ไว้ — ดึงมาตรง ๆ จาก terraform providers schema -json ของคอร์สนี้เองกับ google provider 8.4.0 ไม่ใช่เดาเอา:
$ terraform providers schema -json | jq '.provider_schemas["registry.terraform.io/hashicorp/google"].resource_schemas.google_sql_database_instance.block.attributes.deletion_protection'{ "type": "bool", "description": "Used to block Terraform from deleting a SQL Instance. Defaults to true.", "description_kind": "plain", "optional": true}
$ terraform providers schema -json | jq '.provider_schemas["registry.terraform.io/hashicorp/google"].resource_schemas.google_sql_database_instance.block.attributes.deletion_policy'{ "type": "string", "description": "Whether Terraform will be prevented from destroying the instance. Defaults to \"DELETE\".\nWhen a 'terraform destroy' or 'terraform apply' would delete the instance,\nthe command will fail if this field is set to \"PREVENT\" in Terraform state.\nWhen set to \"ABANDON\", the command will remove the resource from Terraform\nmanagement without updating or deleting the resource in the API.\nWhen set to \"DELETE\", deleting the resource is allowed.\n", "description_kind": "plain", "optional": true, "computed": true}guard จริงสองตัวแยกกันบน resource เดียวกัน: deletion_protection (boolean default เป็น true) กับ deletion_policy (string — DELETE/PREVENT/ABANDON default เป็น DELETE) output จริงของ terraform plan ยืนยันว่า default ทั้งสองมีผลแม้ config จะไม่พูดถึงมันเลย — terraform show -json ของ plan จริงสำหรับ resource นี้:
{ "deletion_protection": true, "deletion_policy": "DELETE" }deletion_protection default เป็น เปิด — google_sql_database_instance ถูก destroy-protect ตั้งแต่แรกเริ่ม ก่อนที่ใครจะเขียน lifecycle block ด้วยซ้ำ deletion_policy = "PREVENT" คือ opt-in ที่เข้มกว่า ชัดเจนกว่า สำหรับ resource ที่ checklist นี้เรียกว่าแทนที่ไม่ได้จริง ๆ ซ้อนอยู่บน deletion_protection ไม่ใช่แทนที่มัน และ "ABANDON" คือตัวเลือกที่สามจริง ๆ ที่ resource คู่กันของ AWS หรือ Azure ไม่มี: มันเอา resource ออกจากการจัดการของ Terraform โดยไม่แตะ GCP resource จริงเลย มีประโยชน์สำหรับการส่งต่อแบบตั้งใจ ไม่ใช่การ destroy
force-unlock runbook — พิสูจน์แล้ว รวมถึงเรื่องเซอร์ไพรส์จริง
หัวข้อที่มีชื่อว่า “force-unlock runbook — พิสูจน์แล้ว รวมถึงเรื่องเซอร์ไพรส์จริง”lock ที่ค้าง (CI job ที่ถูก kill กลาง apply, laptop ที่หลุด network กลางทาง) บล็อก plan/apply ทุกครั้งถัดไปจนกว่าจะมีคนรัน terraform force-unlock runbook จริง ที่ test กับ local backend ของคอร์สนี้ พร้อม lock ที่ active จริง (time_sleep ถือ lock ไว้ 20 วินาทีระหว่างที่ apply แบบ background รันอยู่):
$ cat .terraform.tfstate.lock.info{"ID":"564d6b33-65bd-5b30-f952-c2d2a2297461","Operation":"OperationTypeApply", ...}
$ terraform force-unlock -force 00000000-0000-0000-0000-000000000000Failed to unlock state: LocalState not locked
$ terraform force-unlock -force 564d6b33-65bd-5b30-f952-c2d2a2297461Failed to unlock state: LocalState not lockedเซอร์ไพรส์จริง: แม้แต่ lock ID ที่ ถูกต้อง กับ lock ที่ active จริง ก็ให้ message เหมือนกัน terraform force-unlock --help อธิบายว่าทำไม: "Local state files cannot be unlocked by another process." lock ของ local backend เป็น OS-level file lock ผูกกับ process ที่ถือมันไว้ ไม่ใช่ named lock ที่ backend เป็นตัวกลางแบบ locking ในตัวของ GCS เลย process terraform ตัวที่สองที่รัน force-unlock จึงไม่มีอะไรให้ release ได้อย่างมีความหมาย คอร์สนี้พิสูจน์ message จริงของ ID-mismatch จาก locking ของ GCS เองไม่ได้ (ต้องมี GCS bucket จริง) เลยไม่ claim message เฉพาะนั้นที่นี่ — แต่บทเรียนของ runbook เป็นจริงและไม่ผูกกับ provider: force-unlock คือ recovery tool สำหรับ remote backend กับ local state ทางแก้จริงคือ kill process ที่ค้างอยู่ หรือลบไฟล์ .terraform.tfstate.lock.info ที่ค้างอยู่ตรง ๆ
backup จริง ก่อน operation ที่เสี่ยง — ไม่ใช่หลัง
หัวข้อที่มีชื่อว่า “backup จริง ก่อน operation ที่เสี่ยง — ไม่ใช่หลัง”กลไก backup ที่เป็นจริงมีสองแบบ ควรแยกให้ออก แบบแรกคืออัตโนมัติ: local backend เขียน terraform.tfstate.backup ทุกครั้งที่มี state-changing operation เก็บ state ก่อนหน้า ไว้ — พิสูจน์ด้วยการ apply config change สองครั้งแล้ว diff ไฟล์ทั้งสอง:
$ cat terraform.tfstate.backup | grep content "content": "listen=8080\n",
$ cat terraform.tfstate | grep content "content": "listen=9090\n",แบบที่สองคือตั้งใจทำเอง: terraform state pull > backup-$(date +%Y%m%d-%H%M%S).tfstate ก่อน operation ไหนก็ตามที่ operator กังวล — snapshot จริงที่มี timestamp แยกต่างหากจากไฟล์ .backup อัตโนมัติที่มีอยู่ the-state-file.mdx พิสูจน์ไปแล้วว่า state push enforce lineage/serial ก่อนจะรับการ restore ซึ่งคือ safety net ที่ทำให้ backup แบบนี้ใช้ได้จริง
ระบบเดียวที่สอดคล้องกัน: repo layout จริง
หัวข้อที่มีชื่อว่า “ระบบเดียวที่สอดคล้องกัน: repo layout จริง”เอาทุกอย่างมารวมกันแล้วคอร์สนี้ก็รวมเป็นรูปเดียว เจาะจง — ไม่ใช่ list ของ fact ที่แยกกัน แต่เป็น repository เดียวที่หน้าตาแบบนี้จริง ๆ:
infra/├── root.hcl├── vpc/│ └── terragrunt.hcl└── cloudsql/ └── terragrunt.hclroot.hcl สำหรับ production มี GCS backend จริงพร้อม locking ในตัว — รูปแบบที่ remote-backends-and-locking.mdx พิสูจน์จริงไปแล้ว:
# Production shape only — needs a real GCS bucket, so it is not collected/run by this lesson's harness proof below.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" }}proof ของบทนี้เองข้างล่างใช้ local backend แทน ให้ทั้ง tree validate และ plan ได้แบบ offline เต็มรูปแบบ:
remote_state { backend = "local" config = { path = "${get_parent_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate" }}
generate "provider" { path = "provider.tf" if_exists = "overwrite" contents = <<EOFprovider "google" { project = "acme-prod-123456" region = "us-central1"}EOF}
generate "backend" { path = "backend.tf" if_exists = "overwrite" contents = <<EOFterraform { backend "local" {}}EOF}include "root" { path = find_in_parent_folders("root.hcl")}
terraform { source = "../modules/vpc"}terraform { required_providers { google = { source = "hashicorp/google" version = "~> 8.0" } }}
resource "google_compute_network" "this" { name = "app-vpc" auto_create_subnetworks = false}
resource "google_compute_subnetwork" "private" { name = "app-private" ip_cidr_range = "10.0.1.0/24" region = "us-central1" network = google_compute_network.this.id}
output "network_self_link" { value = google_compute_network.this.self_link}
output "private_subnet_self_links" { value = [google_compute_subnetwork.private.self_link]}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}# modules/cloudsql/terraform.tfvars — harness fixture only, so the raw# `terraform plan` proof for modules/cloudsql can attempt to run standalone;# a real deployment gets this value from Terragrunt's dependency block instead.network_self_link = "mock-network-self-link"terraform { required_providers { google = { source = "hashicorp/google" version = "~> 8.0" } }}
variable "network_self_link" { type = string}
resource "google_sql_database_instance" "this" { name = "app-db" database_version = "POSTGRES_15" region = "us-central1" settings { tier = "db-f1-micro" }}ต่างจาก azurerm (พิสูจน์ในคอร์ส Azure พี่น้องว่าต้องมี session จริงแม้แค่จะ plan การสร้างล้วน ๆ) google proceed ได้ด้วย service-account credential ปลอมแต่ shape ถูก — พฤติกรรมเดียวกับที่คอร์สนี้ใช้ไปแล้วใน policy-as-code-and-static-analysis.mdx และ secrets-and-credential-management.mdx การชี้ provider block ไปที่ไฟล์แบบนั้น (นอกเหนือจาก fence ที่ collect ไว้ของบทนี้ เพราะ JSON key ดิบไม่ใช่ HCL) ทำให้ terragrunt run --all plan แบบ end-to-end จริงเต็มรูปแบบเป็นไปได้:
$ terragrunt run --all plan --non-interactiveINFO The following units will be run, starting with dependencies and then their dependents:.╰── vpc ╰── cloudsql...[cloudsql] terraform: Plan: 1 to add, 0 to change, 0 to destroy.
❯❯ Run Summary 2 units 5s ──────────────────────────── Succeeded 2vpc plan ก่อน คำนวณ network_self_link จริง cloudsql plan ทีหลัง ใช้ mock_outputs ของ vpc สำหรับ plan (output จริงของ vpc จะไหลผ่านอัตโนมัติทันทีที่ vpc ถูก apply จริงแล้ว) ทุกชิ้นตรงนี้โยงกลับไปที่บทเรียนก่อนหน้า: GCS locking ในตัวใน production root.hcl ข้างบนคือเรื่อง locking ของ module State Management; root.hcl บวก include คือ pattern DRY backend ของ 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/ post output ให้ review และ terragrunt run --all apply รันแค่หลัง merge เข้า main authenticate ผ่าน Workload Identity Federation ตัวเดียวกันแทนที่จะเป็น key ที่ดาวน์โหลดมา ไม่มีกลไกใหม่อะไรเลยตรงนี้ — เป็นชิ้นส่วนเดียวกันจากทุก module ก่อนหน้า ประกอบกันเป็นระบบเดียวที่ deploy vpc แล้วตามด้วย cloudsql ตามลำดับที่ dependency block บอก โดยมี plan ที่คนอ่านจริงเป็น gate
flowchart TB root["root.hcl: shared GCS backend, built-in locking"] root --> vpc["vpc unit"] root --> cloudsql["cloudsql unit"] 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 --> cloudsql
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ทุกข้อใน checklist นี้คือ guardrail กันไว้จาก failure mode ที่เจาะจง ระบุชื่อได้ prevent_destroy กับ precondition (พิสูจน์ข้างบนด้วยการปฏิเสธจริง) คุ้มกันจังหวะที่ plan ถูกคำนวณ deletion_protection/deletion_policy ของ GCP เอง (พิสูจน์ข้างบนด้วยการดึง schema จริงและ plan JSON จริง) คือ guard ชั้นที่สองที่ผูกกับ resource เอง — default เป็น เปิด สำหรับ google_sql_database_instance ก่อนที่ใครจะเขียน lifecycle block ด้วยซ้ำ force-unlock (พิสูจน์ข้างบนพร้อมเซอร์ไพรส์จริงบน local state) คุ้มกันจังหวะที่ lock อยู่นานกว่า process ที่ถือมัน state backup คุ้มกันจังหวะที่มีอะไรผิดพลาดไปแล้ว และ DAG ของ dependency block (พิสูจน์ข้างบนด้วย offline proof ที่ครบถ้วนที่สุดในคอร์สนี้ — terragrunt run --all plan จริงที่สำเร็จเต็มรูปแบบ) คุ้มกันจังหวะที่ rollout หลาย unit ต้องเกิดตามลำดับที่ถูกต้องโดยไม่ต้องพึ่งความจำของใครเลย
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ได้อะไร | เสียอะไร |
|---|---|---|
lifecycle.prevent_destroy บน resource สำคัญ | พิสูจน์ข้างบน: destroy หรือ forced replacement ถูกปฏิเสธตรง ๆ | ต้องเอาออกอย่างตั้งใจก่อน destroy ที่ตั้งใจจริง ๆ จะไปต่อได้ |
deletion_protection/deletion_policy แบบ native ของ GCP | พิสูจน์ข้างบน: deletion_protection default เป็น true — destroy-protected ก่อนที่ Terraform-side lifecycle block จะมีอยู่เลย | เฉพาะ resource: พิสูจน์จริงแค่กับ google_sql_database_instance ที่นี่ — resource อื่นอาจไม่มี guard แบบเดียวกัน เลยต้องเช็ค schema ไม่ใช่สมมติเอา |
precondition block | พิสูจน์ข้างบน: input ที่ผิด fail ตอน plan time บอกปัญหาจริง | เป็นอีกสิ่งที่ต้องคอยตามให้ทันเมื่อ constraint จริงของ module เปลี่ยนไป |
พิสูจน์ DAG เต็มรูปแบบผ่าน terragrunt run --all plan จริง (credential ปลอมแต่ shape ถูก) | offline proof ที่ครบถ้วนที่สุดในคอร์สนี้ — สำเร็จแบบ end-to-end จริง ไม่ใช่แค่เช็ค schema/wiring | เป็นไปได้เพราะ google ยอมรับ shape ของ credential file ปลอมสำหรับการสร้างล้วน ๆ เท่านั้น คอร์ส Azure พี่น้องทำแบบเดียวกันไม่ได้ |
backup แบบ state pull ที่ตั้งใจทำก่อน operation เสี่ยง | snapshot ที่ operator คุมเอง แยกจาก .tfstate.backup อัตโนมัติที่มีอยู่ | เป็นอีกขั้นตอนที่ต้องจำก่อน state rm/import/provider upgrade |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
prevent_destroyหยุด change ทุกแบบกับ resource” พิสูจน์ข้างบน: มันบล็อกแค่ destroy โดยเฉพาะ — in-place update ไปต่อได้ปกติ - “GCP resource ต้องมี
lifecycleblock ฝั่ง Terraform ถึงจะ destroy-protected ได้” พิสูจน์ข้างบนด้วยการดึง schema จริง:google_sql_database_instance.deletion_protectiondefault เป็นtrueใน provider เองเลย — protection มีอยู่ก่อนที่lifecycleblock ไหนจะถูกเขียนด้วยซ้ำ - “
force-unlockคือทางแก้ที่ถูกต้องสำหรับ Terraform run ที่ค้างไหนก็ได้” พิสูจน์ข้างบน พร้อมเซอร์ไพรส์จริง: กับlocalstateforce-unlockrelease lock ที่ process อื่นถืออยู่ไม่ได้เลยterraform force-unlock --helpบอกตรง ๆ - “
.tfstate.backupอัตโนมัติของ Terraform เป็น backup strategy ที่พอแล้ว” พิสูจน์ข้างบน: มันเก็บแค่ state ก่อนหน้าตัวเดียว — snapshot แบบstate pullที่ตั้งใจทำ คือ backup เดียวที่รอดเกินกว่าหนึ่ง apply
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เวลา review Terraform/Terragrunt setup บน GCP ที่ AI เขียนแล้วบอกว่า “production-ready” ให้เช็ค:
- resource ที่ stateful (
google_sql_database_instance,google_storage_bucketที่มีข้อมูลจริง, อะไรก็ตามที่เก็บ state) ที่ตั้งdeletion_protectionเป็นfalseหรือdeletion_policyเป็น"ABANDON"/"DELETE"ตรง ๆ โดยไม่มี comment อธิบายว่าทำไม — พิสูจน์ข้างบน default ของ provider เอง (deletion_protection = true) คือตัวที่ปลอดภัย การ override ตรง ๆ ควรมีเหตุผลชัดเจน - input validation ที่เกิดแค่ใน application code ไม่เคยเป็น
preconditionเลย — พิสูจน์ข้างบนpreconditionตอน plan time จับความผิดพลาดได้ก่อนเรียก provider เลย - runbook ที่ claim ว่า
terraform force-unlock <id>“ใช้ได้เลย” กับ backend ไหนก็ตามที่ใช้อยู่ — พิสูจน์ข้างบน claim นั้นผิดสำหรับlocalstate โดยเฉพาะ dependencyblock ที่ reference output ของ unit ต้นทางโดยไม่มีmock_outputs/mock_outputs_allowed_terraform_commands— พิสูจน์ข้างบนterragrunt run --all planต้องมีสองอย่างนี้ถึงจะ plan unit ปลายทางได้ก่อนที่ unit ต้นทางจะถูก apply จริง- module
sourceที่ pin ไว้เป็นชื่อ branch แทนที่จะเป็น tag หรือ commit SHA — ความเสี่ยง unpinned-dependency แบบเดียวกับที่ checklist นี้ flag ไว้กับ provider version
🐛 หา bug ในโค้ดนี้
resource "google_sql_database_instance" "main" {name = "app-db-prod"database_version = "POSTGRES_15"region = "us-central1"
deletion_protection = false
settings { tier = "db-f1-micro"}}เฉลย
deletion_protection = false ปิด guard ตัวเดียวที่ resource นี้ ship มา เปิดไว้ default ตรง ๆ พิสูจน์ข้างบน google_sql_database_instance.deletion_protection default เป็น true ตั้งใจไว้เพื่อบล็อก accidental destroy การตั้งเป็น false บน resource ที่ชื่อ app-db-prod เอา protection นั้นออกไปเลยทั้งหมด โดยไม่มี lifecycle.prevent_destroy เขียนแทนที่ — terraform destroy ครั้งถัดไป หรือ config change ที่บังคับ replacement ไปต่อตามแผนแบบเงียบ ๆ ต่างจาก lifecycle block override นี้เป็นแค่ boolean ตัวเดียวที่มองข้ามตอน review ได้ง่ายมาก ทางแก้คือลบบรรทัดนี้ (เก็บ default จริงไว้) หรือถ้าต้องการ protection ที่เข้มกว่า default deletion_policy = "PREVENT" (พิสูจน์ข้างบนว่าเป็น guard จริงแยกต่างหาก) ซ้อนไว้ด้านบน