Production Checklist
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”Terraform กับ Terragrunt setup ที่พร้อมสำหรับ production บน Azure ไม่ใช่ feature เดียว แต่เป็นผลรวมของทุก practice ที่คอร์สนี้คุยไปแล้ว — remote state, version ที่ pin ไว้, plan ที่ review แล้ว, secret ที่ป้องกันไว้, static analysis กับ policy gate, unit ที่เรียงลำดับด้วย dependency, และ test จริง — ผูกเข้าด้วยกันเป็น repository เดียวที่ขับเคลื่อนด้วย CI
Checklist หนึ่งเหตุผลต่อหนึ่งข้อ
หัวข้อที่มีชื่อว่า “Checklist หนึ่งเหตุผลต่อหนึ่งข้อ”แต่ละข้อข้างล่างโยงกลับไปที่ failure mode จริงที่บทเรียนหนึ่งบทในคอร์สนี้ถูกสร้างขึ้นมาเพื่อแก้:
- remote
azurermbackend พร้อม blob-lease locking ตั้งแต่วันแรก local state แปลว่า disk ของ laptop เครื่องเดียวคือสำเนาความจริงเดียวของ Azure footprint ทั้งหมด และ engineer สองคนรันapplyพร้อมกันทำให้มันเสียหายได้ — เหตุผลทั้งหมดที่ module State Management มีอยู่ - provider version pin ไว้ผ่าน
.terraform.lock.hcl, module version pin เป็น tag หรือ version constraint provider หรือ module ที่ไม่ pin resolve ไป major version ใหม่แบบเงียบ ๆ ได้ตอนinitรันครั้งถัดไป เปลี่ยน behavior ที่ไม่มีใครขอกลางการเปลี่ยนแปลงที่ไม่เกี่ยวกันเลย planreview ใน CI ก่อนapplyทุกครั้ง เฝ้าดู resource replacement ที่ไม่คาดคิดโดยเฉพาะ-/+ใน plan แปลว่า destroy-แล้ว-สร้างใหม่บน Azure resource จริงที่ stateful — review plan แบบ code diff ธรรมดาพลาดบรรทัดที่สำคัญที่สุดพอดี- ค่า sensitive อยู่นอก Terraform variable และ state ในที่ที่มี secrets-manager data source ให้ใช้ พร้อม Azure RBAC จำกัดคนอ่าน state container เข้มงวด
sensitive = trueแค่ redact CLI output การป้องกันจริงคือจำกัด read access ของ backend และลดสิ่งที่ต้องพิมพ์ลง variable ตั้งแต่แรก tflintกับ security scanner และ policy as code ถ้าทำได้ wired เข้า CI static analysis จับ HCL ที่รู้อยู่แล้วว่าไม่ดีในไม่กี่วินาทีก่อนplanจะรันด้วยซ้ำ policy as code (Sentinel หรือ OPA) ประเมิน plan ที่คำนวณจริงเทียบ rule ขององค์กรที่มีความหมายก็ต่อเมื่อมี change จริงอยู่แล้วเท่านั้น- Terragrunt
dependencyblock ขับเคลื่อนลำดับ rollout หลาย unit การแยก infrastructure เป็น unit อิสระ (vnet,vm,database) ปลอดภัยก็ต่อเมื่อ unit ที่ต้องการ output ของอีก unit — subnet ID, resource group name — ประกาศ dependency นั้นตรง ๆ ให้ Terragrunt apply ตามลำดับที่ถูกต้องแทนที่จะพึ่งคนจำ terraform testกับ.tftest.hclcoverage สำหรับ module ที่ทำอะไรไม่ธรรมดาvalidateเช็ค syntax และplanเช็คว่า Azure รับ request แต่ไม่มีตัวไหนยืนยันว่า behavior จริงของ module ถูกต้อง — นั่นคือหน้าที่ของterraform testlifecycle.prevent_destroyบน resource ที่แทนที่ไม่ได้จริง ๆ พิสูจน์ข้างล่างด้วยการปฏิเสธจริง — guardrail ที่เปลี่ยน “config change จะทำลายข้อมูล production” ให้เป็น error ตอน plan time แบบดัง ๆ แทนที่จะ apply สำเร็จแบบเงียบ ๆpreconditionblock ที่ validate สมมติฐานก่อนจะกลายเป็น resource พิสูจน์ข้างล่างทั้งกรณีผ่านจริงและ fail จริง — error ตอน plan time ที่บอก input ที่ผิดจริง แทนที่จะเป็น provider error ลึกลับกลางapplyforce-unlockrunbook ที่ซ้อมมาแล้ว, เข้าใจ blob-lease locking, และนิสัย backup state ที่ทำจริง พิสูจน์ข้างล่าง รวมถึงเซอร์ไพรส์จริงเกี่ยวกับสิ่งที่force-unlockทำได้และทำไม่ได้กับ state คนละแบบ
Guardrail ที่ enforce จริง: prevent_destroy กับ precondition
หัวข้อที่มีชื่อว่า “Guardrail ที่ enforce จริง: prevent_destroy กับ precondition”prevent_destroy กับ precondition เป็น lifecycle sub-block ทั้งคู่ ไม่ใช่กลไกที่ไม่เกี่ยวกัน — แต่คุ้มกันคนละจังหวะ prevent_destroy = true บล็อก action เฉพาะ (destroy) แบบไม่มีเงื่อนไข ส่วน precondition บล็อก action ไหนก็ได้ เมื่อสมมติฐานเกี่ยวกับ configuration กลายเป็นเท็จ พิสูจน์จริง โดยใช้ 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 — ไม่มี flag ไหน bypass ได้นอกจากลบ lifecycle block เอง ซึ่งเป็น diff ที่ review ได้ในตัวมันเอง precondition 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 message บอกค่าที่ผิดจริงกับ rule จริง ซึ่งคือประเด็นทั้งหมด: fail ตอน plan time ที่ reviewer จัดการได้ทันที ไม่ใช่ provider-level error ลึกลงไปใน apply
force-unlock runbook และ blob-lease locking
หัวข้อที่มีชื่อว่า “force-unlock runbook และ blob-lease locking”remote-backends-and-locking.mdx พิสูจน์กลไก locking จริงของ Azure ไปแล้ว: azurerm backend เอา blob lease บน state blob ก่อนจะเขียน เป็น native feature ของ Azure Blob Storage ที่ grant exclusive write access — ไม่มี lock table หรือ resource แยกต่างหากต้อง provision ต่างจาก DynamoDB-based locking แบบเก่าของ S3 บทเดียวกันนั้นก็พิสูจน์ไปแล้วกับ local backend ของคอร์สนี้ ตรงกับ fact ของ runbook ที่ checklist ข้อนี้พูดถึงเป๊ะ ๆ: terraform force-unlock --help บอกตรง ๆ ว่า "Local state files cannot be unlocked by another process." reproduce ที่นี่กับ lock ที่ active จริง (time_sleep ถือ state lock ไว้ 15 วินาทีระหว่างที่ apply แบบ background รันอยู่):
$ cat .terraform.tfstate.lock.info{"ID":"7e6d8bc3-3128-3765-f72c-65d99f8b0afb","Operation":"OperationTypeApply", ...}
$ terraform force-unlock -force 7e6d8bc3-3128-3765-f72c-65d99f8b0afbFailed to unlock state: LocalState not locked
$ terraform force-unlock -force 00000000-0000-0000-0000-000000000000Failed to unlock state: LocalState not lockedแม้แต่ lock ID ที่ ถูกต้อง กับ lock ที่ active จริง ก็ fail เหมือนกัน กับ blob lease จริง force-unlock ทำงานตามที่ remote-backends-and-locking.mdx อธิบายไว้จริง: มันลบ lock ที่ apply ที่ถูก kill ทิ้งไว้ โดยใช้ lock ID ที่ error message ของ Azure เองรายงานเมื่อ run ที่สองขอ lease ไม่ได้ กับ local state กลไกนี้ใช้ไม่ได้เลย — lock ของ local backend เป็น OS-level flock ผูกกับ process ที่ถือมันไว้ ไม่ใช่ named lock ที่ backend เป็นตัวกลาง บทเรียนของ runbook เป็นจริงทั้งสองกรณี: force-unlock ต้องการ lock ที่ค้างจริง ๆ ยืนยันได้อย่างอิสระ (process ที่ถือมันหายไปจริง) ไม่ใช่ reflex สำหรับ “apply ของฉันช้าอยู่”
backup จริง ก่อน operation ที่เสี่ยง — ไม่ใช่หลัง
หัวข้อที่มีชื่อว่า “backup จริง ก่อน operation ที่เสี่ยง — ไม่ใช่หลัง”กลไก backup ที่เป็นจริงมีสองแบบ ควรแยกให้ออก แบบแรกคืออัตโนมัติ: 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 แยกต่างหากจากไฟล์ .backup อัตโนมัติที่มีอยู่ the-state-file.mdx พิสูจน์ไปแล้วว่า state push enforce lineage/serial ก่อนจะรับการ restore ซึ่งคือ safety net ที่ทำให้ backup แบบนี้ใช้ได้จริง
Dependency block: ชิ้นส่วนที่เรียงลำดับ rollout หลาย unit
หัวข้อที่มีชื่อว่า “Dependency block: ชิ้นส่วนที่เรียงลำดับ rollout หลาย unit”Terragrunt dependency block คือสิ่งที่ให้ unit หนึ่ง consume output จริงของอีก unit ได้โดยไม่ต้องรู้ internal implementation ของกันและกันเลย tree สอง unit จริงที่สร้างได้แบบ offline เต็มรูปแบบ — sql ต้องการ subnet ที่ vnet สร้าง:
# root.hcl — local backend, so this whole tree validates/renders fully offlineremote_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 "azurerm" { features {} subscription_id = "00000000-0000-0000-0000-000000000000"}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/vnet"}terraform { required_providers { azurerm = { source = "hashicorp/azurerm" version = "~> 5.0" } }}
resource "azurerm_resource_group" "main" { name = "app-rg" location = "West Europe"}
resource "azurerm_virtual_network" "this" { name = "app-vnet" resource_group_name = azurerm_resource_group.main.name location = azurerm_resource_group.main.location address_space = ["10.0.0.0/16"]}
resource "azurerm_subnet" "private" { name = "private" resource_group_name = azurerm_resource_group.main.name virtual_network_name = azurerm_virtual_network.this.name address_prefixes = ["10.0.1.0/24"]}
output "vnet_id" { value = azurerm_virtual_network.this.id}
output "subnet_id" { value = azurerm_subnet.private.id}
output "resource_group_name" { value = azurerm_resource_group.main.name}include "root" { path = find_in_parent_folders("root.hcl")}
terraform { source = "../modules/sql"}
dependency "vnet" { config_path = "../vnet"
mock_outputs = { subnet_id = "/subscriptions/00000000-0000-0000-0000-000000000000/mock-subnet" resource_group_name = "mock-rg" } mock_outputs_allowed_terraform_commands = ["plan"]}
inputs = { subnet_id = dependency.vnet.outputs.subnet_id resource_group_name = dependency.vnet.outputs.resource_group_name}# modules/sql/terraform.tfvars — harness fixture only, so the raw `terraform# plan` proof for modules/sql can attempt to run standalone; a real deployment# gets these values from Terragrunt's dependency block instead.subnet_id = "mock-subnet-id"resource_group_name = "mock-rg"terraform { required_providers { azurerm = { source = "hashicorp/azurerm" version = "~> 5.0" } }}
variable "subnet_id" { type = string}
variable "resource_group_name" { type = string}
resource "azurerm_postgresql_flexible_server" "main" { name = "app-db" resource_group_name = var.resource_group_name location = "West Europe" version = "15" sku_name = "GP_Standard_D2s_v3" administrator_login = "appadmin"
administrator_password_wo = "mock" administrator_password_wo_version = 1}terragrunt hcl validate กับ terragrunt render จริงกับ tree ตัวนี้เป๊ะ ๆ สำเร็จทั้งคู่แบบ offline (ไม่ต้องมี Azure authentication เลย) terragrunt run --all plan คือคำสั่งเดียวใน chain นี้ที่ต้องมี Azure session จริง — พิสูจน์สม่ำเสมอตลอดทั้งคอร์สนี้ (ci-cd-for-infrastructure.mdx, policy-as-code-and-static-analysis.mdx, และ secrets-and-credential-management.mdx ของบทนี้เอง) และ reproduce ที่นี่กับ tree ตัวนี้เป๊ะ ๆ:
$ terragrunt run --all plan --non-interactive...Error: unable to build authorizer for Resource Manager API: could notconfigure AzureCli Authorizer: could not parse Azure CLI version: launchingAzure CLI: exec: "az": executable file not found in $PATH
with provider["registry.terraform.io/hashicorp/azurerm"], on provider.tf line 2, in provider "azurerm": 2: provider "azurerm" {นี่คือเพดานที่ซื่อสัตย์ที่สุดสำหรับ azurerm ใน environment แบบ offline ของคอร์สนี้: การเชื่อมต่อ DAG (dependency, mock_outputs, include/generate) พิสูจน์จริงและ offline แล้ว ส่วน plan จริงกับ Azure resource จริงต้องมี Azure credential จริงที่คอร์สนี้ไม่มี ข้อจำกัดเดียวกับที่ azurerm เจอทุกที่ในคอร์สนี้ Terragrunt ยังอ่าน dependency block และหาลำดับ apply ที่ถูกต้องเองได้ทันทีที่มี credential จริง: terragrunt run --all apply apply vnet ก่อน อ่าน output subnet_id จริงเมื่อมันมีอยู่แล้ว แล้วค่อยรัน sql ด้วยค่าจริงนั้นเป็น input ไม่มีใครต้อง hardcode ลำดับหรือจำว่าต้อง apply unit ไหนก่อนด้วยมือเลย mock_outputs block สำคัญเฉพาะกับ plan: มันทำให้ plan ของ sql รันได้สะอาดแม้ vnet จะยังไม่เคยถูก apply เลย โดยใช้ placeholder value แทนที่จะ fail เพราะ output จริงยังไม่มีอยู่
ภาพรวมแบบ end-to-end
หัวข้อที่มีชื่อว่า “ภาพรวมแบบ end-to-end”เอาทุกข้อมารวมกันแล้ว production Terragrunt repository บน Azure จะหน้าตาแบบนี้: root.hcl หนึ่งตัวที่ด้านบนประกาศ remote_state backend ที่แชร์กันกับ provider generate block, unit directory จำนวนหนึ่งข้างล่างมัน — vnet/, vm/ (หรือ aks/), และ sql/ คือชุดที่สมจริงและน้อยที่สุด — แต่ละอันมี terragrunt.hcl ของตัวเอง wire dependency block ที่ต้องการจริง ๆ และ CI pipeline ที่ authenticate ผ่าน OIDC federated credential รัน terragrunt run --all plan ทุก pull request และ terragrunt run --all apply แค่หลัง merge เข้า main
name: terraform
on: pull_request: paths: - 'infra/**' push: branches: - main paths: - 'infra/**'
permissions: id-token: write contents: read pull-requests: write
jobs: static-analysis: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: terraform-linters/setup-tflint@v4 - run: tflint --init && tflint --recursive - name: Run checkov with: directory: infra
plan: needs: static-analysis if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: azure/login@v3 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - uses: hashicorp/setup-terraform@v4 - run: terragrunt run --all plan -no-color | tee plan.txt - run: gh pr comment ${{ github.event.pull_request.number }} --body-file plan.txt env: GH_TOKEN: ${{ github.token }}
apply: needs: static-analysis if: github.event_name == 'push' runs-on: ubuntu-latest environment: production steps: - uses: actions/checkout@v7 - uses: azure/login@v3 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - uses: hashicorp/setup-terraform@v4 - run: terragrunt run --all apply --non-interactiveไม่มีอะไรใหม่เลยใน pipeline นั้น ณ จุดนี้ของคอร์ส — ทุก stage คือบทเรียนที่ทำมาแล้ว ประกอบกันเป็น repository เดียว: vnet, vm/aks, และ sql เป็น unit อิสระใต้ root.hcl เดียวกัน dependency block ให้ลำดับจริงระหว่างกันกับ Terragrunt tflint กับ checkov fail เร็วก่อนจะพยายาม plan ด้วยซ้ำ OIDC แทนที่ client secret ที่เก็บไว้ plan ที่ review แล้ว gate ทุก apply และ apply เองสงวนไว้แค่หลัง merge หลัง approval gate รันโดย pipeline ไม่ใช่ laptop
flowchart TB root["root.hcl (remote_state + provider generate)"] --> vnet["vnet unit"] root --> sql["sql unit"] vnet -->|dependency block: subnet_id, resource_group_name| sql pr["Pull request"] -->|OIDC auth| planj["terragrunt run --all plan"] planj -->|reviewed and merged| applyj["terragrunt run --all apply"] vnet -.-> planj sql -.-> planj
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ทุกข้อใน checklist นี้คือ guardrail กันไว้จาก failure mode ที่เจาะจง ระบุชื่อได้ prevent_destroy กับ precondition (พิสูจน์ข้างบนด้วยการปฏิเสธจริง) คุ้มกันจังหวะที่ plan ถูกคำนวณ Blob-lease locking กับ force-unlock (พิสูจน์ข้างบน cross-reference จาก proof จริงของ remote-backends-and-locking.mdx เอง) คุ้มกันจังหวะที่ writer สองตัวแข่งกัน และจังหวะที่ lock อยู่นานกว่า process ที่ถือมัน state backup คุ้มกันจังหวะที่มีอะไรผิดพลาดไปแล้วและ “restore state ก่อนหน้า” ต้องเป็น action ที่ซ้อมมาแล้ว และ DAG ของ dependency block (พิสูจน์ข้างบนแบบ offline ผ่าน hcl validate/render จริง พร้อมเพดาน Azure-auth ที่ระบุตรง ๆ) คุ้มกันจังหวะที่ rollout หลาย unit ต้องเกิดตามลำดับที่ถูกต้องโดยไม่ต้องพึ่งความจำของใครเลย ไม่มีข้อไหนเป็นของแต่งเสริม แต่ละข้อปิด gap ที่เจาะจงและพิสูจน์แล้วหนึ่งข้อ และบทนี้บอกตรง ๆ ว่า offline proof สำหรับ azurerm จบตรงไหนจริง ๆ
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ได้อะไร | เสียอะไร |
|---|---|---|
lifecycle.prevent_destroy บน resource สำคัญ | พิสูจน์ข้างบน: destroy หรือ forced replacement ถูกปฏิเสธตรง ๆ | ต้องเอาออกอย่างตั้งใจ (diff ที่ review ได้) ก่อน destroy ที่ตั้งใจจริง ๆ จะไปต่อได้ |
precondition block | พิสูจน์ข้างบน: input ที่ผิด fail ตอน plan time บอกปัญหาจริง | เป็นอีกสิ่งที่ต้องคอยตามให้ทันเมื่อ constraint จริงของ module เปลี่ยนไป |
Blob-lease locking (built-in ใน azurerm backend) | พิสูจน์ใน remote-backends-and-locking.mdx: ไม่ต้อง provision resource เพิ่มเลย ต่างจาก lock table แยก | ไม่มี resource สำหรับ audit trail แยกต่างหากแบบที่ lock table เฉพาะอาจมี access log ของตัวเอง |
backup แบบ state pull ที่ตั้งใจทำก่อน operation เสี่ยง | snapshot ที่ operator คุมเอง แยกจาก .tfstate.backup อัตโนมัติที่มีอยู่ | เป็นอีกขั้นตอนที่ต้องจำก่อน state rm/import/provider upgrade |
พิสูจน์ DAG ของ dependency block ผ่าน hcl validate/render แทน live run --all plan | พิสูจน์ได้จริงแบบ offline สำหรับ azurerm เพราะ plan เองทำไม่ได้ | ไม่ได้ทดสอบการคำนวณ plan จริง — แค่การเชื่อมต่อ ไม่ใช่สิ่งที่ plan จริงจะแสดง |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
prevent_destroyหยุด change ทุกแบบกับ resource” พิสูจน์ข้างบน: มันบล็อกแค่ destroy โดยเฉพาะ — in-place update ที่ไม่ต้อง destroy resource ไปต่อได้ปกติ - “Azure state locking ต้องมี lock resource แยกต่างหาก แบบที่ S3 ใช้ DynamoDB” พิสูจน์ใน
remote-backends-and-locking.mdx:azurermbackend lock ด้วยการเอา blob lease ตรงบน state blob เลย ไม่มี resource แยกต้อง provision - “
force-unlockคือทางแก้ที่ถูกต้องสำหรับ Terraform run ที่ค้างไหนก็ได้” พิสูจน์ข้างบนและในremote-backends-and-locking.mdx: กับlocalstateforce-unlockrelease lock ที่ process อื่นถืออยู่ไม่ได้เลยterraform force-unlock --helpบอกตรง ๆ กับ blob lease จริง มันคือ recovery tool ที่มี document ไว้ แต่แค่สำหรับ lock ที่ยืนยันได้ว่าค้างจริง ๆ เท่านั้น - “
bridgecrewio/checkov-action@masterก็โอเคตราบใดที่ action นั้น trust ได้” พิสูจน์ในpolicy-as-code-and-static-analysis.mdx: trust ได้วันนี้ไม่ได้บอกอะไรเลยว่าพรุ่งนี้อะไรจะอยู่บนmaster— pin ข้างบนเป็น tag จริงปัจจุบัน (v12.1347.0) แทน
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เวลา review Terraform/Terragrunt setup บน Azure ที่ AI เขียนแล้วบอกว่า “production-ready” ให้เช็ค:
- resource ที่ stateful (database, Key Vault, อะไรก็ตามที่เก็บข้อมูล) ที่ไม่มี
lifecycle.prevent_destroyเลย — พิสูจน์ข้างบน นี่คือ guardrail ที่เปลี่ยน accidental destroy ให้เป็น plan ที่ถูกปฏิเสธแบบดัง ๆ แทนที่จะเงียบและร้ายแรง - runbook หรือ comment ที่ claim ว่า
terraform force-unlock <id>“ใช้ได้เลย” กับ backend ไหนก็ตามที่ใช้อยู่ โดยไม่แยก local state กับ blob lease จริง — พิสูจน์ข้างบน claim นั้นผิดสำหรับlocalstate โดยเฉพาะ uses: bridgecrewio/checkov-action@master(หรือ action ไหนก็ตามที่ pin ด้วยชื่อ branch) แทนที่จะเป็น release tag จริง — พิสูจน์ข้างบนและในpolicy-as-code-and-static-analysis.mdxความเสี่ยงแบบ reproducibility กับ supply-chain จริง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 แค่ขยับลงมาหนึ่งชั้น directory
🐛 หา bug ในโค้ดนี้
resource "azurerm_postgresql_flexible_server" "main" {name = "app-db-prod"resource_group_name = "app-rg"location = "West Europe"version = "15"sku_name = "GP_Standard_D2s_v3"administrator_login = "appadmin"
administrator_password_wo = var.db_passwordadministrator_password_wo_version = 1}เฉลย
resource database สำหรับ production ตัวนี้ไม่มี lifecycle { prevent_destroy = true } block เลย พิสูจน์ข้างบน block นั้นคือสิ่งที่เปลี่ยน plan ที่จะทำลาย resource นี้ให้เป็น error ที่ถูกปฏิเสธแบบดัง ๆ — ถ้าไม่มี config change ที่บังคับ replacement หรือ terraform destroy ที่พลาดไปโดน workspace ผิดตัว จะไปต่อตามแผนแบบเงียบ ๆ ไม่มี confirmation เพิ่มนอกจาก prompt ปกติของ apply การจัดการ password แบบ write-only (administrator_password_wo ที่ใช้ถูกต้องแล้วตรงนี้) ปกป้อง ค่า secret มันไม่ได้ปกป้องอะไรเลยเกี่ยวกับ ตัว resource เอง ที่ถูกทำลาย — เป็น guardrail สองอันแยกกันจากสองบทเรียนแยกกัน มีอันหนึ่งไม่ได้แปลว่ามีอีกอันด้วย ทางแก้คือ lifecycle block ที่พิสูจน์ข้างบน ตั้งใจมีไว้บน resource ที่แทนที่ไม่ได้จริง ๆ