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

Production Checklist

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

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

  • Remote S3 backend พร้อม locking ตั้งแต่วันแรก use_lockfile = true บน S3 backend ตั้งไว้ก่อน apply ครั้งแรก ไม่ใช่ย้อนกลับมาใส่ทีหลังหลังจาก state file เสียหายจนบังคับให้ต้องทำ คุยไปแล้วใน 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 ที่ไม่คาดคิดโดยเฉพาะ ไม่ใช่อ่านผ่าน ๆ แบบ code diff คุยไปแล้วในบท CI/CD ของ module นี้
  • ค่า sensitive อยู่นอก variable และนอก state ให้มากที่สุดเท่าที่ทำได้ ดึงจาก secrets manager ผ่าน data source แทนที่จะพิมพ์ลงไฟล์ tfvars พร้อม state backend เอง encrypt at rest คุยไปแล้วใน State Management และบท secrets ของ module นี้
  • tflint, security scanner, และ policy-as-code (ถ้าทำได้) wired เข้า CI เพื่อให้ pattern ที่รู้อยู่แล้วว่าไม่ดีและ rule ขององค์กรถูก enforce แบบ mechanical แทนที่จะพึ่งคนคอยจับด้วยตาทุกครั้ง คุยไปแล้วในบท policy-as-code ของ module นี้
  • Terragrunt dependency block ขับเคลื่อนลำดับ rollout ไม่ใช่คนคอยจำว่า VPC ต้อง apply ก่อน database คุยไปแล้วใน Terragrunt Fundamentals
  • terraform test / .tftest.hcl coverage สำหรับ module ที่ทำอะไรไม่ธรรมดา เพื่อให้การ refactor internals ของ module ถูกจับได้ด้วย assertion แทนที่จะไปเจอกับ downstream consumer ที่งงว่าเกิดอะไรขึ้น คุยไปแล้วใน Modules
  • lifecycle.prevent_destroy บน resource ที่แทนที่ไม่ได้จริง ๆ เพื่อให้ config change ที่จะทำลายข้อมูล production fail plan แบบดัง ๆ แทนที่จะสำเร็จแบบเงียบ ๆ พิสูจน์ข้างล่าง
  • precondition block ที่ validate สมมติฐานก่อนจะกลายเป็น resource จับ input ที่ผิดตอน plan time ด้วย message ที่บอกปัญหาจริง แทนที่จะเป็น provider error ลึกลับกลาง apply พิสูจน์ข้างล่าง
  • force-unlock runbook ที่ซ้อมมาแล้ว และนิสัย backup state ที่ทำจริง เพื่อให้ lock ที่ค้างหรือ apply ที่พังมี recovery path ที่ซ้อมมาแล้ว แทนที่จะกลายเป็น incident ที่มีคนต้อง improvise กับ production state เป็นครั้งแรก พิสูจน์ข้างล่าง

สองข้อนี้ — prevent_destroy กับ precondition — เป็น lifecycle block ทั้งคู่ ไม่ใช่สองกลไกแยกกัน แต่คุ้มกันคนละจังหวะ: prevent_destroy บล็อก action เฉพาะ (destroy) แบบไม่มีเงื่อนไข ส่วน precondition บล็อก action ไหนก็ได้ เมื่อสมมติฐานเกี่ยวกับ configuration กลายเป็นเท็จ

prevent_destroy = true ทำให้ Terraform ปฏิเสธ plan ที่จะทำลาย resource นั้น จบ ไม่มี flag ไหน bypass ได้นอกจากลบ lifecycle block ทิ้งเอง พิสูจน์จริง โดยใช้ local_file แทน resource ไหนก็ได้ที่ความผิดพลาดอาจทำลายได้:

main.tf
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 the
plan calls for this resource to be destroyed. To avoid this error and
continue with the plan, either disable lifecycle.prevent_destroy or reduce
the scope of the plan using the -target option.

ปฏิเสธจริง exit code จริงเป็น 1 — terraform destroy (หรือ plan ที่จะ replace resource นี้) ไปต่อไม่ได้เลย 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 plan ไปต่อปกติ) — พิสูจน์ทั้งสองฝั่งของ condition เดียวกัน ไม่ใช่แค่ฝั่ง fail error message บอกค่าที่ผิดจริง ("bogus") กับ rule จริง ซึ่งคือประเด็นทั้งหมด: fail ตอน plan time ที่ reviewer จัดการได้ทันที แทนที่จะเป็น provider-level error ลึกลงไปใน apply

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 รันอยู่):

