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

Worked review 1: network + compute module

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 ทำเช่นกัน

# network+compute module, as generated -- modules/network-compute/main.tf
provider "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.tf
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_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.tf
terraform {
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]
}
entries/01-provider-in-module/modules/network-compute/main.tf
provider "google" {
project = "test-project"
}
# entries/01-provider-in-module/main.tf -- @expect-error Module is incompatible with count, for_each, and depends_on
module "app" {
source = "./modules/network-compute"
count = length(var.environments)
}

อาการ — จริง ตอน terraform init (พังตอน install module ก่อน validate จะรันด้วยซ้ำ):

$ terraform init -backend=false
Initializing 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 local
provider 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 updating
this module to instead expect provider configurations to be passed by its
caller.

🐛 หา 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 ข้างใน

# excerpt, as generated -- entries/02-cidrsubnet-swapped/main.tf
resource "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)

# 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 ตอนนี้ที่มันมีอยู่จริงแล้ว

# 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 แทน”
entries/05-deprecated-enable-logging/main.tf
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 ด้วย

entries/06-unpinned-provider/main.tf
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"

# excerpt, as generated -- entries/07-count-index-shift/main.tf
resource "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 ด้วยชื่อ

# excerpt, as generated -- entries/08-no-lifecycle/main.tf
resource "google_compute_instance" "app" {
# ...
# no lifecycle block at all
}

อาการ — พิสูจน์จริงด้วย terraform_data ที่ apply แล้วแทน instance:

$ terraform apply -auto-approve
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
$ terraform plan -destroy
Plan: 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 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.

mechanics เดียวกับ AI bug catalog entry 8 — prevent_destroy ป้องกันแค่ resource ที่ประกาศไว้บนตัวมันเองเท่านั้น Compute Engine instance ที่ไม่มีมันเลยจะถูกทำลายโดยไม่มีแรงต้านจาก -destroy ที่พลาดมา, module block ที่ถูกลบ, หรือการเปลี่ยนชื่อที่ไม่ได้ใช้ moved

entries/09-hardcoded-project/excerpt
provider "google" {
project = "test-project-hardcoded"
}
variable "project" {
type = string
default = "test-project-hardcoded"
}

อาการ — จริง บนไฟล์ที่ AI สร้างจริง:

$ grep -n "project" main.tf variables.tf
main.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 }

modules/network-compute/main.tf
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
}
}
modules/network-compute/variables.tf
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"
}
modules/network-compute/outputs.tf
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
}
main.tf
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"]
}
variables.tf
variable "project" {
type = string
default = "test-project-hardcoded"
}
variable "environments" {
type = list(string)
default = ["dev", "staging"]
}
outputs.tf
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 }
}
tests/network_compute.tftest.hcl
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 test
tests/network_compute.tftest.hcl... in progress
run "creates_one_subnet_per_name"... pass
run "prevent_destroy_is_set"... pass
tests/network_compute.tftest.hcl... tearing down
tests/network_compute.tftest.hcl... pass
Success! 2 passed, 0 failed.
#Bugวิธีแก้
1provider block อยู่ใน module เรียกด้วย countลบ provider block ออกจาก module — terraform init จริงขึ้น “Module is incompatible with count, for_each, and depends_on” ก่อนแก้
2cidrsubnet(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 มองไม่เห็น
3root output อ่าน m.network_id แต่ module export แค่ network_self_linkmodule export network_id จริง; root อ่านมัน — terraform test จริง (command = apply) “Unsupported attribute” ก่อนแก้
4google_compute_instance ไม่มี network_interface block เลยเพิ่ม network_interface { network = ... } — “Insufficient network_interface blocks” จริงที่ validate ก่อนแก้
5google_compute_firewall.enable_logging (deprecated)แทนที่ด้วย log_config { metadata = "INCLUDE_ALL_METADATA" } — warning deprecation จริงที่ validate ยืนยันว่ารอดผ่าน module call
6required_providers ใช้ >= 8.0~> 8.0 — .terraform.lock.hcl จริงแสดงว่า constraint ลอยตัว resolve ไปเป็น 8.4.0 แบบเงียบๆ
7count บน subnet_names, index เลื่อนตอนลบfor_each = toset(var.subnet_names) — plan จริงพิสูจน์ว่าลบชื่อตัวกลางแล้ว replace/destroy index ผิดตัว
8ไม่มี lifecycle block บน google_compute_instance.applifecycle { prevent_destroy = true } — plan -destroy จริงทำลายมันโดยไม่มีแรงต้านก่อนแก้
9project hardcode เป็น literal, variable "project" ไม่เคยถูกอ่านจริงprovider { project = var.project } — พิสูจน์ด้วย grep จริงกับไฟล์ที่ AI สร้าง

สี่ใน 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
แต่ละ bug อยู่ตรงไหนในเส้นทางประเมินของ module นี้
ทำไม error นี้ถึงโผล่ตอน terraform init แทนที่จะเป็น terraform validate?
Error: Module is incompatible with count, for_each, and depends_on
google_compute_instance ที่ไม่มี network_interface block เลย กับ aws_instance ที่ไม่ได้ตั้ง subnet_id/security group ทั้งคู่คือ "ขาด networking wiring" terraform validate จริงแสดงอะไรสำหรับแต่ละแบบ?
ทำไม review ฝั่ง AWS ถึงพิสูจน์ bug argument ลำดับผิดของ cidrsubnet ด้วย terraform plan จริง แต่ review google นี้พิสูจน์ bug เดียวกันด้วย terraform test และ mock_provider แทน?
google_compute_firewall.enable_logging (attribute) และ network_interface ของ azurerm (block) ทั้งคู่ deprecated ประกาศหลัง module call terraform validate จริงแสดงอะไรสำหรับแต่ละแบบ ตาม review ของคอร์สนี้?
google_compute_subnetwork.this ใช้ count = length(var.subnet_names) apply 3 ชื่อ subnet จริง แล้วลบตัวกลางออกจาก list terraform plan จริงแสดงอะไร?
provider "google" block hardcode project = "test-project-hardcoded" เป็น literal ทั้งที่ variable "project" มี default ตรงกันอยู่ เพราะ google plan offline ที่นี่ไม่ได้ แล้วพิสูจน์ยังไง?