Worked review 1: network + compute module
Feature ที่ต้องทำ
หัวข้อที่มีชื่อว่า “Feature ที่ต้องทำ”Prompt คือ: “เขียน Terraform module ที่ reuse ได้สำหรับ VPC พร้อม subnets, firewall rule, และ Compute Engine instance แล้วเรียกใช้ครั้งละหนึ่ง environment” สิ่งที่ตามมาคือ draft แรกแบบที่ AI assistant ส่งกลับมาให้กับ prompt แบบนี้พอดี — ทุก block ดูสมเหตุสมผลเป็น HCL รายตัว resource type เป็นของจริง ชื่อ argument ส่วนใหญ่ก็เป็นของจริง อ่านตามที่ review checklist แนะนำ: provider และ version ก่อน แล้วตามด้วย backend แล้วค่อย resources กับ lifecycle แล้วจึงถึงขอบเขตของ module เอง มี bug จริง 9 ข้อ; ดู AI bug catalog สำหรับ pattern ทั่วไปเบื้องหลังแต่ละข้อ ทุกข้อที่กล่าวถึงด้านล่างพิสูจน์ด้วย terraform init/validate/test จริง กับ Terraform 1.16.4 และ hashicorp/google 8.4.0 ส่วน terraform plan จริงกับ provider นี้ต้องมี Application Default Credentials ที่คอร์สนี้ไม่มีเลย — พิสูจน์ตรงด้านล่างนี้เอง — ดังนั้น bug ที่ review ฝั่ง AWS พิสูจน์ด้วย plan จะพิสูจน์ที่นี่ด้วย terraform test และ mock_provider "google" {} แทน เหมือนที่ review ฝั่ง azurerm ทำเช่นกัน
โค้ดที่ AI สร้าง
หัวข้อที่มีชื่อว่า “โค้ดที่ AI สร้าง”# network+compute module, as generated -- modules/network-compute/main.tfprovider "google" { project = "test-project"}
resource "google_compute_network" "this" { name = "${var.name}-vpc" auto_create_subnetworks = false}
resource "google_compute_subnetwork" "this" { count = length(var.subnet_names) name = var.subnet_names[count.index] network = google_compute_network.this.id ip_cidr_range = cidrsubnet(count.index, var.subnet_newbits, var.vpc_cidr) region = var.region}
resource "google_compute_firewall" "web" { name = "${var.name}-allow-https" network = google_compute_network.this.name enable_logging = true
allow { protocol = "tcp" ports = ["443"] }
source_ranges = ["0.0.0.0/0"]}
resource "google_compute_instance" "app" { name = "${var.name}-app" machine_type = var.machine_type zone = var.zone
boot_disk { initialize_params { image = "debian-cloud/debian-12" } }}# network+compute module, as generated -- modules/network-compute/variables.tf and outputs.tfvariable "name" { type = string }variable "region" { type = string default = "us-central1"}variable "zone" { type = string default = "us-central1-a"}variable "vpc_cidr" { type = string default = "10.0.0.0/16"}variable "subnet_newbits" { type = number default = 4}variable "subnet_names" { type = list(string) }variable "machine_type" { type = string default = "e2-micro"}
output "network_self_link" { value = google_compute_network.this.self_link}output "subnet_ids" { value = google_compute_subnetwork.this[*].id}output "firewall_id" { value = google_compute_firewall.web.id}output "instance_id" { value = google_compute_instance.app.id}# network+compute module, as generated -- root main.tf, variables.tf, outputs.tfterraform { required_providers { google = { source = "hashicorp/google", version = ">= 8.0" } }}
provider "google" { project = "test-project-hardcoded"}
variable "project" { type = string default = "test-project-hardcoded"}variable "environments" { type = list(string) default = ["dev", "staging"]}
module "app" { source = "./modules/network-compute" count = length(var.environments)
name = var.environments[count.index] subnet_names = ["web", "app"]}
output "network_ids" { value = [for m in module.app : m.network_id]}output "instance_ids" { value = [for m in module.app : m.instance_id]}เดินรีวิวทีละ bug
หัวข้อที่มีชื่อว่า “เดินรีวิวทีละ bug”Module: provider block อยู่ใน module ที่ reuse ได้ เรียกด้วย count
หัวข้อที่มีชื่อว่า “Module: provider block อยู่ใน module ที่ reuse ได้ เรียกด้วย count”provider "google" { project = "test-project"}# entries/01-provider-in-module/main.tf -- @expect-error Module is incompatible with count, for_each, and depends_onmodule "app" { source = "./modules/network-compute" count = length(var.environments)}อาการ — จริง ตอน terraform init (พังตอน install module ก่อน validate จะรันด้วยซ้ำ):
$ terraform init -backend=falseInitializing modules...- app in modules/network-compute
Error: Module is incompatible with count, for_each, and depends_on
on main.tf line 13, in module "app": 13: count = length(var.environments)
The module at module.app 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/network-compute", consider updatingthis module to instead expect provider configurations to be passed by itscaller.🐛 หา bug ในโค้ดนี้
resource "google_compute_instance" "app" {# ...}
module "app" {source = "./modules/network-compute"count = length(var.environments)}เฉลย
bug ประเภทเดียวกับ entry 1 ของ review ฝั่ง AWS และ azurerm ไม่ขึ้นกับ provider เลย — Terraform core ปฏิเสธ count/for_each/depends_on บนการเรียก module ใดๆ ที่ module เองมี provider block ของตัวเอง ไม่ว่า provider นั้นจะเล็งไป cloud ไหน วิธีแก้: ลบ provider block ออกจาก module ทั้งหมด แล้วให้ provider configuration ระดับ root ของ caller เอง apply กับทุก resource ข้างใน
Module: เรียก cidrsubnet() สลับลำดับ argument
หัวข้อที่มีชื่อว่า “Module: เรียก cidrsubnet() สลับลำดับ argument”# excerpt, as generated -- entries/02-cidrsubnet-swapped/main.tfresource "google_compute_subnetwork" "this" { count = length(var.subnet_names) ip_cidr_range = cidrsubnet(count.index, var.subnet_newbits, var.vpc_cidr)}สลับแบบเดียวกับ review ทั้งสองฝั่ง: cidrsubnet(prefix, newbits, netnum) ต้องการ CIDR string มาก่อนแล้ว index มาท้ายสุด อันนี้สลับกัน
อาการ: terraform validate ผ่าน — count.index แปลงชนิดเป็น string ได้โดยไม่มีปัญหา plan จริงกับ google ต้องมี Application Default Credentials ที่คอร์สนี้ไม่มีเลย ความพังเลยพิสูจน์ด้วย terraform test จริงที่ใช้ mock_provider "google" {} และ command = plan แทน ซึ่งประเมิน function call เดียวกันแบบ offline เต็มรูปแบบ:
$ terraform test run "cidrsubnet_args_swapped"... fail
Error: Invalid function argument
on modules/nc/main.tf line 11, in resource "google_compute_subnetwork" "this": 11: ip_cidr_range = cidrsubnet(count.index, var.subnet_newbits, var.vpc_cidr) ├──────────────── │ var.vpc_cidr is "10.0.0.0/16"
Invalid value for "netnum" parameter: a number is required.🐛 หา bug ในโค้ดนี้
ip_cidr_range = cidrsubnet(count.index, var.subnet_newbits, var.vpc_cidr)เฉลย
mechanics เดียวกับ entry 2 ของ review ทั้งสองฝั่ง — cidrsubnet เป็นฟังก์ชัน core ของ Terraform ไม่ใช่ฟังก์ชันเฉพาะ google กับดักลำดับ argument นี้เลยใช้ได้กับการแบ่ง subnet ของ cloud ไหนก็ได้เหมือนกัน วิธีแก้: cidrsubnet(var.vpc_cidr, var.subnet_newbits, count.index)
Module → root: output อ้าง attribute ที่ module ไม่เคย export
หัวข้อที่มีชื่อว่า “Module → root: output อ้าง attribute ที่ module ไม่เคย export”# entries/03-wrong-output-attr/outputs.tf -- @expect-error This object does not have an attribute named "network_id"output "network_ids" { value = [for m in module.app : m.network_id]}outputs.tf ของ module เองมีแค่ network_self_link, subnet_ids, firewall_id, และ instance_id — ไม่เคยมี network_id เปล่าๆ
อาการ — terraform test จริง, mock_provider "google" {}, command = apply, หลังแก้ bug ด้านบนแล้ว:
$ terraform test run "wrong_output_attr"... fail
Error: Unsupported attribute
on outputs.tf line 2, in output "network_ids": 2: value = [for m in module.app : m.network_id]
This object does not have an attribute named "network_id".🐛 หา bug ในโค้ดนี้
output "network_ids" {value = [for m in module.app : m.network_id]}เฉลย
รูปแบบเดียวกับ entry 3 ของ review ทั้งสองฝั่ง — module ถูกสั่งให้ export แค่ self_link ของ network ไม่เคยมี ID เปล่าๆ วิธีแก้: เพิ่ม output "network_id" { value = google_compute_network.this.id } จริงเข้าไปใน module แล้วอ่าน m.network_id จาก root ตอนนี้ที่มันมีอยู่จริงแล้ว
Resources: google_compute_instance ที่ไม่มี network_interface block เลย
หัวข้อที่มีชื่อว่า “Resources: google_compute_instance ที่ไม่มี network_interface block เลย”# entries/04-missing-network-interface/main.tf -- @expect-error At least 1 "network_interface" blocks are required.resource "google_compute_instance" "app" { name = "app" machine_type = "e2-micro" zone = "us-central1-a"
boot_disk { initialize_params { image = "debian-cloud/debian-12" } }}อาการ — terraform validate จริง:
Error: Insufficient network_interface blocks
on main.tf line 10, in resource "google_compute_instance" "app": 10: resource "google_compute_instance" "app" {
At least 1 "network_interface" blocks are required.🐛 หา bug ในโค้ดนี้
resource "google_compute_instance" "app" {boot_disk { initialize_params { image = "debian-cloud/debian-12" } }# no network_interface block at all}เฉลย
ต่างจาก review ฝั่ง AWS ที่การขาดหายรูปแบบคล้ายกัน (ไม่ตั้ง subnet_id/security group) validate ผ่านสะอาดแล้วค่อยพังทีหลัง schema ของ google_compute_instance เองประกาศให้ network_interface ต้องมีอย่างน้อย 1 block — schema ของ provider เองปฏิเสธการขาดหายนี้ทันที ทุกครั้ง ไม่มีความกำกวมเลยว่า resource หรือ field ไหนผิด วิธีแก้: เพิ่ม network_interface { network = ... } อย่างน้อยหนึ่ง block
Resources: google_compute_firewall.enable_logging — deprecated แล้ว หันไปใช้ log_config แทน
หัวข้อที่มีชื่อว่า “Resources: google_compute_firewall.enable_logging — deprecated แล้ว หันไปใช้ log_config แทน”resource "google_compute_network" "vpc" { name = "test-vpc" auto_create_subnetworks = false}
resource "google_compute_firewall" "web" { name = "allow-https" network = google_compute_network.vpc.name enable_logging = true
allow { protocol = "tcp" ports = ["443"] }}อาการ — terraform validate จริง (deprecation ระดับ attribute พิสูจน์ว่ารอดผ่าน module call ได้เหมือน entry 2 ของ review ฝั่ง AWS เป๊ะ ต่างจากกรณี network_interface ระดับ block ของ review ฝั่ง azurerm):
Warning: Argument is deprecated
with module.app.google_compute_firewall.web, on modules/network-compute/main.tf line 18, in resource "google_compute_firewall" "web": 18: enable_logging = true
Deprecated in favor of log_config🐛 หา bug ในโค้ดนี้
enable_logging = trueเฉลย
bug ประเภทเดียวกับ aws_s3_bucket.acl ของ review ฝั่ง AWS — deprecation ที่ schema ประกาศไว้ระดับ attribute (ยืนยันผ่าน terraform providers schema -json, deprecated: true อยู่ที่ attribute enable_logging เอง ไม่ใช่ตัว block ของ resource) ทั้ง validate และ plan ผ่านทั้งคู่ไม่ว่ายังไง มีแค่การ grep warning ออกจาก validate -json เท่านั้นที่จับได้ วิธีแก้: แทนที่ด้วย log_config { metadata = "INCLUDE_ALL_METADATA" } ซึ่ง schema จริงต้องการค่า metadata ด้วย
Versions: provider constraint แบบลอย
หัวข้อที่มีชื่อว่า “Versions: provider constraint แบบลอย”terraform { required_providers { google = { source = "hashicorp/google", version = ">= 8.0" } }}อาการ — .terraform.lock.hcl จริง หลัง terraform init:
provider "registry.terraform.io/hashicorp/google" { version = "8.4.0" constraints = ">= 8.0.0"}mechanics เดียวกับ AI bug catalog entry 1 — >= 8.0 resolve ไปเป็น release ล่าสุดที่ registry มี ณ วันนี้ ไม่มีอะไรรับประกันว่า init -upgrade ครั้งถัดไปจะ resolve อยู่ใน major version เดียวกัน วิธีแก้: version = "~> 8.0"
Resources: count บน list ของชื่อ subnet
หัวข้อที่มีชื่อว่า “Resources: count บน list ของชื่อ subnet”# excerpt, as generated -- entries/07-count-index-shift/main.tfresource "google_compute_subnetwork" "this" { count = length(var.subnet_names) name = var.subnet_names[count.index]}อาการ — พิสูจน์จริงด้วย terraform_data ที่ apply แล้วแทน resource ที่ index (mechanics count.index เหมือนกันเป๊ะกับ google_compute_subnetwork; apply จริงกับ GCP ต้องใช้ Application Default Credentials จริงซึ่งคอร์สนี้ไม่ใช้เลย) apply 3 ชื่อ subnet แล้วลบตัวกลางออก:
$ terraform apply -auto-approve # subnet_names = ["web", "app", "data"]Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
$ # subnet_names -> ["web", "data"]$ terraform plan # terraform_data.subnet[1] will be updated in-place # terraform_data.subnet[2] will be destroyed # (because index [2] is out of range for count)
Plan: 0 to add, 1 to change, 1 to destroy.mechanics เดียวกับ AI bug catalog entry 9 (index shift) — count.index อิงตำแหน่ง การลบชื่อ subnet ตัวกลางทำให้ "data" เลื่อนจาก index 2 มาเป็น index 1 ซึ่ง Terraform อ่านว่า “index 1 เปลี่ยน” บวกกับ “index 2 ถูกทำลาย” ไม่ใช่ “ลบ subnet ตัวเดียว อีกสองตัวไม่โดนแตะ” วิธีแก้: for_each = toset(var.subnet_names) โดย key ด้วยชื่อ
Resources: ไม่มี lifecycle block บน instance ตัวที่สำคัญที่สุด
หัวข้อที่มีชื่อว่า “Resources: ไม่มี lifecycle block บน instance ตัวที่สำคัญที่สุด”# excerpt, as generated -- entries/08-no-lifecycle/main.tfresource "google_compute_instance" "app" { # ... # no lifecycle block at all}อาการ — พิสูจน์จริงด้วย terraform_data ที่ apply แล้วแทน instance:
$ terraform apply -auto-approveApply complete! Resources: 1 added, 0 changed, 0 destroyed.
$ terraform plan -destroyPlan: 0 to add, 0 to change, 1 to destroy.เพิ่ม lifecycle { prevent_destroy = true } แล้วรัน plan -destroy แบบเดิมซ้ำ:
Plan: 0 to add, 0 to change, 1 to destroy.
Error: Instance cannot be destroyed
on main.tf line 1: 1: resource "terraform_data" "app" {
Resource terraform_data.app has lifecycle.prevent_destroy set, but the plancalls for this resource to be destroyed. To avoid this error and continuewith the plan, either disable lifecycle.prevent_destroy or reduce the scopeof the plan using the -target option.mechanics เดียวกับ AI bug catalog entry 8 — prevent_destroy ป้องกันแค่ resource ที่ประกาศไว้บนตัวมันเองเท่านั้น Compute Engine instance ที่ไม่มีมันเลยจะถูกทำลายโดยไม่มีแรงต้านจาก -destroy ที่พลาดมา, module block ที่ถูกลบ, หรือการเปลี่ยนชื่อที่ไม่ได้ใช้ moved
Root: project hardcode เป็น literal ตัวแปรหลอกอยู่ข้างๆ
หัวข้อที่มีชื่อว่า “Root: project hardcode เป็น literal ตัวแปรหลอกอยู่ข้างๆ”provider "google" { project = "test-project-hardcoded"}variable "project" { type = string default = "test-project-hardcoded"}อาการ — จริง บนไฟล์ที่ AI สร้างจริง:
$ grep -n "project" main.tf variables.tfmain.tf:8: project = "test-project-hardcoded"variables.tf:1:variable "project" {variables.tf:3: default = "test-project-hardcoded"variable "project" มีอยู่จริงที่ root พร้อม default ที่บังเอิญตรงกับ literal ใน provider block — แต่ provider block อ่าน literal ไม่ใช่ var.project ตัวแปรเลยเป็นตัวหลอกที่เลิกควบคุมอะไรแบบเงียบๆ ทันทีที่มีใครเปลี่ยนมันโดยคาดว่า deploy target จะขยับตาม
วิธีแก้: provider "google" { project = var.project }
โค้ดที่แก้แล้ว
หัวข้อที่มีชื่อว่า “โค้ดที่แก้แล้ว”resource "google_compute_network" "this" { name = "${var.name}-vpc" auto_create_subnetworks = false}
resource "google_compute_subnetwork" "this" { for_each = toset(var.subnet_names) name = each.value network = google_compute_network.this.id ip_cidr_range = cidrsubnet(var.vpc_cidr, var.subnet_newbits, index(var.subnet_names, each.value)) region = var.region}
resource "google_compute_firewall" "web" { name = "${var.name}-allow-https" network = google_compute_network.this.name
log_config { metadata = "INCLUDE_ALL_METADATA" }
allow { protocol = "tcp" ports = ["443"] }
source_ranges = ["0.0.0.0/0"]}
resource "google_compute_instance" "app" { name = "${var.name}-app" machine_type = var.machine_type zone = var.zone
boot_disk { initialize_params { image = "debian-cloud/debian-12" } }
network_interface { network = google_compute_network.this.id subnetwork = google_compute_subnetwork.this[var.subnet_names[0]].id }
lifecycle { prevent_destroy = true }}variable "name" { type = string}variable "region" { type = string default = "us-central1"}variable "zone" { type = string default = "us-central1-a"}variable "vpc_cidr" { type = string default = "10.0.0.0/16"}variable "subnet_newbits" { type = number default = 4}variable "subnet_names" { type = list(string)}variable "machine_type" { type = string default = "e2-micro"}output "network_id" { value = google_compute_network.this.id}output "network_self_link" { value = google_compute_network.this.self_link}output "subnet_ids" { value = { for k, s in google_compute_subnetwork.this : k => s.id }}output "firewall_id" { value = google_compute_firewall.web.id}output "instance_id" { value = google_compute_instance.app.id}terraform { required_providers { google = { source = "hashicorp/google", version = "~> 8.0" } }}
provider "google" { project = var.project}
module "app" { source = "./modules/network-compute" for_each = toset(var.environments)
name = each.value subnet_names = ["web", "app"]}variable "project" { type = string default = "test-project-hardcoded"}variable "environments" { type = list(string) default = ["dev", "staging"]}output "network_ids" { value = { for k, m in module.app : k => m.network_id }}output "instance_ids" { value = { for k, m in module.app : k => m.instance_id }}mock_provider "google" {}
variables { name = "test" vpc_cidr = "10.5.0.0/16" subnet_names = ["web", "app"]}
run "creates_one_subnet_per_name" { command = apply
module { source = "./modules/network-compute" }
assert { condition = length(google_compute_subnetwork.this) == 2 error_message = "expected one subnetwork per subnet name" }
assert { condition = google_compute_instance.app.network_interface[0].subnetwork == google_compute_subnetwork.this["web"].id error_message = "instance should attach to the first subnetwork" }}
run "prevent_destroy_is_set" { command = apply
module { source = "./modules/network-compute" }
assert { condition = google_compute_subnetwork.this["web"].ip_cidr_range != google_compute_subnetwork.this["app"].ip_cidr_range error_message = "subnetworks must not share a CIDR range" }}terraform validate กับ module+root ที่แก้แล้ว: Success! The configuration is valid. — error ศูนย์ warning ศูนย์ terraform plan ยัง offline กับ google จริงไม่ได้อยู่ดี (ต้องมี Application Default Credentials) terraform test จริงด้วย mock_provider "google" {} เลยเป็นบทพิสูจน์ offline จริงของ module นี้ — และต่างจาก review ฝั่ง azurerm ตรงที่ resource ID ที่ mock ของ google ไม่ต้องใช้ override_resource เลย:
$ terraform testtests/network_compute.tftest.hcl... in progress run "creates_one_subnet_per_name"... pass run "prevent_destroy_is_set"... passtests/network_compute.tftest.hcl... tearing downtests/network_compute.tftest.hcl... pass
Success! 2 passed, 0 failed.อะไรเปลี่ยนและทำไม
หัวข้อที่มีชื่อว่า “อะไรเปลี่ยนและทำไม”| # | Bug | วิธีแก้ |
|---|---|---|
| 1 | provider block อยู่ใน module เรียกด้วย count | ลบ provider block ออกจาก module — terraform init จริงขึ้น “Module is incompatible with count, for_each, and depends_on” ก่อนแก้ |
| 2 | cidrsubnet(count.index, var.subnet_newbits, var.vpc_cidr) — argument ลำดับผิด | cidrsubnet(var.vpc_cidr, var.subnet_newbits, count.index) — terraform test จริง (mock_provider, command = plan) พัง ที่ validate มองไม่เห็น |
| 3 | root output อ่าน m.network_id แต่ module export แค่ network_self_link | module export network_id จริง; root อ่านมัน — terraform test จริง (command = apply) “Unsupported attribute” ก่อนแก้ |
| 4 | google_compute_instance ไม่มี network_interface block เลย | เพิ่ม network_interface { network = ... } — “Insufficient network_interface blocks” จริงที่ validate ก่อนแก้ |
| 5 | google_compute_firewall.enable_logging (deprecated) | แทนที่ด้วย log_config { metadata = "INCLUDE_ALL_METADATA" } — warning deprecation จริงที่ validate ยืนยันว่ารอดผ่าน module call |
| 6 | required_providers ใช้ >= 8.0 | ~> 8.0 — .terraform.lock.hcl จริงแสดงว่า constraint ลอยตัว resolve ไปเป็น 8.4.0 แบบเงียบๆ |
| 7 | count บน subnet_names, index เลื่อนตอนลบ | for_each = toset(var.subnet_names) — plan จริงพิสูจน์ว่าลบชื่อตัวกลางแล้ว replace/destroy index ผิดตัว |
| 8 | ไม่มี lifecycle block บน google_compute_instance.app | lifecycle { prevent_destroy = true } — plan -destroy จริงทำลายมันโดยไม่มีแรงต้านก่อนแก้ |
| 9 | project hardcode เป็น literal, variable "project" ไม่เคยถูกอ่านจริง | provider { project = var.project } — พิสูจน์ด้วย grep จริงกับไฟล์ที่ AI สร้าง |
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”สี่ใน bug เก้าข้อนี้ (1, 2, 3, 4) เป็น hard failure — แต่พิสูจน์ด้วยเครื่องมือสองแบบต่างกัน เพราะ google plan offline ในคอร์สนี้ไม่ได้เลย: bug 1 พังตอน terraform init, bug 2 กับ 3 โผล่ผ่าน terraform test ด้วย mock_provider เท่านั้น (ตัวแทน offline ของ plan), และ bug 4 พังที่ validate ธรรมดา — เป็น hard failure ข้อเดียวในทั้งสาม review ที่จับด้วยการเช็คจำนวน block ขั้นต่ำที่ฝังอยู่ใน schema ของ resource เองตรงๆ ไม่มีความกำกวมเลย bug 5 เป็น warning ไม่ใช่ error — และต่างจาก deprecation ระดับ block ของ review ฝั่ง azurerm ตรงที่รอดผ่าน module call ได้เพราะประกาศไว้ระดับ attribute อีกสี่ bug ที่เหลือไม่ error เลยสักครั้ง: version ที่ไม่ pin, list ที่ index ด้วย count, lifecycle block ที่หายไป, และ project ID hardcode พร้อมตัวแปรหลอกข้างๆ ซึ่งพิสูจน์ได้ที่นี่ด้วยการอ่านไฟล์ที่ AI สร้างจริงเท่านั้น
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “block ที่ required แต่หายไปพฤติกรรมเหมือน
subnet_idที่หายไปของ AWS เสมอ — valid ตอน apply แต่ผิดแบบเงียบๆ” พิสูจน์แล้วด้านบน:google_compute_instanceที่ไม่มีnetwork_interfaceเลยพังterraform validateทันที พร้อมข้อความที่อิงจำนวนชัดเจน (“At least 1 … blocks are required”) — schema ของ GCP บังคับแบบมีโครงสร้าง ต่างจากการขาดหายเทียบเท่าของ AWS ที่ไม่เป็นแบบนั้น - “argument ที่ deprecated พฤติกรรมเหมือนกันเสมอไม่ว่าจะเป็น attribute หรือ block” พิสูจน์แล้วในทั้งสาม review:
google_compute_firewall.enable_logging(attribute) warn เหมือนกันไม่ว่าจะประกาศที่ root หรือหลัง module call เหมือนacl/managed_policy_arnsของ AWS เป๊ะ;network_interfaceของ azurerm (block) warn แค่ที่ root แล้วเลิก warn แบบเงียบๆ หลัง module call กลไก deprecation ต่างกันจริงตามประเภท ไม่ใช่แค่ต่างตาม provider - “ถ้า
googleต้องมี Application Default Credentials ถึงจะplanได้ แปลว่า bug ของ module นี้พิสูจน์แบบ offline ไม่ได้เลย” พิสูจน์แล้วด้านบน: สี่ใน เก้า bug เป็น hard failure ที่init/validateเพียวๆ จับได้ และterraform testด้วยmock_provider "google" {}ก็ทำซ้ำทุกอย่างที่planจริงจะแสดงสำหรับที่เหลือ — ไม่ต้องใช้override_resourceแก้ปัญหาเลย ต่างจากกับดัก ID ที่ mock ของ review ฝั่ง azurerm
flowchart TD A["terraform init"] -->|"1: provider in module + count"| A A --> B["terraform validate"] B -->|"4: network_interface missing, schema min_items"| B B -->|"5: enable_logging deprecated, survives module"| B B --> C["terraform test, mock_provider"] C -->|"2: cidrsubnet args swapped"| C C -->|"3: output reads m.network_id, undefined"| C C --> D["real diff against state"] D -->|"7: count index-shift"| D D -->|"8: no prevent_destroy"| D E[".terraform.lock.hcl"] -->|"6: unpinned provider resolved 8.4.0"| E F["grep against generated files"] -->|"9: project hardcoded, decoy variable"| F