# real lock file present while the background apply holds it
$ cat .terraform.tfstate.lock.info
{"ID":"564d6b33-65bd-5b30-f952-c2d2a2297461","Operation":"OperationTypeApply", ...}
$ terraform force-unlock -force 00000000-0000-0000-0000-000000000000
Failed to unlock state: LocalState not locked
$ terraform force-unlock -force 564d6b33-65bd-5b30-f952-c2d2a2297461
Failed to unlock state: LocalState not locked

เซอร์ไพรส์จริง: แม้แต่ lock ID ที่ ถูกต้อง กับ lock ที่ active จริง ก็ให้ message LocalState not locked เหมือนกัน นี่ไม่ใช่ bug ของ harness — terraform force-unlock --help อธิบายไว้ตรง ๆ เลยว่าทำไม: "Local state files cannot be unlocked by another process." lock ของ local backend เป็น OS-level file lock (flock) ผูกกับ process ที่ถือมันไว้ ไม่ใช่ named lock ที่ backend เป็นตัวกลางแบบ S3 native locking หรือ DynamoDB lock table เลย process terraform ตัวที่สองที่รัน force-unlock จึงไม่มีอะไรให้ release ได้อย่างมีความหมาย คอร์สนี้พิสูจน์ message จริงของ ID-mismatch จาก S3 native locking ไม่ได้ (ต้องมี S3 bucket จริง) เลยไม่ claim message เฉพาะนั้นที่นี่ — แต่บทเรียนของ runbook เป็นจริงและไม่ผูกกับ provider: force-unlock คือ recovery tool สำหรับ remote backend กับ local state โดยเฉพาะ ทางแก้จริงคือ kill process ที่ค้างอยู่ หรือลบไฟล์ .terraform.tfstate.lock.info ที่ค้างอยู่ตรง ๆ ไม่ใช่ force-unlock เลย

กลไก 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 กังวล (state rm ด้วยมือ, import, การ upgrade provider major version) — snapshot จริงที่มี timestamp ที่ operator ตั้งใจเก็บไว้ แยกต่างหากจากไฟล์ .backup อัตโนมัติของ Terraform เอง the-state-file.mdx พิสูจน์ไปแล้วว่า state push enforce lineage/serial ก่อนจะรับการ restore ซึ่งคือ safety net ที่ทำให้ backup แบบนี้ใช้ได้จริง: push snapshot เก่ากลับไปจะไม่ไปเขียนทับ state ที่ใหม่กว่าแบบเงียบ ๆ ได้เลย

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

infra/
├── root.hcl
├── vpc/
│ └── terragrunt.hcl
└── rds/
└── terragrunt.hcl

root.hcl สำหรับ production มี S3 backend จริงพร้อม locking — รูปแบบที่ remote-backends-and-locking.mdx พิสูจน์จริงไปแล้วกับ S3 bucket จริง:

# Production shape only — needs a real S3 bucket, so it is not collected/run by this lesson's harness proof below.
remote_state {
backend = "s3"
config = {
bucket = "acme-terraform-state"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-east-1"
use_lockfile = true
}
}

proof ของบทนี้เองข้างล่างใช้ local backend แทน ตั้งใจให้ทั้ง tree — root.hcl, ทั้งสอง unit, และทั้งสอง module — validate และ plan ได้แบบ offline เต็มรูปแบบ ไม่ต้องมี S3 bucket เลย:

root.hcl
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 = <<EOF
provider "aws" {
region = "us-east-1"
access_key = "mock"
secret_key = "mock"
skip_credentials_validation = true
skip_requesting_account_id = true
skip_metadata_api_check = true
skip_region_validation = true
}
EOF
}
generate "backend" {
path = "backend.tf"
if_exists = "overwrite"
contents = <<EOF
terraform {
backend "local" {}
}
EOF
}

docs ของ Terragrunt เองพูดชัดว่าทำไม generate "backend" block ควรอยู่ที่ root แทนที่จะเขียนมือไว้ในแต่ละ module: remote_state block ไม่มีผลอะไรเลยถ้าโค้ด Terraform ที่อยู่ข้างใต้ไม่ประกาศ backend block ที่ตรงกัน และ generate คือสิ่งที่เขียน block นั้นเข้าไปในทุก unit อัตโนมัติ เลยทำให้ไฟล์ .tf ของแต่ละ module เองไม่ต้องรู้เลยว่า environment ไหนใช้ backend อะไร

