AI Bug Catalog
ทุก snippet ด้านล่างเขียนให้ดูเหมือน HCL ที่ผู้ช่วย AI สร้างขึ้นจริงๆ — ลื่นไหล น่าเชื่อถือ ตาม training data ที่ยังจำ dynamodb_table แบบยุค Terraform 0.11 หรือความเคยชินกับ count แบบก่อนมี for_each — แล้วรันจริง: terraform validate, terraform plan (ด้วย mock AWS credentials ที่ harness ของคอร์สนี้ใช้ทุกที่ — access_key = "mock", skip_credentials_validation = true, ไม่เรียก AWS จริงเลย), terraform test, terragrunt hcl validate, terragrunt run --all plan, และการรัน docker run จริงของ tflint/checkov/conftest output ด้านล่าง copy มาจากการรันจริงๆ ไม่ได้แต่งขึ้นจากความจำ สังเกตว่ามีกี่ตัวที่ terraform validate/plan ปฏิเสธตรงๆ — เป็นส่วนน้อย ส่วนใหญ่ของ 23 pattern นี้คือ Terraform ที่ schema valid, plan สะอาด แต่พังทีหลัง: ตอน apply จริงกับ AWS จริง, ตอน plan ครั้งถัดไปหลังมีคนแก้ list ที่ไม่เกี่ยวข้อง, หรือไม่พังเลยแต่ทำสิ่งผิดอย่างเงียบๆ ตลอดไป 23 pattern นี้จัดกลุ่มตามลำดับเดียวกับ วิธีอ่าน Terraform ที่ไม่ได้เขียนเอง และ Review Checklist
Versions & providers
หัวข้อที่มีชื่อว่า “Versions & providers”1. Provider constraint ที่ลอยตัว (>= แทนที่จะเป็น ~>)
หัวข้อที่มีชื่อว่า “1. Provider constraint ที่ลอยตัว (>= แทนที่จะเป็น ~>)”terraform { required_providers { azurerm = { source = "hashicorp/azurerm", version = ">= 5.0" } }}อาการ — .terraform.lock.hcl จริงหลัง terraform init:
provider "registry.terraform.io/hashicorp/azurerm" { version = "5.7.0" constraints = ">= 5.0.0" hashes = [ ... ]}>= 5.0 resolve ไปที่ release ใหม่สุดที่ registry มีวันนี้ — 5.7.0 ถ้า .terraform.lock.hcl ไม่ถูก commit (ดู entry 4) หรือมีใครรัน init -upgrade การ init ครั้งถัดไปอาจ resolve release ใหม่สุดที่ต่างออกไป เปลี่ยนพฤติกรรม provider อย่างเงียบๆ ระหว่างการรัน main.tf ไฟล์เดียวกันสองครั้ง ไม่มีอะไรใน >= 5.0 ที่สัญญาว่าการ resolve ครั้งถัดไปจะอยู่ในขอบเขตการรับประกันของ major version เดียวกันแบบที่ ~> 5.0 ให้ได้
วิธีแก้:
terraform { required_providers { azurerm = { source = "hashicorp/azurerm", version = "~> 5.0" } }}จับได้โดย: ไม่มีอะไรจับอัตโนมัติเลย — terraform init/validate/plan ผ่านทั้งคู่ไม่ว่าจะใช้ constraint แบบไหน มีแค่การ review diff หรือ policy ที่บังคับ pin version เท่านั้นที่จับได้ ดู Verification Tools
2. Argument ที่ deprecated แล้วแต่ AI ยังเขียนจากความเคยชิน
หัวข้อที่มีชื่อว่า “2. Argument ที่ deprecated แล้วแต่ AI ยังเขียนจากความเคยชิน”Warning การ deprecate จริงที่ดึงมาจาก schema ของ provider hashicorp/azurerm 5.7.0 ที่ติดตั้งจริง (terraform providers schema -json) ไม่ได้เดาเอา:
resource "azurerm_resource_group" "this" { name = "acme-rg" location = "eastus"}
resource "azurerm_storage_account" "logs" { name = "acmelogsstorage" resource_group_name = azurerm_resource_group.this.name location = azurerm_resource_group.this.location account_tier = "Standard" account_replication_type = "LRS" public_network_access_enabled = true}อาการ — terraform validate -json จริง:
warning - Argument is deprecated - `public_network_access_enabled` has been deprecated in favour of `public_network_access` and will be removed in v6.0 of the AzureRM providerยัง validate ผ่านสำเร็จ — เป็นแค่ warning ไม่ใช่ error ดังนั้น CI ที่เช็คแค่ exit code (ดู entry 18) จะปล่อยผ่านไปเฉยๆ
วิธีแก้:
resource "azurerm_resource_group" "this" { name = "acme-rg" location = "eastus"}
resource "azurerm_storage_account" "logs" { name = "acmelogsstorage" resource_group_name = azurerm_resource_group.this.name location = azurerm_resource_group.this.location account_tier = "Standard" account_replication_type = "LRS" public_network_access = "Enabled"}จับได้โดย: terraform validate -json | jq '.diagnostics[] | select(.severity=="warning")' — grep หา warning ใน CI ไม่ใช่แค่ exit code เริ่มต้น
3. provider block ข้างใน module ที่ reusable
หัวข้อที่มีชื่อว่า “3. provider block ข้างใน module ที่ reusable”provider "azurerm" { features {} subscription_id = "00000000-0000-0000-0000-000000000000"}
resource "azurerm_resource_group" "this" { name = "acme-rg" location = "eastus"}# entries/03-provider-in-module/main.tf — @expect-error Module is incompatible with count, for_each, and depends_onmodule "rg" { source = "./modules/rg" count = 2}ตัวนี้ resolve ผ่านสบายๆ จนกว่าผู้เรียกใช้จะพยายามใช้ count/for_each/depends_on บน module call ซึ่งเป็นสิ่งที่ environment ที่สองมักต้องการพอดี
อาการ — terraform init จริง, Terraform 1.16.4:
Upgrading modules...- rg in modules/rg
Error: Module is incompatible with count, for_each, and depends_on
on main.tf line 3, in module "rg": 3: count = 2
The module at module.rg is a legacy module which contains its own localprovider configurations, and so calls to it may not use the count,for_each, or depends_on arguments.
If you also control the module "./modules/rg", consider updating thismodule to instead expect provider configurations to be passed by itscaller.วิธีแก้:
resource "azurerm_resource_group" "this" { name = "acme-rg" location = "eastus"}provider "azurerm" { features {} subscription_id = "00000000-0000-0000-0000-000000000000"}
module "rg" { source = "./modules/rg" count = 2}จับได้โดย: terraform init/validate — แต่เฉพาะตอนที่มีคนเพิ่ม count/for_each/depends_on จริงๆ ซึ่งมักช้ากว่าตอนที่ module ถูกเขียนและ review ผ่านครั้งแรกไปทั้ง environment หนึ่ง
State & backend
หัวข้อที่มีชื่อว่า “State & backend”4. .terraform.lock.hcl ถูก gitignore
หัวข้อที่มีชื่อว่า “4. .terraform.lock.hcl ถูก gitignore”.gitignore ที่ copy มาจาก template “Terraform” ทั่วไปมักรวม .terraform.lock.hcl เข้าไปพร้อมกับ .terraform/ — แต่มีแค่อันเดียวในสองอันนี้เท่านั้นที่ควรถูก ignore
.terraform/.terraform.lock.hcl*.tfstate*.tfstate.backupอาการ — reproduce จริงในเครื่อง: ลบ .terraform.lock.hcl แล้วรัน terraform init ใหม่ด้วย constraint ที่ลอยตัวจาก entry 1 (>= 6.0):
$ rm .terraform.lock.hcl && terraform init -backend=falseInitializing provider plugins...- Finding hashicorp/aws versions matching ">= 6.0.0"...- Installing hashicorp/aws v6.66.0...ไม่มีอะไรเตือนว่า version ที่ resolve อาจต่างจากที่เพื่อนร่วมทีม — หรือ CI ในวันอื่น — resolve ได้ lock file มีไว้เพื่อการันตีว่าการ resolve นี้ reproduce ได้ข้ามเครื่องและข้ามเวลาพอดี การ gitignore มันทิ้งการรับประกันนั้นไปเลย
วิธีแก้:
.terraform/*.tfstate*.tfstate.backupจับได้โดย: git status หลัง init สะอาดๆ แสดง .terraform.lock.hcl เป็น untracked — หรือ policy ของ repo ที่ทำให้ CI fail ถ้าไม่มี lock file
5. Copy pattern การล็อก dynamodb_table ของ S3 backend มาใส่ azurerm ที่ไม่เคยมีจริง
หัวข้อที่มีชื่อว่า “5. Copy pattern การล็อก dynamodb_table ของ S3 backend มาใส่ azurerm ที่ไม่เคยมีจริง”terraform { backend "azurerm" { resource_group_name = "acme-tfstate-rg" storage_account_name = "acmetfstate" container_name = "tfstate" key = "network.terraform.tfstate" dynamodb_table = "acme-tf-locks" }}dynamodb_table เป็นของจริง — บน backend s3 สำหรับ cloud ที่ต่างกันเลย training data ของ AI ผสม backend example ข้าม cloud กันตลอดเวลา และ argument นี้ไม่มีอยู่ใน schema ของ backend azurerm เลย ตาม เอกสาร azurerm backend: backend นี้ “supports state locking and consistency checking with Azure Blob Storage native capabilities” — locking จัดการด้วย blob lease ของ Azure อัตโนมัติ โดยไม่ต้องมี table, resource, หรือ argument เพิ่มให้ provision, จัดการ IAM-permission, หรือจ่ายเงินเลย ต่างจาก dynamodb_table ของ S3 ที่ deprecated แล้ว (ดู aws sibling ของคอร์สนี้)
วิธีแก้:
terraform { backend "azurerm" { resource_group_name = "acme-tfstate-rg" storage_account_name = "acmetfstate" container_name = "tfstate" key = "network.terraform.tfstate" }}จับได้โดย: ไม่มีอะไรใน validate/plan กับ -backend=false เลย — argument นี้ถูกยอมรับอย่างเงียบๆ โดยไม่เคยถูกเช็คกับ backend schema จริง มีแค่ terraform init จริงที่มี credential (ซึ่งคอร์สนี้ไม่เคยรัน) หรือ reviewer ที่รู้ว่า locking ของ azurerm ไม่ต้องมี table เลยเท่านั้นที่จับได้
6. terraform_remote_state ที่ชี้ไป backend/path ที่ไม่เคยถูก apply
หัวข้อที่มีชื่อว่า “6. terraform_remote_state ที่ชี้ไป backend/path ที่ไม่เคยถูก apply”# entries/06-remote-state-cross-backend/main.tf — @expect-error No stored state was found for the given workspace in the given backend.data "terraform_remote_state" "network" { backend = "local" config = { path = "../does-not-exist/terraform.tfstate" }}อาการ — terraform plan จริง:
data.terraform_remote_state.network: Reading...
Planning failed. Terraform encountered an error while generating this plan.
Error: Unable to find remote state
with data.terraform_remote_state.network, on main.tf line 1, in data "terraform_remote_state" "network": 1: data "terraform_remote_state" "network" {
No stored state was found for the given workspace in the given backend.ไม่มี mock fallback เลย — terraform_remote_state พัง plan ทั้งหมดหนักๆ ทันทีที่ state ที่อ้างอิงไม่มีอยู่ ซึ่งเป็นสถานการณ์ปกติระหว่างการ apply ครั้งแรกของ stack ที่ stack อื่นต้องพึ่งพา หรือหลัง state file ถูกย้ายไป key/bucket อื่นโดยไม่ได้อัปเดต consumer ทุกตัว
วิธีแก้: ชี้ config.path/config.key ไปที่ state file ที่ apply ไปแล้วจริง หรือเปลี่ยนการอ่าน cross-stack เป็น pattern dependency block ของ Terragrunt (mock_outputs) ที่บทเรียน dependencies-between-units ของคอร์สนี้สอนไว้ ซึ่งมี offline fallback จริงๆ
จับได้โดย: terraform plan — ดังและทันทีทุกครั้ง — แต่จับได้เฉพาะ plan ของ consumer ไม่ใช่ของ producer ถ้า producer stack เปลี่ยน backend key อย่างเงียบๆ consumer ทุกตัวจะพังตอน plan ครั้งถัดไป ไม่ใช่ producer
Variables
หัวข้อที่มีชื่อว่า “Variables”7. nullable = false แทนที่ default อย่างเงียบๆ แทนที่จะ error
หัวข้อที่มีชื่อว่า “7. nullable = false แทนที่ default อย่างเงียบๆ แทนที่จะ error”variable "instance_count" { type = number default = 3 nullable = false}output "effective_count" { value = var.instance_count}variable "override" { type = number default = null}module "child" { source = "./child" instance_count = var.override}
output "child_effective_count" { value = module.child.effective_count}อาการ — terraform apply จริง, var.override เป็น null:
Changes to Outputs: + child_effective_count = 3
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
child_effective_count = 3ด้วย nullable = false (ค่า default สำหรับ variable ที่ไม่ได้ตั้ง nullable ไว้เลย — AI มักเขียนออกมาตรงๆ โดยคิดว่ามันจะปฏิเสธ null) การส่ง null มาตรงๆ ไม่ error: Terraform แทนที่ default อย่างเงียบๆ เป็น 3 โดยไม่มีสัญญาณใดๆ ว่ามีการ override เกิดขึ้นเลย การเปลี่ยน variable ตัวเดียวกันเป็น nullable = true แล้วรันซ้ำพิสูจน์ความต่างตรงๆ — output กลายเป็น null จริง:
Changes to Outputs: - child_effective_count = 3 -> null
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.วิธีแก้: ถ้า null ควรเป็น input ที่แยกแยะได้จริง ตั้ง nullable = true แล้วให้ logic ด้านล่าง branch ตาม var.x == null ตรงๆ ถ้า null ควรถูกปฏิเสธจริงๆ อย่าตั้ง nullable = false แล้วพึ่งการแทนที่อย่างเงียบๆ — validate มันแทน:
variable "instance_count" { type = number default = 3
validation { condition = var.instance_count != null error_message = "instance_count must not be null." }}จับได้โดย: ไม่มีอะไรใน plan/apply output เลยโดย default — มีแค่ terraform plan ที่ diff ค่า ก่อน กับ หลัง ข้าม refactor หรือ validation block ที่เขียนมาปฏิเสธ null ตรงๆ
8. default ตรงๆ ของ ephemeral variable รั่วเข้า show -json
หัวข้อที่มีชื่อว่า “8. default ตรงๆ ของ ephemeral variable รั่วเข้า show -json”variable "api_token" { type = string ephemeral = true default = "shh-do-not-print-me"}
resource "terraform_data" "example" { input = "static-value"}อาการ — terraform show -json จริงบน plan ที่ save ไว้:
$ terraform plan -out=plan.out && terraform show -json plan.out | jq '.variables'{ "api_token": { "value": "shh-do-not-print-me" }}ephemeral = true (Terraform 1.10+) กันค่าออกจาก state แต่ไม่กัน default ตรงๆ ออกจาก variables block ของ plan file ที่ save ไว้ — โครงสร้าง JSON เดียวกับที่ policy conftest/OPA ใน CI อ่าน (ดู entry 20) default ที่ตั้งใจแค่เป็น fallback ในเครื่องหลุดตรงเข้าไปใน artifact ที่ CI archive ไว้เลย
วิธีแก้: อย่าให้ ephemeral variable มี default เป็น secret ตรงๆ เด็ดขาด (ไม่มี default, หรือใช้ ephemeral resource เป็นแหล่งข้อมูลจริง):
variable "api_token" { type = string ephemeral = true}
resource "terraform_data" "example" { input = "static-value"}จับได้โดย: grep หา string ที่ดูเหมือน secret ใน plan artifact ที่ archive จาก show -json — validate/plan ไม่เคย flag เรื่องนี้เอง
Resources & lifecycle
หัวข้อที่มีชื่อว่า “Resources & lifecycle”9. prevent_destroy อยู่บน resource ที่ทิ้งได้ ไม่ใช่ตัวที่สำคัญ
หัวข้อที่มีชื่อว่า “9. prevent_destroy อยู่บน resource ที่ทิ้งได้ ไม่ใช่ตัวที่สำคัญ”resource "azurerm_resource_group" "scratch" { name = "acme-scratch-rg" location = "eastus"
lifecycle { prevent_destroy = true }}
resource "azurerm_key_vault" "primary" { name = "acme-primary-kv" location = "eastus" resource_group_name = "acme-prod-rg" tenant_id = "11111111-1111-1111-1111-111111111111" sku_name = "standard" rbac_authorization_enabled = true}prevent_destroy เป็นกลไก lifecycle หลักของ Terraform เหมือนกันทุก provider — พิสูจน์แล้วครั้งเดียวเต็มรูปแบบด้วย resource AWS จริงใน aws sibling ของคอร์สนี้: plan -destroy กับ stack ที่มีแค่ aws_iam_role ที่ทิ้งได้มี prevent_destroy = true บล็อกแค่การ destroy ของ role นั้นเท่านั้น ในขณะที่ aws_db_instance จริงข้างๆ destroy ได้โดยไม่มีอะไรต้านเลย กลไกระดับ graph เดียวกันเป๊ะใช้กับ resource ของ provider ไหนก็ได้ รวมถึง azurerm
วิธีแก้: ย้าย prevent_destroy ไปยัง resource ที่ต้องการมันจริงๆ:
resource "azurerm_key_vault" "primary" { name = "acme-primary-kv" location = "eastus" resource_group_name = "acme-prod-rg" tenant_id = "11111111-1111-1111-1111-111111111111" sku_name = "standard" rbac_authorization_enabled = true
lifecycle { prevent_destroy = true }}จับได้โดย: plan -destroy/terraform plan สำหรับ resource ที่เจาะจงจริงๆ — prevent_destroy ป้องกันเฉพาะ resource ที่ประกาศไว้เท่านั้น ไม่เคยป้องกัน “สิ่งที่สำคัญที่อยู่ใกล้ๆ”
10. count กับ list ที่จัดลำดับใหม่ได้ — การเลื่อน index
หัวข้อที่มีชื่อว่า “10. count กับ list ที่จัดลำดับใหม่ได้ — การเลื่อน index”variable "rg_names" { type = list(string) default = ["logs", "assets", "backups"]}
resource "azurerm_resource_group" "this" { count = length(var.rg_names) name = "acme-${var.rg_names[count.index]}-${count.index}" location = "eastus"}อาการ — terraform test จริงกับ mock_provider "azurerm" {}: seed resource group สามตัวด้วย apply แล้ว plan โดยเอา element ตรงกลาง ("assets") ออกจาก rg_names:
# azurerm_resource_group.this[1] will be updated in-place ~ resource "azurerm_resource_group" "this" { id = "tbfvcse9" ~ name = "acme-assets-1" -> "acme-backups-1" # (1 unchanged attribute hidden) }
# azurerm_resource_group.this[2] will be destroyed # (because index [2] is out of range for count) - resource "azurerm_resource_group" "this" { - id = "7wknoyg5" -> null - location = "eastus" -> null - name = "acme-backups-2" -> null }
Plan: 0 to add, 1 to change, 1 to destroy.count.index อิงตำแหน่ง ไม่ใช่ identity การเอา "assets" ออกจากตรงกลางเลื่อน "backups" จาก index 2 ไปเป็น index 1 — Terraform อ่านว่า “index 1 เปลี่ยนจาก resource group assets เป็น resource group backups” (การ rename ในที่เดิม — name เปลี่ยนได้บน azurerm_resource_group เลยไม่ถึงกับบังคับ replace แค่ rename resource group ผิดตัวอย่างเงียบๆ) และ “index 2 ไม่มีอยู่แล้ว” (การ destroy) ทั้งที่เจตนาของมนุษย์คือแค่ “ลบ resource group เดียว เหลืออีกสองตัวไว้เฉยๆ”
วิธีแก้: key ด้วย identity ที่แน่นอนด้วย for_each ไม่ใช่ตำแหน่งเลย:
variable "rg_names" { type = list(string) default = ["logs", "backups"]}
resource "azurerm_resource_group" "this" { for_each = toset(var.rg_names) name = "acme-${each.value}" location = "eastus"}จับได้โดย: terraform plan -detailed-exitcode ทุกครั้งที่มีการเปลี่ยน list ที่ index ด้วย count — คู่ destroy/replace มองเห็นได้ทุก plan แต่เฉพาะถ้ามีคนอ่าน plan จริงๆ แทนที่จะเชื่อ apply ที่ขึ้นสีเขียว
11. for_each บนค่าที่รู้เฉพาะหลัง apply
หัวข้อที่มีชื่อว่า “11. for_each บนค่าที่รู้เฉพาะหลัง apply”resource "azurerm_resource_group" "generator" { name = "acme-generator-rg" location = "eastus"}
resource "azurerm_role_assignment" "per_tag" { for_each = toset(azurerm_resource_group.generator.tags == null ? [] : keys(azurerm_resource_group.generator.tags)) scope = azurerm_resource_group.generator.id role_definition_name = "Reader" principal_id = "11111111-1111-1111-1111-111111111111"}count ยอมรับตัวเลขที่รู้เฉพาะหลัง apply ได้ในบางบริบท แต่ for_each ไม่เคยยอมรับ set ของ key ที่ไม่รู้ตอน plan เลย — key คือ identity และ Terraform ต้องรู้ identity ของทุก resource ก่อนจะคำนวณ plan ได้เลย นี่คือพฤติกรรม graph-evaluation หลักของ Terraform เหมือนกันทุก provider — พิสูจน์ตรงๆ เต็มรูปแบบด้วย resource AWS จริงใน aws sibling ของคอร์สนี้: error “Invalid for_each argument” จริงและปัจจุบัน ระบุ computed attribute ที่ชัดเจน (tags_all is a map of string, known only after apply) ที่ทำให้ set ไม่รู้ค่า
วิธีแก้: derive set ของ for_each จากสิ่งที่รู้ตอน plan — variable, ค่า locals, หรือ attribute ของ data source ที่รู้ตอน plan — ไม่ใช่จาก output ที่ computed ของ managed resource อีกตัว
จับได้โดย: terraform plan — เสมอ ดังทันที ก่อนมีอะไรถูกสร้างเลย กับ provider จริง ตัวนี้ไม่เคยหลุดไปแบบเงียบๆ
12. null_resource ในที่ที่ terraform_data (1.4+) เป็นตัวแทนในตัว
หัวข้อที่มีชื่อว่า “12. null_resource ในที่ที่ terraform_data (1.4+) เป็นตัวแทนในตัว”resource "null_resource" "seed" { triggers = { always = timestamp() } provisioner "local-exec" { command = "echo seeding" }}ตัวนี้ยัง validate ผ่านสะอาด — null_resource ไม่ถูกเอาออก — แต่มันพึ่งพา provider แยกต่างหาก hashicorp/null สำหรับสิ่งที่ Terraform 1.4+ มีให้เป็น resource type ในตัวโดยไม่ต้องพึ่ง provider: terraform_data ส่วนหนึ่งของ built-in provider terraform.io/builtin/terraform ของ Terraform เอง (ใช้ทั่ว catalog นี้แล้ว, entry 6, 8, 11, 17) terraform_data เพิ่ม attribute input/output ที่ null_resource ไม่เคยมี ทำให้ reference ที่อยู่ปลายทางได้ค่าจริงที่มี type แทนที่จะมีแค่ id/triggers
วิธีแก้:
resource "terraform_data" "seed" { triggers_replace = [timestamp()] provisioner "local-exec" { command = "echo seeding" }}จับได้โดย: ไม่มีอะไรใน validate/plan เลย — resource type ทั้งสองทำงานได้ มีแค่ reviewer ที่รู้ว่า terraform_data ตัดทั้ง provider dependency เพิ่มออกไป (hashicorp/null — อีกหนึ่งบรรทัดใน .terraform.lock.hcl อีกหนึ่ง plugin ที่ต้อง download และ pin) สำหรับพฤติกรรมที่เท่ากันเท่านั้นที่จับได้
13. Argument ที่ hallucinate มาจาก resource type อื่น
หัวข้อที่มีชื่อว่า “13. Argument ที่ hallucinate มาจาก resource type อื่น”resource "azurerm_network_interface" "app" { name = "app-nic" resource_group_name = "acme-rg" location = "eastus" ip_configuration { name = "internal" subnet_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/acme-rg/providers/Microsoft.Network/virtualNetworks/acme-vnet/subnets/acme-subnet" private_ip_address_allocation = "Dynamic" }}# entries/13-hallucinated-argument/main.tf — @expect-error An argument named "deletion_protection" is not expected here.resource "azurerm_linux_virtual_machine" "app" { name = "app-vm" resource_group_name = "acme-rg" location = "eastus" size = "Standard_B1s" admin_username = "adminuser" deletion_protection = true network_interface_ids = [azurerm_network_interface.app.id] admin_ssh_key { username = "adminuser" public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCw76OeKnxX3rY+weRZtTNo1mo2LE8lVHFsVULODxrTyLOlEFcAvgeRjFcV7W+WH7FPWmEtCWPKQjPPpCMF4Vi/5xtHh0p0ZGNqa7V31/wdHCMZmyZRtNuKnRznl9EjozCuaHTGIzkKTd7kEsJXETFOgAhR9lNQUAyT895imscQ45Ld6zt3XhU35AgQbQynxGhI7M1MWni3RwRBg5Rzm23wMsXAlv7qXP0axoQhR0CNxFXOCW56E4rNp42yUCoPQJL3LJWBGLv/HN4GMB8P4ksL/e4UdiFcKK4gKL5vlivkyK7brTrit+8khv4Uj66vDGSbKAzDdiYXtI01csKlfwNZ" } os_disk { caching = "ReadWrite" storage_account_type = "Standard_LRS" } source_image_reference { publisher = "Canonical" offer = "0001-com-ubuntu-server-jammy" sku = "22_04-lts" version = "latest" }}deletion_protection เป็น argument จริงของ Terraform บน resource หลายตัว (aws_db_instance, google_sql_database_instance) — มันไม่มีอยู่บน azurerm_linux_virtual_machine เลย training data ของ AI ผสม idiom เรื่อง deletion-protection ข้าม cloud และ resource type กันตลอดเวลา
อาการ — terraform validate จริง:
Error: Unsupported argument
on main.tf line 6, in resource "azurerm_linux_virtual_machine" "app": 6: deletion_protection = true
An argument named "deletion_protection" is not expected here.วิธีแก้: Azure VM ไม่มีสวิตช์ “deletion protection” ตรงๆ เลย — ตัวที่เทียบเท่าจริงและปัจจุบันคือ resource lock (azurerm_management_lock) ของ Microsoft.Authorization/locks ซึ่งเป็น resource แยกต่างหากเลย ไม่ใช่ argument บน VM:
resource "azurerm_network_interface" "app" { name = "app-nic" resource_group_name = "acme-rg" location = "eastus" ip_configuration { name = "internal" subnet_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/acme-rg/providers/Microsoft.Network/virtualNetworks/acme-vnet/subnets/acme-subnet" private_ip_address_allocation = "Dynamic" }}resource "azurerm_linux_virtual_machine" "app" { name = "app-vm" resource_group_name = "acme-rg" location = "eastus" size = "Standard_B1s" admin_username = "adminuser" network_interface_ids = [azurerm_network_interface.app.id] admin_ssh_key { username = "adminuser" public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCw76OeKnxX3rY+weRZtTNo1mo2LE8lVHFsVULODxrTyLOlEFcAvgeRjFcV7W+WH7FPWmEtCWPKQjPPpCMF4Vi/5xtHh0p0ZGNqa7V31/wdHCMZmyZRtNuKnRznl9EjozCuaHTGIzkKTd7kEsJXETFOgAhR9lNQUAyT895imscQ45Ld6zt3XhU35AgQbQynxGhI7M1MWni3RwRBg5Rzm23wMsXAlv7qXP0axoQhR0CNxFXOCW56E4rNp42yUCoPQJL3LJWBGLv/HN4GMB8P4ksL/e4UdiFcKK4gKL5vlivkyK7brTrit+8khv4Uj66vDGSbKAzDdiYXtI01csKlfwNZ" } os_disk { caching = "ReadWrite" storage_account_type = "Standard_LRS" } source_image_reference { publisher = "Canonical" offer = "0001-com-ubuntu-server-jammy" sku = "22_04-lts" version = "latest" }}
resource "azurerm_management_lock" "app" { name = "app-vm-cannot-delete" scope = azurerm_linux_virtual_machine.app.id lock_level = "CanNotDelete"}จับได้โดย: terraform validate — ตัวนี้ดังและทันทีเสมอตอน plan รูปแบบที่อันตรายของ pattern นี้คือ entry 2/9 ที่ argument ผิดแต่จริงไม่ error เลย
Modules
หัวข้อที่มีชื่อว่า “Modules”14. Module source ที่ไม่มี ?ref=
หัวข้อที่มีชื่อว่า “14. Module source ที่ไม่มี ?ref=”# not a real path — a git clone against a live, network-reachable repo is out of scope for this offline harness; see the callout belowmodule "vpc" { source = "git::https://example.com/vpc.git"}ตาม เอกสาร module sources: “You can add the ref query parameter to the location specified in the source argument to reference any value supported by the git checkout command, such as a branch, SHA-1 hash, or tag.” ถ้าไม่มี: “Terraform clones and uses the default branch referenced by HEAD” — หมายความว่า configuration เดียวกันเป๊ะ ที่ re-initialize คนละวัน สามารถดึง commit ของ module คนละตัวได้อย่างเงียบๆ
วิธีแก้:
# not a real path — same reason as abovemodule "vpc" { source = "git::https://example.com/vpc.git?ref=v1.4.0"}จับได้โดย: ไม่มีอะไรใน validate/plan เลย — resolve สำเร็จทั้งคู่ .terraform.lock.hcl บันทึก hash ของ provider ไม่เคยบันทึก revision ของ module source เลย จึงไม่มีตัวจับ drift ในตัวสำหรับเรื่องนี้เลย มีแค่ tag/SHA ที่ pin ไว้และการ review ของมนุษย์บนค่า ref= เท่านั้นที่จับได้
15. depends_on ที่เพิ่มมาปิดบัง reference cycle ที่มีอยู่จริง
หัวข้อที่มีชื่อว่า “15. depends_on ที่เพิ่มมาปิดบัง reference cycle ที่มีอยู่จริง”# entries/15-depends-on-hides-cycle/main.tf — @expect-error Error: Cycleresource "terraform_data" "a" { input = terraform_data.b.output}
resource "terraform_data" "b" { input = terraform_data.a.output}อาการ — terraform validate จริง:
Error: Cycle: terraform_data.a terraform_data.bการเพิ่ม depends_on ตรงๆ ให้ฝั่งใดฝั่งหนึ่ง หวังว่าจะ “บังคับ” ลำดับ ไม่ได้แก้ปัญหานี้เลย — depends_on เพิ่มแค่ constraint ลำดับเพิ่มเติมบน graph ที่ Terraform สร้างจาก reference จริง มันเอา cycle ที่ reference เหล่านั้นสร้างขึ้นเองออกไม่ได้ วิธีแก้ที่ถูกต้องคือทำลาย reference loop จริงๆ ไม่ใช่ปิดบัง graph error ด้วยคำใบ้เรื่องลำดับ
วิธีแก้: ทำลาย cycle โดยให้ทั้งสองฝั่งมีแหล่งความจริงที่เป็นอิสระต่อกัน ไม่วนซ้ำ:
locals { shared_seed = "2024-01-shared"}
resource "terraform_data" "a" { input = local.shared_seed}
resource "terraform_data" "b" { input = local.shared_seed}จับได้โดย: terraform validate — เสมอ ทันที และไม่มี depends_on ใดกดไว้ได้ ความเสี่ยงจริงๆ คือเวลาที่ผู้ช่วย AI ใช้ไปกับการเพิ่ม depends_on block พยายามแก้ error ที่สาเหตุจริงคือ circular reference แท้ๆ
Terragrunt
หัวข้อที่มีชื่อว่า “Terragrunt”16. run-all/plan-all — ถูกเอาออกใน CLI redesign 1.1
หัวข้อที่มีชื่อว่า “16. run-all/plan-all — ถูกเอาออกใน CLI redesign 1.1”$ terragrunt plan-all --working-dir live/dev/vpcERROR unknown command: "plan-all". Terragrunt no longer forwards unknown commands by default. Use 'terragrunt run -- plan-all ...' or a supported shortcut. Learn more: https://docs.terragrunt.com/migrate/cli-redesign/#use-the-new-run-command
$ terragrunt run-all plan --working-dir .ERROR unknown command: "run-all". Terragrunt no longer forwards unknown commands by default. Use 'terragrunt run -- run-all ...' or a supported shortcut. Learn more: https://docs.terragrunt.com/migrate/cli-redesign/#use-the-new-run-commandทั้งสองคำสั่งอยู่เต็มไปหมดใน tutorial ยุคก่อน 1.1 ดังนั้นอยู่เต็ม training data ของ AI ด้วย CLI redesign ของ Terragrunt 1.1 แทนที่ทั้งคู่
วิธีแก้ — คำสั่งจริงปัจจุบัน รันจริงกับ tree สองหน่วย:
remote_state { backend = "local" generate = { path = "backend.tf", if_exists = "overwrite" } config = { path = "${get_terragrunt_dir()}/terraform.tfstate" }}variable "cidr" { type = string default = "10.0.0.0/16"}resource "terraform_data" "vpc" { input = var.cidr}include "root" { path = find_in_parent_folders("root.hcl")}
terraform { source = "${get_terragrunt_dir()}/../../../modules/vpc"}$ terragrunt run --all plan --working-dir .[live/dev/vpc] terraform: Plan: 1 to add, 0 to change, 0 to destroy.
❯❯ Run Summary 1 units 56ms Succeeded 1จับได้โดย: ทันที ทุกครั้ง — Terragrunt ปฏิเสธไม่ยอมเริ่มด้วยซ้ำ ไม่เคยหลุดไปแบบเงียบๆ
17. terragrunt.hcl เดียวกัน copy-paste จาก dev มา prod โดยไม่แตะ inputs เลย
หัวข้อที่มีชื่อว่า “17. terragrunt.hcl เดียวกัน copy-paste จาก dev มา prod โดยไม่แตะ inputs เลย”terraform { source = "${get_terragrunt_dir()}/../../../modules/vpc"}
inputs = { account_id = "111111111111" region = "us-east-1"}terraform { source = "${get_terragrunt_dir()}/../../../modules/vpc"}
inputs = { account_id = "111111111111" region = "us-east-1"}อาการ — reproduce จริงในเครื่อง:
$ terragrunt hcl validate --working-dir live/prod/vpc(no output — both files are syntactically valid HCL)
$ diff live/dev/vpc/terragrunt.hcl live/prod/vpc/terragrunt.hcl(no output — the files are byte-identical)ไม่มีอะไรในนี้เป็น HCL ที่ invalid เลย prod/vpc deploy ทับ account AWS ของ dev อย่างเงียบๆ — terragrunt hcl validate ไม่มีทางรู้ว่า account_id = "111111111111" ภายใต้ live/prod/ ผิด เพราะไม่มีอะไรในไฟล์ที่บอกว่า account ไหนคือ “ถูกต้อง” สำหรับ path นั้น
วิธีแก้: derive account_id/region จาก directory path เอง เพื่อให้ dev กับ prod ไม่สามารถแชร์ค่าผ่าน copy-paste ได้จริงๆ:
locals { account_id = "222222222222" region = "us-east-1"}locals { env_vars = read_terragrunt_config(find_in_parent_folders("env.hcl"))}
terraform { source = "${get_terragrunt_dir()}/../../../modules/vpc"}
inputs = { account_id = local.env_vars.locals.account_id region = local.env_vars.locals.region}จับได้โดย: ไม่มีอะไรอัตโนมัติเลย — hcl validate/hcl format --check ผ่านทั้งคู่ มีแค่ terragrunt run --all plan จริงต่อ environment ที่ diff โดยมนุษย์ที่รู้ว่า environment ไหนควรเล็งไป account ไหนเท่านั้นที่จับ copy-paste ที่สะอาดขนาดนี้ได้
CI & policy
หัวข้อที่มีชื่อว่า “CI & policy”18. -auto-approve ใน CI pipeline
หัวข้อที่มีชื่อว่า “18. -auto-approve ใน CI pipeline”อาการ — จริง เทียบข้างกัน:
$ terraform applyDo you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: noApply cancelled.
$ terraform apply -auto-approveterraform_data.release: Creating...terraform_data.release: Creation complete after 0s [id=bb8b46af-eb1e-0b33-104a-b4b39a2559a6]Apply complete! Resources: 1 added, 0 changed, 0 destroyed.-auto-approve ไม่ได้แค่ข้าม prompt ที่ CI ตอบไม่ได้อยู่แล้ว — มันเอาจุดสุดท้ายที่มนุษย์ (หรือ policy gate ที่อ่าน plan ที่กำลังจะถูก apply เป๊ะๆ) จะหยุดการเปลี่ยนแปลงที่ทำลายข้อมูลได้ออกไป ทางแก้ที่ถูกต้องคือแยกขั้นตอน plan -out=tfplan, policy check (entry 20) หรือ human approval gate กับ plan ที่ save ไว้นั้น แล้วค่อย apply tfplan — ไม่ใช่ apply -auto-approve กับ plan สดใหม่ที่ยังไม่ review เลย
วิธีแก้:
# .github/workflows/terraform.yml (fragment, not a collected fence)- run: terraform plan -out=tfplan -detailed-exitcode- run: conftest test --policy policy $(terraform show -json tfplan | tee plan.json | true)- run: terraform apply tfplan # applies the reviewed plan file, not a fresh -auto-approveจับได้โดย: การ review pipeline เท่านั้น ไม่เคยเป็น validate/plan — ทั้งสอง flag เป็นการใช้ CLI ที่ valid
19. checkov-action@master — tag ที่เปลี่ยนได้ถูกใช้ราวกับ pin แล้ว
หัวข้อที่มีชื่อว่า “19. checkov-action@master — tag ที่เปลี่ยนได้ถูกใช้ราวกับ pin แล้ว”# .github/workflows/checkov.yml (fragment, not a collected fence)- uses: bridgecrewio/checkov-action@master with: directory: .ตาม คู่มือ hardening ของ GitHub เอง: “Pinning an action to a full-length commit SHA is currently the only way to use an action as an immutable release.” @master (หรือ @main, @v1) เป็น branch reference — มันชี้ไปที่ commit ที่ต่างออกไปพรุ่งนี้ก็ได้ โดยไม่มี diff ใน history ของ repo นี้เองให้ review เลย เหตุผลของ GitHub, quote ตรงๆ: bad actor ที่ compromise repo ของ action นั้นต้องใช้ “a SHA-1 collision for a valid Git object payload” เพื่อเอาชนะ SHA ที่ pin ไว้ — ทำไม่ได้ในทางคำนวณ — ในขณะที่ tag หรือ branch ที่เปลี่ยนได้สามารถถูกย้ายได้เฉยๆ
วิธีแก้:
# .github/workflows/checkov.yml (fragment, not a collected fence)- uses: bridgecrewio/checkov-action@c98096a34955fc70f684e0c1ce43b4e1b2ea3f9 # v12.2919.0 with: directory: .จับได้โดย: ไม่มีอะไรใน output ของ CI เองเลย — workflow รันเหมือนกันทั้งคู่ จนกว่าจะถึงวันที่ไม่เหมือน มีแค่ linter ที่ pin dependency (zizmor, การเตือนเรื่อง pin version ของ Dependabot เอง) หรือการ review ด้วยมือเท่านั้นที่จับได้
20. Rego ก่อน v1 (deny[msg] { ... }) load ไม่ผ่านอย่างเงียบๆ ภายใต้ OPA v1
หัวข้อที่มีชื่อว่า “20. Rego ก่อน v1 (deny[msg] { ... }) load ไม่ผ่านอย่างเงียบๆ ภายใต้ OPA v1”# entries/20-rego-v1/policy/deny_no_tags.rego — @expect-error rego_parse_error: `if` keyword is required before rule bodypackage main
deny[msg] { resource := input.resource_changes[_] resource.type == "terraform_data" msg := sprintf("%s must set tags for cost allocation", [resource.address])}อาการ — conftest test จริง, openpolicyagent/conftest ปัจจุบันที่ default เป็น syntax Rego v1, กับ plan terraform show -json จริง (plan ของ config provider azurerm สำหรับ resource marker terraform_data — plan จริงของ azurerm ต้องการ authorizer ของ Azure จริงซึ่งคอร์สนี้ไม่มี ดู entry 9 ดังนั้น policy target ที่นี่คือ resource terraform_data ที่ไม่ผูกกับ provider เดียวกับที่ใช้ทั่ว catalog นี้):
Error: running test: load: loading policies: load: 2 errors occurred during loading:policy/deny_no_tags.rego:3: rego_parse_error: `if` keyword is required before rule bodypolicy/deny_no_tags.rego:3: rego_parse_error: `contains` keyword is required for partial set rulesรูปแบบ bracket-set rule (deny[msg] { ... }) ที่เต็มไป tutorial Rego มาหลายปี ซึ่ง training data ของ AI ดึงมา คือสิ่งที่ OPA v1 (default ตั้งแต่ OPA 1.0 / conftest ปัจจุบัน) ปฏิเสธตรงๆ — deny กลายเป็น error ที่ load ไม่ผ่านเลย ไม่ใช่แค่ rule ที่ไม่เคยทำงาน
วิธีแก้ — syntax v1 จริง, policy เดียวกัน, รันกับ plan จริง:
package main
import rego.v1
deny contains msg if { resource := input.resource_changes[_] resource.type == "terraform_data" msg := sprintf("%s must set tags for cost allocation", [resource.address])}$ docker run --rm -v "$(pwd)":/project openpolicyagent/conftest test --policy policy plan.jsonFAIL - plan.json - main - terraform_data.app_marker must set tags for cost allocation
1 test, 0 passed, 0 warnings, 1 failure, 0 exceptionsจับได้โดย: conftest test เอง จริงและทันที — แต่ในรูปแบบ load error ที่ดูเหมือนปัญหา CI/tooling ได้ ทั้งที่จริงๆ แล้วคือ syntax policy ที่ล้าสมัย
21. Checkov skip comment ที่ระบุ check ID ผิด (แต่จริง)
หัวข้อที่มีชื่อว่า “21. Checkov skip comment ที่ระบุ check ID ผิด (แต่จริง)”resource "azurerm_storage_account" "logs" { #checkov:skip=CKV_AZURE_35: public access is required for this static site bucket name = "acmelogsstorage" resource_group_name = "acme-rg" location = "eastus" account_tier = "Standard" account_replication_type = "LRS" public_network_access_enabled = true}ทั้ง CKV_AZURE_35 และ CKV_AZURE_59 เป็น check ID จริงของ checkov — สำหรับ rule ที่ต่างกัน CKV_AZURE_35 คือ “Ensure default network access rule for Storage Accounts is set to deny” (default action ของ network ACL) CKV_AZURE_59 คือ “Ensure that Storage accounts disallow public access” (ยืนยันตรงกับ source ที่ checkov ติดตั้งจริงเอง checkov/terraform/checks/resource/azure/StorageAccountDefaultNetworkAccessDeny.py และ StorageAccountDisablePublicAccess.py) skip comment ที่ AI สร้างขึ้นระบุ check เรื่อง network-ACL เพื่อ silence สิ่งที่จริงๆ เป็น check เรื่อง public-access — ID จริง แต่เล็งผิด rule
อาการ — reproduce จริงด้วย image bridgecrew/checkov ที่ pin ไว้:
$ docker run --rm -v "$(pwd)":/tf bridgecrew/checkov -d /tf --compact --quietCheck: CKV_AZURE_59: "Ensure that Storage accounts disallow public access"Skip comment ไม่ error ไม่เตือนว่า ID เป้าหมายของมันใช้กับที่นี่ไม่ได้ — CKV_AZURE_59 ยัง fail และยังอยู่ในรายงานเหมือนเดิม เหมือนไม่มี skip comment เลย เพราะ skip นั้น match แค่ CKV_AZURE_35 เท่านั้น (check ที่ไม่เคยเป็นตัวที่ fail อยู่แล้ว)
วิธีแก้: skip ID ที่กำลัง fire จริง และเฉพาะตอนที่ exception นั้นตั้งใจจริงๆ เท่านั้น:
resource "azurerm_storage_account" "logs" { #checkov:skip=CKV_AZURE_59: public access is required for this static site bucket name = "acmelogsstorage" resource_group_name = "acme-rg" location = "eastus" account_tier = "Standard" account_replication_type = "LRS" public_network_access_enabled = true}จับได้โดย: ไม่มีอะไรที่ checkov เองรายงานเลย — นี่คือรูปแบบที่อันตรายที่สุดใน catalog ทั้งหมดนี้: comment suppression ที่อ่านเหมือนจะปิด finding ด้านล่างมัน แต่จริงๆ ปิด check อื่นที่ไม่เกี่ยวข้องกันเลยในการรันนี้ มีแค่การเทียบ ID ที่ skip comment ระบุกับ check catalog จริงและปัจจุบันของ checkov เท่านั้นที่จับได้
Secrets
หัวข้อที่มีชื่อว่า “Secrets”22. terraform.tfvars ที่มี secret จริง commit เข้าไป
หัวข้อที่มีชื่อว่า “22. terraform.tfvars ที่มี secret จริง commit เข้าไป”db_password = "Sup3rSecret!2024"api_token = "ghp_1234567890abcdefghijklmnopqrstuvwxyz".terraform/*.tfstate*.tfvars ไม่อยู่ใน .gitignore เลย — .gitignore ของ Terraform ทั่วไป (ดู entry 4) ป้องกัน state และ provider cache แต่หลาย template จริงไม่เคยพูดถึง .tfvars เลย เพราะไฟล์ *.tfvars.example/ไฟล์ .tfvars ที่ ไม่ใช่ secret ที่ commit เข้าไปเป็น pattern ที่ถูกต้องและพบได้ทั่วไป ผู้ช่วย AI ที่สร้าง .gitignore จาก prompt ทั่วไปก็สร้างช่องว่างแบบเดียวกันซ้ำ
วิธีแก้:
.terraform/*.tfstate*.tfvars!*.tfvars.example…บวกกับการย้าย secret จริงออกจากไฟล์ใดๆ เลย — environment variable (TF_VAR_db_password), reference ไปยัง secrets manager, หรือ write-only argument (entry 23) — เพราะการแก้ .gitignore ทีหลังไม่ได้เอา secret ที่ commit เข้าไปแล้วออกจาก git history เลย
จับได้โดย: git log -p -- '*.tfvars' บน repo ที่มีอยู่แล้ว หรือ pre-commit secret scanner (gitleaks, trufflehog) — ไม่เคยเป็น validate/plan ซึ่งไม่ตรวจ .gitignore เลย
23. sensitive = true ซ่อนแค่ CLI ไม่ใช่ state file
หัวข้อที่มีชื่อว่า “23. sensitive = true ซ่อนแค่ CLI ไม่ใช่ state file”resource "random_password" "db" { length = 16 special = false}
output "db_password" { value = random_password.db.result sensitive = true}อาการ — จริง เทียบข้างกัน:
$ terraform apply -auto-approveOutputs:db_password = <sensitive>
$ grep -A1 '"result"' terraform.tfstate "result": "QxDS75iFGBWDNsqc",sensitive = true ซ่อน CLI output และ terraform show โดย default — มันไม่ encrypt หรือละค่าออกจาก terraform.tfstate บน disk เลย ใครก็ตามที่อ่าน state file ได้ (disk ในเครื่อง, storage account ที่ไม่มี RBAC รัดกุม, CI log artifact ที่ดันไป cat state) อ่าน password จริงเป็น plaintext ได้เลย ไม่ว่าจะติด label “sensitive” หรือไม่ random_password ไม่มี write-only argument ให้หลีกเลี่ยงเรื่องนี้ — แต่ azurerm_postgresql_flexible_server มี: administrator_password_wo + administrator_password_wo_version ยืนยันตรงกับ schema ของ provider hashicorp/azurerm 5.7.0 ที่ติดตั้งจริง ซึ่งไม่ถูกเขียนเข้า state เลย
วิธีแก้: สำหรับค่าที่ไม่จำเป็นต้อง persist ใน state เลยจริงๆ ใช้ write-only argument ที่ resource ปลายทางรองรับ และถือว่า sensitive = true เป็นแค่ตัวควบคุม CLI output — ไม่ใช่ security boundary เลย azurerm ก็มี write-only argument จริงเช่นกัน — ยืนยันตรงกับ schema ของ provider hashicorp/azurerm 5.7.0 ที่ติดตั้งจริง (azurerm_key_vault_secret.value_wo, azurerm_mssql_server.administrator_login_password_wo, azurerm_postgresql_flexible_server.administrator_password_wo และอื่นๆ):
resource "random_password" "db" { length = 16 special = false}
resource "azurerm_postgresql_flexible_server" "primary" { name = "acme-primary-pg" resource_group_name = "acme-prod-rg" location = "eastus" version = "16" administrator_login = "psqladmin" administrator_password_wo = random_password.db.result administrator_password_wo_version = 1 sku_name = "B_Standard_B1ms"}จับได้โดย: grep บน terraform.tfstate/terraform show -json ดิบๆ — ไม่เคยเป็น CLI output ที่ถูกซ่อนไว้เอง ซึ่งเป็นสิ่งที่ทำให้ pattern นี้ถูกเชื่อผิดๆ ได้ง่าย