Worked review 1: network + compute module
Feature ที่ต้องทำ
หัวข้อที่มีชื่อว่า “Feature ที่ต้องทำ”Prompt คือ: “เขียน Terraform module ที่ reuse ได้สำหรับ VPC พร้อม public subnets, security group, และ EC2 instance แล้วเรียกใช้ครั้งละหนึ่ง environment” สิ่งที่ตามมาคือ draft แรกแบบที่ AI assistant ส่งกลับมาให้กับ prompt แบบนี้พอดี — ทุก block ดูสมเหตุสมผลเป็น HCL รายตัว resource type เป็นของจริง ชื่อ argument ส่วนใหญ่ก็เป็นของจริง อ่านตามที่ review checklist แนะนำ: provider และ version ก่อน แล้วตามด้วย backend แล้วค่อย resources กับ lifecycle แล้วจึงถึงขอบเขตของ module เอง มี bug จริง 8 ข้อ; ดู AI bug catalog สำหรับ pattern ทั่วไปเบื้องหลังแต่ละข้อ — review นี้ทำซ้ำ catalog entry 1, 2, 3, 9, 10, และ 17 อยู่ใน module เดียวกันที่สอดคล้องกัน บวกอีกสองข้อที่เฉพาะกับรูปแบบนี้ ทุกข้อที่กล่าวถึงด้านล่างพิสูจน์ด้วย terraform init/validate/plan/test จริง กับ Terraform 1.16.4 และ hashicorp/aws 6.66.0 พร้อม mock credentials (access_key = "mock", skip_credentials_validation = true) — ไม่เคยเรียก AWS จริงเลย
โค้ดที่ AI สร้าง
หัวข้อที่มีชื่อว่า “โค้ดที่ AI สร้าง”# network+compute module, as generated -- modules/network-compute/main.tfprovider "aws" { region = "us-east-1"}
resource "aws_vpc" "this" { cidr_block = var.vpc_cidr enable_dns_support = true enable_dns_hostnames = true tags = { Name = "${var.name}-vpc" }}
resource "aws_internet_gateway" "this" { vpc_id = aws_vpc.this.id tags = { Name = "${var.name}-igw" }}
resource "aws_subnet" "public" { count = length(var.availability_zones) vpc_id = aws_vpc.this.id cidr_block = cidrsubnet(count.index, var.subnet_newbits, var.vpc_cidr) availability_zone = var.availability_zones[count.index] map_public_ip_on_launch = true tags = { Name = "${var.name}-public-${count.index}" }}
resource "aws_route_table" "public" { vpc_id = aws_vpc.this.id route { cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.this.id }}
resource "aws_route_table_association" "public" { count = length(var.availability_zones) subnet_id = aws_subnet.public[count.index].id route_table_id = aws_route_table.public.id}
resource "aws_security_group" "web" { name = "${var.name}-web-sg" description = "Allow web traffic" vpc_id = aws_vpc.this.id
ingress { from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } ingress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] }}
resource "aws_network_interface" "app" { subnet_id = aws_subnet.public[0].id security_groups = [aws_security_group.web.id]}
resource "aws_instance" "app" { ami = var.ami_id instance_type = var.instance_type
network_interface { network_interface_id = aws_network_interface.app.id device_index = 0 }
tags = { Name = "${var.name}-app" }}# network+compute module, as generated -- modules/network-compute/variables.tf and outputs.tfvariable "name" { type = string }variable "vpc_cidr" { type = string default = "10.0.0.0/16"}variable "subnet_newbits" { type = number default = 4}variable "availability_zones" { type = list(string) }variable "ami_id" { type = string }variable "instance_type" { type = string default = "t3.micro"}
output "vpc_arn" { value = aws_vpc.this.arn}output "public_subnet_ids" { value = aws_subnet.public[*].id}output "web_security_group_id" { value = aws_security_group.web.id}output "instance_id" { value = aws_instance.app.id}# network+compute module, as generated -- root main.tf, variables.tf, outputs.tfterraform { required_providers { aws = { source = "hashicorp/aws", version = ">= 6.0" } }}
provider "aws" { region = "us-east-1" access_key = "mock" secret_key = "mock" skip_credentials_validation = true skip_requesting_account_id = true skip_metadata_api_check = true skip_region_validation = true}
variable "region" { type = string default = "us-east-1"}variable "environments" { type = list(string) default = ["dev", "staging"]}variable "ami_id" { type = string default = "ami-0abcdef1234567890"}
module "app" { source = "./modules/network-compute" count = length(var.environments)
name = var.environments[count.index] vpc_cidr = "10.${count.index}.0.0/16" availability_zones = ["us-east-1a", "us-east-1b"] ami_id = var.ami_id}
output "vpc_ids" { value = [for m in module.app : m.vpc_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 "aws" { region = "us-east-1"}# 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 (พังก่อน validate จะรันด้วยซ้ำ ตอน install module):
$ 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 19, in module "app": 19: 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 "aws_instance" "app" {# ...}
module "app" {source = "./modules/network-compute"count = length(var.environments)}เฉลย
module เดี่ยวๆ validate ผ่านสบาย — provider "aws" { } ข้างใน module เป็น HCL ที่ถูกต้องสมบูรณ์ในตัวมันเอง ความพังปรากฏก็ต่อเมื่อ caller ต้องการ count, for_each, หรือ depends_on บนการเรียก module — ซึ่งตรงกับสิ่งที่ “module เดียว เรียกครั้งละ environment” ต้องการพอดีในขั้นถัดไป มันโผล่ตอน terraform init ขั้น install module นี่เอง ก่อนที่ validate จะได้มีโอกาสรันด้วยซ้ำ วิธีแก้: ลบ provider block ออกจาก module ทั้งหมด แล้วให้ provider configuration ระดับ root ของ caller เอง apply กับทุก resource ข้างใน — module ที่ reuse ได้ไม่ควรมี provider configuration เป็นของตัวเองเลย
Module: เรียก cidrsubnet() สลับลำดับ argument
หัวข้อที่มีชื่อว่า “Module: เรียก cidrsubnet() สลับลำดับ argument”# excerpt, as generated -- entries/02-cidrsubnet-swapped/main.tfresource "aws_subnet" "public" { count = length(var.availability_zones) cidr_block = cidrsubnet(count.index, var.subnet_newbits, var.vpc_cidr) # ...}signature จริงตาม เอกสารฟังก์ชัน cidrsubnet คือ cidrsubnet(prefix, newbits, netnum) — prefix ต้องเป็น CIDR block ตั้งต้น netnum คือ index ของ subnet ที่เรียกแบบนี้คือ cidrsubnet(count.index, var.subnet_newbits, var.vpc_cidr) — เอา count.index (ตัวเลข) ไปวางตรงตำแหน่งที่ควรเป็น CIDR string และเอา CIDR string ไปวางตรงตำแหน่งที่ควรเป็น index
อาการ: terraform validate ผ่าน — count.index แปลงชนิดเป็น string ได้โดยไม่มีปัญหา type checker แบบ static เลยไม่เห็นอะไรผิด ความพังจริงปรากฏก็ต่อเมื่อ plan ประเมินฟังก์ชันจริงๆ เท่านั้น:
$ terraform planError: Invalid function argument
on modules/network-compute/main.tf line 16, in resource "aws_subnet" "public": 16: cidr_block = 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 ในโค้ดนี้
cidr_block = cidrsubnet(count.index, var.subnet_newbits, var.vpc_cidr)เฉลย
เป็นกับดักประเภทเดียวกับ entry 13 ใน AI bug catalog (argument ที่หลอนขึ้นมา) แต่แนบเนียนกว่าอีกชั้น: validate เป็นการเช็คแบบ static ระดับ schema เท่านั้น และระบบ type ของ Terraform แปลงตัวเลขเป็น string ได้อย่างสบายในขั้นนี้ — count.index “พอดี” กับตำแหน่งที่ parameter prefix ต้องการ string เลยทำให้ validate ไม่รายงานอะไรเลย ฟังก์ชันจะถูกเรียกจริงพร้อมค่าจริงก็ตอน plan เท่านั้น วิธีแก้คือจัดลำดับ argument ทั้งสามให้ตรงกับ signature: 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 "vpc_id"output "vpc_ids" { value = [for m in module.app : m.vpc_id]}outputs.tf ของ module เองมีแค่ vpc_arn, public_subnet_ids, web_security_group_id, และ instance_id — ไม่เคยมี vpc_id
อาการ — terraform plan จริง หลังแก้ 2 bug ด้านบนแล้ว (plan คำนวณไปจนถึง 20 resource changes ก่อนจะพังตรง output):
Plan: 20 to add, 0 to change, 0 to destroy.
Changes to Outputs: + instance_ids = [ + (known after apply), + (known after apply), ]
Error: Unsupported attribute
on outputs.tf line 2, in output "vpc_ids": 2: value = [for m in module.app : m.vpc_id]
This object does not have an attribute named "vpc_id".🐛 หา bug ในโค้ดนี้
output "vpc_ids" {value = [for m in module.app : m.vpc_id]}เฉลย
ไม่มีอะไรผิดกับ syntax การอ้างอิง module.app : m.vpc_id เลย — มันคือวิธีอ่าน output จาก module instance ที่ถูกต้องเป๊ะ bug อยู่ชั้นถัดลงไป: module ไม่เคยถูกสั่งให้ export VPC ID เปล่าๆ มีแค่ ARN ของมัน (vpc_arn) สังเกตว่า plan ไปถึง Plan: 20 to add แล้วก่อนจะเจอ error นี้ — Terraform plan resource change ทุกตัวก่อน แล้วค่อยประเมิน output expression ทีหลังสุด output ที่พังเลยไม่ได้บล็อกไม่ให้เห็นสิ่งอื่นที่กำลังจะเกิดขึ้น วิธีแก้มีสองส่วน: เพิ่ม output "vpc_id" { value = aws_vpc.this.id } จริงเข้าไปใน module แล้วอ้าง m.vpc_id จาก root ตอนนี้ที่มันมีอยู่จริงแล้ว
Versions: provider constraint แบบลอย
หัวข้อที่มีชื่อว่า “Versions: provider constraint แบบลอย”# excerpt, as generated -- entries/04-unpinned-provider/main.tfterraform { required_providers { aws = { source = "hashicorp/aws", version = ">= 6.0" } }}อาการ — .terraform.lock.hcl จริง หลัง terraform init:
provider "registry.terraform.io/hashicorp/aws" { version = "6.66.0" constraints = ">= 6.0.0"}>= 6.0 resolve ไปเป็น release ล่าสุดที่ registry มี ณ วันนี้ ไม่มีอะไรรับประกันว่า init -upgrade ครั้งถัดไป — บนเครื่องอื่น วันอื่น — จะ resolve อยู่ใน major version เดียวกันแบบที่ ~> 6.0 รับประกัน bug เดียวกับ AI bug catalog entry 1 วิธีแก้: version = "~> 6.0"
Resources: count บน list ของ availability zone
หัวข้อที่มีชื่อว่า “Resources: count บน list ของ availability zone”# excerpt, as generated -- entries/05-count-index-shift/main.tfresource "aws_subnet" "public" { count = length(var.availability_zones) availability_zone = var.availability_zones[count.index]}อาการ — พิสูจน์จริงด้วย terraform_data ที่ apply แล้วแทน resource ที่ index (mechanics ของ count.index เหมือนกันเป๊ะกับ aws_subnet; AWS จริงต้องใช้ credential จริงถึงจะ apply ได้ ซึ่งคอร์สนี้ไม่ใช้เลย) apply 3 AZ แล้วลบตัวกลางออกจาก list:
$ terraform apply -auto-approve # availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
$ # availability_zones -> ["us-east-1a", "us-east-1c"]$ terraform plan # terraform_data.public_subnet[1] will be updated in-place ~ resource "terraform_data" "public_subnet" { ~ input = { ~ availability_zone = "us-east-1b" -> "us-east-1c" } } # terraform_data.public_subnet[2] will be destroyed # (because index [2] is out of range for count)
Plan: 0 to add, 1 to change, 1 to destroy.🐛 หา bug ในโค้ดนี้
resource "aws_subnet" "public" {count = length(var.availability_zones)availability_zone = var.availability_zones[count.index]}เฉลย
count.index อิงตำแหน่งล้วนๆ การลบ AZ ตัวกลางออกทำให้ "us-east-1c" เลื่อนจาก index 2 มาเป็น index 1 — Terraform อ่านว่า “AZ ของ index 1 เปลี่ยน” (update in-place หรือ forced replacement สำหรับ attribute แบบนี้) บวกกับ “index 2 ไม่มีอยู่แล้ว” (destroy) ทั้งที่เจตนาจริงคือ “เลิกใช้ AZ หนึ่งตัว เหลืออีกสองตัวไว้เฉยๆ” mechanics เดียวกับ AI bug catalog entry 10 วิธีแก้: for_each = toset(var.availability_zones) โดย key ด้วยชื่อ AZ เอง ไม่ใช่ตำแหน่ง
Resources: ไม่มี lifecycle block บน instance ตัวที่สำคัญที่สุด
หัวข้อที่มีชื่อว่า “Resources: ไม่มี lifecycle block บน instance ตัวที่สำคัญที่สุด”# excerpt, as generated -- entries/06-no-lifecycle/main.tfresource "aws_instance" "app" { ami = var.ami_id instance_type = var.instance_type # 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.ไม่มีอะไรหยุดมันเลย — -destroy ที่พลาดมา, module block ที่ถูกลบออก, หรือ resource ที่เปลี่ยนชื่อ (ซึ่ง Terraform มองเป็น delete + recreate ถ้าไม่มี moved block) ทำลาย instance นี้ได้โดยไม่มีแรงต้านเลย เพิ่ม 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 9 — prevent_destroy ป้องกันแค่ resource ที่ประกาศไว้บนตัวมันเองเท่านั้น มันเลยต้องถูกประกาศจริงๆ บน resource ที่สำคัญ ถึงจะทำอะไรได้เลย
Root: region hardcode ไว้ใน provider block ตัวแปร region ตายเปล่า
หัวข้อที่มีชื่อว่า “Root: region hardcode ไว้ใน provider block ตัวแปร region ตายเปล่า”# excerpt, as generated -- entries/07-hardcoded-region/main.tfprovider "aws" { region = "us-east-1"}variable "region" { type = string default = "us-east-1"}อาการ — terraform plan -var="region=us-west-2" จริง กับ module+root:
$ terraform plan -var="region=us-west-2" + region = "us-east-1" + region = "us-east-1" + region = "us-east-1"attribute region ที่ plan ของทุก resource ยังอ่านว่า "us-east-1" — ส่งค่าที่ต่างไปเลยให้ var.region ไม่เปลี่ยนอะไรเลย เพราะไม่มีจุดไหนใน configuration อ่านมันเลย ตัวแปรนี้มีไว้เป็นตัวหลอกเฉยๆ region จริงคือ literal string ที่ hardcode ไว้ใน provider block วิธีแก้: provider "aws" { region = var.region }
Module: network_interface บน aws_instance — deprecated และ warning ไม่รอดผ่านขอบเขต module
หัวข้อที่มีชื่อว่า “Module: network_interface บน aws_instance — deprecated และ warning ไม่รอดผ่านขอบเขต module”# excerpt, as generated -- entries/08-deprecated-network-interface/main.tfresource "aws_instance" "app" { network_interface { network_interface_id = aws_network_interface.app.id device_index = 0 }}อาการ — terraform validate จริง กับ resource ตัวนี้ที่ประกาศตรงใน root module:
Warning: Argument is deprecated
with aws_instance.app, on main.tf line 15, in resource "aws_instance" "app": 15: resource "aws_instance" "app" {
network_interface is deprecated. To specify the primary network interface,use primary_network_interface instead. To attach additional networkinterfaces, use the aws_network_interface_attachment resource.วิธีแก้: อย่าต่อ ENI ผ่าน network_interface block ของ instance เองเลย ตั้ง subnet_id/vpc_security_group_ids ตรงบน aws_instance สำหรับ primary interface ซึ่งทั้งง่ายกว่าและไม่ deprecated ด้วย
โค้ดที่แก้แล้ว
หัวข้อที่มีชื่อว่า “โค้ดที่แก้แล้ว”resource "aws_vpc" "this" { cidr_block = var.vpc_cidr enable_dns_support = true enable_dns_hostnames = true tags = { Name = "${var.name}-vpc" }}
resource "aws_internet_gateway" "this" { vpc_id = aws_vpc.this.id tags = { Name = "${var.name}-igw" }}
resource "aws_subnet" "public" { for_each = toset(var.availability_zones) vpc_id = aws_vpc.this.id cidr_block = cidrsubnet(var.vpc_cidr, var.subnet_newbits, index(var.availability_zones, each.value)) availability_zone = each.value map_public_ip_on_launch = true tags = { Name = "${var.name}-public-${each.value}" }}
resource "aws_route_table" "public" { vpc_id = aws_vpc.this.id route { cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.this.id } tags = { Name = "${var.name}-public-rt" }}
resource "aws_route_table_association" "public" { for_each = aws_subnet.public subnet_id = each.value.id route_table_id = aws_route_table.public.id}
resource "aws_security_group" "web" { name = "${var.name}-web-sg" description = "Allow web traffic" vpc_id = aws_vpc.this.id
ingress { from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } ingress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] }}
resource "aws_instance" "app" { ami = var.ami_id instance_type = var.instance_type subnet_id = aws_subnet.public[var.availability_zones[0]].id vpc_security_group_ids = [aws_security_group.web.id]
tags = { Name = "${var.name}-app" }
lifecycle { prevent_destroy = true }}variable "name" { type = string}variable "vpc_cidr" { type = string default = "10.0.0.0/16"}variable "subnet_newbits" { type = number default = 4}variable "availability_zones" { type = list(string)}variable "ami_id" { type = string}variable "instance_type" { type = string default = "t3.micro"}output "vpc_id" { value = aws_vpc.this.id}output "vpc_arn" { value = aws_vpc.this.arn}output "public_subnet_ids" { value = [for s in aws_subnet.public : s.id]}output "web_security_group_id" { value = aws_security_group.web.id}output "instance_id" { value = aws_instance.app.id}terraform { required_providers { aws = { source = "hashicorp/aws", version = "~> 6.0" } }}
provider "aws" { region = var.region access_key = "mock" secret_key = "mock" skip_credentials_validation = true skip_requesting_account_id = true skip_metadata_api_check = true skip_region_validation = true}
module "app" { source = "./modules/network-compute" for_each = toset(var.environments)
name = each.value vpc_cidr = "10.${index(var.environments, each.value)}.0.0/16" availability_zones = var.availability_zones ami_id = var.ami_id}variable "region" { type = string default = "us-east-1"}variable "environments" { type = list(string) default = ["dev", "staging"]}variable "availability_zones" { type = list(string) default = ["us-east-1a", "us-east-1b"]}variable "ami_id" { type = string default = "ami-0abcdef1234567890"}output "vpc_ids" { value = { for k, m in module.app : k => m.vpc_id }}output "instance_ids" { value = { for k, m in module.app : k => m.instance_id }}mock_provider "aws" {}
variables { name = "test" vpc_cidr = "10.5.0.0/16" availability_zones = ["us-east-1a", "us-east-1b"] ami_id = "ami-0abcdef1234567890"}
run "creates_one_subnet_per_az" { command = apply
module { source = "./modules/network-compute" }
assert { condition = length(aws_subnet.public) == 2 error_message = "expected one subnet per availability zone" }
assert { condition = aws_instance.app.subnet_id == aws_subnet.public["us-east-1a"].id error_message = "instance should launch in the first AZ's subnet" }}
run "prevent_destroy_is_set" { command = apply
module { source = "./modules/network-compute" }
assert { condition = aws_subnet.public["us-east-1a"].cidr_block != aws_subnet.public["us-east-1b"].cidr_block error_message = "subnets must not share a CIDR block" }}terraform validate กับ module+root ที่แก้แล้ว: Success! The configuration is valid. — error ศูนย์ warning ศูนย์ terraform plan (mock AWS credentials): Plan: 18 to add, 0 to change, 0 to destroy. terraform test จริง mock_provider "aws" ทั้งสอง run block กับ module ที่แก้แล้ว:
$ terraform testtests/network_compute.tftest.hcl... in progress run "creates_one_subnet_per_az"... 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) — plan จริงขึ้น “Invalid value for netnum parameter” ที่ validate มองไม่เห็น |
| 3 | root output อ่าน m.vpc_id แต่ module export แค่ vpc_arn | module export vpc_id จริง; root อ่านมัน — จริงขึ้น “This object does not have an attribute named vpc_id” ก่อนแก้ |
| 4 | required_providers ใช้ >= 6.0 | ~> 6.0 — .terraform.lock.hcl จริงแสดงว่า constraint ลอยตัว resolve ไปเป็น 6.66.0 แบบเงียบๆ |
| 5 | count บน availability_zones, index เลื่อนตอนลบ | for_each = toset(var.availability_zones) — plan จริงพิสูจน์ว่าลบ AZ ตัวกลางแล้ว replace/destroy index ผิดตัว |
| 6 | ไม่มี lifecycle block บน aws_instance.app | lifecycle { prevent_destroy = true } — plan -destroy จริงทำลายมันโดยไม่มีแรงต้านก่อนแก้ |
| 7 | provider { region = "us-east-1" } hardcode, var.region ไม่ถูกใช้ | provider { region = var.region } — plan จริงด้วย -var="region=us-west-2" พิสูจน์ว่าตัวแปรตายเปล่า |
| 8 | aws_instance.network_interface block (deprecated) | ลบออก; ตั้ง subnet_id/vpc_security_group_ids ตรงบน instance — warning deprecation จริงที่ root พิสูจน์ว่าหายไปผ่าน module call |
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”สามใน bug แปดข้อนี้ (1, 2, 3) เป็น hard failure — แต่พังกันคนละขั้นตอน: bug 1 พังตอน terraform init (ขั้น install module), bug 2 พังก็ต่อเมื่อ plan ประเมิน function call จริงๆ เท่านั้น (validate มองไม่เห็นเพราะเป็น static type check), และ bug 3 พังตอนท้ายสุดของ plan หลังคำนวณ resource change ครบ 20 ตัวแล้ว อีกห้า bug ที่เหลือไม่ error เลยสักครั้ง: version ที่ไม่ pin, list ที่ index ด้วย count, lifecycle block ที่หายไป, ตัวแปรตายเปล่าที่บัง literal hardcode, และ deprecated block ที่ warn เฉพาะตอนอ่านตรงที่ root เท่านั้น การอ่าน module นี้ให้ถูกต้องแปลว่าต้องรันทั้งบันได — init, validate, plan, diff เทียบกับ state change จริง, test — เพราะแต่ละขั้นจับ subset ที่ต่างกันจริงๆ และไม่มีขั้นไหนจับได้ครบทั้งแปดข้อ
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “ถ้า
terraform validateผ่าน แปลว่า function call ข้างในถูกต้อง” พิสูจน์แล้วด้านบน:cidrsubnet(count.index, var.subnet_newbits, var.vpc_cidr)validate ผ่านสะอาด — type checker แปลงcount.indexเป็น string ได้โดยไม่บ่น — แล้วพังก็ต่อเมื่อplanเรียกฟังก์ชันจริงเท่านั้น - “argument ที่ deprecated จะให้ validate warning เสมอ ไม่ว่าใช้ตรงไหน” พิสูจน์แล้วด้านบน:
aws_instance.network_interfaceblock เดียวกันเป๊ะให้ warning จริงที่ root แต่ warning ศูนย์ข้อผ่าน module call — deprecation ระดับ block ไม่ไหลผ่านขอบเขต module แบบที่ deprecation ระดับ attribute ของ entry 2 (acl,managed_policy_arns) ไหลผ่าน - “output ที่พังบล็อกไม่ให้ plan แสดงอะไรเลย” พิสูจน์แล้วด้านบน:
Plan: 20 to add, 0 to change, 0 to destroyพิมพ์ออกมาครบก่อน error ของ outputvpc_idจะปรากฏ — Terraform plan resource ทุกตัวก่อน แล้วค่อยประเมิน output ทีหลังสุด
flowchart TD A["terraform init"] -->|"1: provider in module + count"| A A --> B["terraform validate"] B --> C["terraform plan"] C -->|"2: cidrsubnet args swapped"| C C -->|"3: output reads m.vpc_id, undefined"| C C --> D["real diff against state"] D -->|"5: count index-shift"| D D -->|"6: no prevent_destroy"| D B -->|"8: network_interface deprecated -- root only"| B E[".terraform.lock.hcl"] -->|"4: unpinned provider resolved 6.66.0"| E F["provider block region"] -->|"7: hardcoded, var.region dead"| F