vpc/terragrunt.hcl
include "root" {
path = find_in_parent_folders("root.hcl")
}
terraform {
source = "../modules/vpc"
}
modules/vpc/main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
resource "aws_vpc" "this" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "private" {
vpc_id = aws_vpc.this.id
cidr_block = "10.0.1.0/24"
}
output "vpc_id" {
value = aws_vpc.this.id
}
output "private_subnet_ids" {
value = [aws_subnet.private.id]
}
rds/terragrunt.hcl
include "root" {
path = find_in_parent_folders("root.hcl")
}
terraform {
source = "../modules/rds"
}
dependency "vpc" {
config_path = "../vpc"
mock_outputs = {
vpc_id = "mock-vpc-id"
private_subnet_ids = ["mock-subnet-id"]
}
mock_outputs_allowed_terraform_commands = ["plan"]
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
subnet_id = dependency.vpc.outputs.private_subnet_ids[0]
}
# modules/rds/terraform.tfvars — harness fixture only, so this lesson's raw
# `terraform plan` proof for modules/rds can run standalone offline; a real
# deployment gets these values from Terragrunt's dependency block instead
# (proven below via terragrunt run --all plan, using vpc's real outputs).
vpc_id = "mock-vpc-id"
subnet_id = "mock-subnet-id"
modules/rds/main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
variable "vpc_id" {
type = string
}
variable "subnet_id" {
type = string
}
resource "aws_db_instance" "this" {
identifier = "app-db"
engine = "postgres"
instance_class = "db.t3.micro"
allocated_storage = 20
username = "app"
password_wo = "mock"
password_wo_version = 1
skip_final_snapshot = true
}

terragrunt run --all plan จริงกับ tree ตัวนี้เป๊ะ ๆ:

$ terragrunt run --all plan --non-interactive
INFO The following units will be run, starting with dependencies and then their dependents:
.
╰── vpc
╰── rds
...
[rds] terraform: Plan: 1 to add, 0 to change, 0 to destroy.
❯❯ Run Summary 2 units 20s
────────────────────────────
Succeeded 2

vpc plan ก่อน คำนวณ vpc_id จริง rds plan ทีหลัง ใช้ mock_outputs ของ vpc สำหรับคำสั่ง plan (output จริงของ vpc จะไหลผ่านอัตโนมัติทันทีที่ vpc ถูก apply จริงแล้ว) — ลำดับ DAG ใน log (vpc ก่อน rds) คือ dependency graph ของ Terragrunt ไม่ใช่ความจำของใครเลยว่า unit ไหนต้องมาก่อน ทุกชิ้นตรงนี้โยงกลับไปที่บทเรียนก่อนหน้า: use_lockfile = true ใน production root.hcl ข้างบนคือเรื่อง locking ของ module State Management; root.hcl บวก include คือ pattern DRY backend ของ Terragrunt Fundamentals; dependency block คือสิ่งที่ทำให้ rds reference output จริงของ vpc ได้โดยไม่ต้องมีใครจำว่า unit ไหนต้อง apply ก่อน และ source ../../modules/rds ที่ 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 ใช้ role แบบ OIDC-federated ตัวเดียวกันแทนที่จะเป็น key ที่เก็บไว้ ไม่มีกลไกใหม่อะไรเลยตรงนี้ — เป็นชิ้นส่วนเดียวกันจากทุก module ก่อนหน้า ประกอบกันเป็นระบบเดียวที่ deploy vpc แล้วตามด้วย rds ตามลำดับที่ dependency block บอก โดยมี plan ที่คนอ่านจริงเป็น gate

flowchart TB
  root["root.hcl: shared backend, locking enabled"]
  root --> vpc["vpc unit"]
  root --> rds["rds unit"]
  vpc -->|dependency block: vpc_id, subnet_ids| rds
  pr["Pull request"] -->|terragrunt run --all plan| review["Reviewed for -/+ replacements"]
  review -->|merge to main| apply["terragrunt run --all apply"]
  apply --> vpc
  apply --> rds
root.hcl กับ unit ที่ผูกกันด้วย dependency, deploy ผ่าน CI pipeline ที่ gate plan และ apply

ทุกข้อใน checklist นี้คือ guardrail กันไว้จาก failure mode ที่เจาะจง ระบุชื่อได้ — ไม่ใช่ good practice แบบนามธรรม prevent_destroy มีอยู่เพราะ plan ที่ทำลาย resource ผิดตัวคือ failure mode จริง พิสูจน์ได้ (พิสูจน์ข้างบน) precondition มีอยู่เพราะ typo ตอน plan time ที่ไปโผล่เป็น provider error ลึกลับตอน apply คือเรื่องจริง (พิสูจน์ข้างบน) force-unlock มีอยู่เพราะ lock ที่อยู่นานกว่า process ที่ถือมันคือเรื่องจริง — และการพิสูจน์กับ local backend เผยบทเรียนที่คมกว่าที่คาดไว้: --help text ของ tool เองขีดเส้นชัดเจนว่า force-unlock ทำอะไรได้และทำอะไรไม่ได้ state backup มีอยู่เพราะ “restore state ก่อนหน้า” ต้องเป็น action ที่ซ้อมมาแล้ว ไม่ใช่สิ่งที่คิดสด ๆ ระหว่าง incident ไม่มีข้อไหนเป็นของแต่งเสริมที่ไม่จำเป็น แต่ละข้อปิด gap ที่เจาะจงและพิสูจน์แล้วหนึ่งข้อ

ทางเลือกได้อะไรเสียอะไร
lifecycle.prevent_destroy บน resource สำคัญพิสูจน์ข้างบน: destroy หรือ forced replacement ถูกปฏิเสธตรง ๆ ไม่มี flag ไหน bypass ได้ต้องเอาออกอย่างตั้งใจ (diff จริงที่ review ได้) ก่อน destroy ที่ตั้งใจจริง ๆ จะไปต่อได้ — friction นี้แหละคือจุดประสงค์ทั้งหมด
precondition blockพิสูจน์ข้างบน: input ที่ผิด fail ตอน plan time พร้อม message บอกปัญหาจริงprecondition ทุกตัวคือสิ่งที่ต้องคอยตามให้ทันเมื่อ constraint จริงของ module เปลี่ยนไป
ซ้อม force-unlock กับ local state แทนที่จะสมมติเอาเองพิสูจน์ข้างบน: คอร์สนี้เจอว่า force-unlock ทำอะไรกับ local state จริง ๆ ไม่ได้ ก่อนจะไปเจอ failure นี้ใน productionบทเรียนจริงใช้ได้เต็มที่แค่กับ remote backend (S3 ฯลฯ) — คอร์สนี้ verify message จริงของ ID-mismatch จาก S3 native locking ไม่ได้ถ้าไม่มี bucket จริง
backup แบบ state pull ที่ตั้งใจทำก่อน operation เสี่ยงsnapshot ที่ operator คุมเอง แยกจากไฟล์ .tfstate.backup อัตโนมัติที่มีอยู่เป็นอีกขั้นตอนที่ต้องจำก่อน state rm/import/provider upgrade — ลืมง่ายตอนรีบ ซึ่งเป็นจังหวะที่มันสำคัญที่สุดพอดี
  • “prevent_destroy หยุด change ทุกแบบกับ resource” พิสูจน์ข้างบน: มันบล็อกแค่ destroy โดยเฉพาะ (รวมถึงครึ่ง destroy ของ forced replacement) — in-place update ที่ไม่ต้อง destroy resource ไปต่อได้ปกติ
  • “precondition fail เป็น error แบบเดียวกับที่ provider ปฏิเสธ request” พิสูจน์ข้างบน: precondition fail ตอน plan time ก่อนที่ Terraform จะคุยกับ provider เลยด้วยซ้ำ พร้อม message ที่คนเขียนตั้งใจบอก rule จริง — เป็น failure ที่เร็วกว่าและชัดกว่า provider error ตอน apply มาก
  • “force-unlock คือทางแก้ที่ถูกต้องสำหรับ Terraform run ที่ค้างไหนก็ได้” พิสูจน์ข้างบน พร้อมเซอร์ไพรส์จริง: กับ local state force-unlock release lock ที่ process อื่นถืออยู่ไม่ได้เลย terraform force-unlock --help บอกตรง ๆ ทางแก้จริงสำหรับ local state คือ stop process ที่ค้างหรือลบไฟล์ lock ไม่ใช่ force-unlock
  • “.tfstate.backup อัตโนมัติของ Terraform เป็น backup strategy ที่พอแล้ว” พิสูจน์ข้างบน: มันเก็บแค่ state ก่อนหน้าตัวเดียว ถูกเขียนทับทุกครั้งที่มี operation ถัดไป — snapshot แบบ state pull ที่ตั้งใจทำก่อน operation เสี่ยง คือ backup เดียวที่รอดเกินกว่าหนึ่ง apply

เวลา review Terraform/Terragrunt setup ที่ AI เขียนแล้วบอกว่า “production-ready” ให้เช็ค:

  • resource ที่ stateful (database, KMS key, อะไรก็ตามที่เก็บข้อมูล) ที่ไม่มี lifecycle.prevent_destroy เลย — พิสูจน์ข้างบน นี่คือ guardrail ที่เปลี่ยน accidental destroy ให้เป็น plan ที่ถูกปฏิเสธแบบดัง ๆ แทนที่จะเงียบและร้ายแรง
  • input validation ที่เกิดแค่ใน application code หรือ wiki page ไม่เคยเป็น precondition เลย — พิสูจน์ข้างบน precondition ตอน plan time จับความผิดพลาดได้ก่อนเรียก provider เลย ไม่ใช่หลังจาก fail แบบงง ๆ
  • runbook หรือ comment ที่ claim ว่า terraform force-unlock <id> “ใช้ได้เลย” กับ backend ไหนก็ตามที่ใช้อยู่ โดยไม่แยก local กับ remote state — พิสูจน์ข้างบน claim นั้นผิดสำหรับ local state โดยเฉพาะ และคอร์สนี้ก็ verify claim เกี่ยวกับ mismatch behavior ที่แน่ชัดของ S3 ไม่ได้เหมือนกัน เลยพูดตรง ๆ แบบนั้น
  • dependency block ที่ reference output ของ unit ต้นทางโดยไม่มี mock_outputs/mock_outputs_allowed_terraform_commands — พิสูจน์ข้างบน terragrunt run --all plan ต้องมีสองอย่างนี้ถึงจะ plan unit ปลายทางได้ก่อนที่ unit ต้นทางจะถูก apply จริง
  • module source ที่ pin ไว้เป็นชื่อ branch (ref=main) แทนที่จะเป็น tag หรือ commit SHA — ความเสี่ยง unpinned-dependency แบบเดียวกับที่ checklist นี้ flag ไว้กับ provider version แค่ขยับลงมาหนึ่งชั้น directory

🐛 หา bug ในโค้ดนี้

resource "aws_db_instance" "main" {
identifier = "app-prod"
engine = "postgres"
instance_class = "db.t3.micro"
allocated_storage = 20
skip_final_snapshot = true
}
เฉลย

resource database สำหรับ production ตัวนี้ไม่มี lifecycle { prevent_destroy = true } block เลย พิสูจน์ข้างบน block นั้นคือสิ่งที่เปลี่ยน plan ที่จะทำลาย resource นี้ให้เป็น error ที่ถูกปฏิเสธแบบดัง ๆ — ถ้าไม่มี config change ที่บังคับ replacement (เปลี่ยน identifier, argument ที่ตั้งได้แค่ตอนสร้าง) หรือ terraform destroy ที่พลาดไปโดน workspace ผิดตัว จะไปต่อตามแผนแบบเงียบ ๆ ไม่มี confirmation เพิ่มนอกจาก prompt ปกติของ apply skip_final_snapshot = true ยิ่งทำให้แย่ลง ไม่ใช่ดีขึ้น มันปิด safety net ตัวเดียว (final RDS snapshot) ที่อาจช่วยลดความเสียหายได้ ทางแก้คือ lifecycle block ที่พิสูจน์ข้างบน ไม่มีไว้บน resource ที่ disposable จริง ๆ และตั้งใจมีไว้บน resource ที่ไม่ใช่

ทำไมการ pin module version ไว้เป็น tag เจาะจงถึงสำคัญ แทนที่จะชี้ source ไปที่ branch ที่ลอยได้
ทำไม remote backend พร้อม locking ควร config ตั้งแต่วันแรก แทนที่จะเพิ่มทีหลัง
ทำไม Terragrunt dependency block ถึงสำคัญสำหรับ rollout หลาย unit แทนที่จะพึ่ง engineer apply ตามลำดับที่ถูกต้องด้วยมือ
ทำไมการ review plan ใน CI โดยเฉพาะหา -/+ replacement ถึงสำคัญในฐานะส่วนหนึ่งของ checklist นี้
error จริงที่พิสูจน์แล้วนี้แสดงอะไรเกี่ยวกับ lifecycle.prevent_destroy?
$ terraform destroy -input=false -auto-approve
...
Error: Instance cannot be destroyed

Resource local_file.critical has lifecycle.prevent_destroy set, but the
plan calls for this resource to be destroyed.
นี่คือ lock ID ที่ถูกต้องกับ lock ที่ active จริง แล้วยัง fail แบบนี้ ผลลัพธ์จริงที่พิสูจน์แล้วนี้หมายความว่าอะไรสำหรับ force-unlock runbook?
$ cat .terraform.tfstate.lock.info
{"ID":"564d6b33-...","Operation":"OperationTypeApply", ...}
$ terraform force-unlock -force 564d6b33-...
Failed to unlock state: LocalState not locked