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

Worked review 1: network + compute module

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 จริงเลย

# network+compute module, as generated -- modules/network-compute/main.tf
provider "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.tf
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_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.tf
terraform {
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]
}
entries/01-provider-in-module/modules/network-compute/main.tf
provider "aws" {
region = "us-east-1"
}
# 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 (พังก่อน validate จะรันด้วยซ้ำ ตอน install module):

$ 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 19, in module "app":
19: 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 "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 เป็นของตัวเองเลย

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

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

# excerpt, as generated -- entries/04-unpinned-provider/main.tf
terraform {
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"

# excerpt, as generated -- entries/05-count-index-shift/main.tf
resource "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 เอง ไม่ใช่ตำแหน่ง

# excerpt, as generated -- entries/06-no-lifecycle/main.tf
resource "aws_instance" "app" {
ami = var.ami_id
instance_type = var.instance_type
# 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.

ไม่มีอะไรหยุดมันเลย — -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 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 9 — prevent_destroy ป้องกันแค่ resource ที่ประกาศไว้บนตัวมันเองเท่านั้น มันเลยต้องถูกประกาศจริงๆ บน resource ที่สำคัญ ถึงจะทำอะไรได้เลย

# excerpt, as generated -- entries/07-hardcoded-region/main.tf
provider "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.tf
resource "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 network
interfaces, use the aws_network_interface_attachment resource.

วิธีแก้: อย่าต่อ ENI ผ่าน network_interface block ของ instance เองเลย ตั้ง subnet_id/vpc_security_group_ids ตรงบน aws_instance สำหรับ primary interface ซึ่งทั้งง่ายกว่าและไม่ deprecated ด้วย

modules/network-compute/main.tf
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
}
}
modules/network-compute/variables.tf
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"
}
modules/network-compute/outputs.tf
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
}
main.tf
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
}
variables.tf
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"
}
outputs.tf
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 }
}
tests/network_compute.tftest.hcl
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 test
tests/network_compute.tftest.hcl... in progress
run "creates_one_subnet_per_az"... 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) — plan จริงขึ้น “Invalid value for netnum parameter” ที่ validate มองไม่เห็น
3root output อ่าน m.vpc_id แต่ module export แค่ vpc_arnmodule export vpc_id จริง; root อ่านมัน — จริงขึ้น “This object does not have an attribute named vpc_id” ก่อนแก้
4required_providers ใช้ >= 6.0~> 6.0 — .terraform.lock.hcl จริงแสดงว่า constraint ลอยตัว resolve ไปเป็น 6.66.0 แบบเงียบๆ
5count บน availability_zones, index เลื่อนตอนลบfor_each = toset(var.availability_zones) — plan จริงพิสูจน์ว่าลบ AZ ตัวกลางแล้ว replace/destroy index ผิดตัว
6ไม่มี lifecycle block บน aws_instance.applifecycle { prevent_destroy = true } — plan -destroy จริงทำลายมันโดยไม่มีแรงต้านก่อนแก้
7provider { region = "us-east-1" } hardcode, var.region ไม่ถูกใช้provider { region = var.region } — plan จริงด้วย -var="region=us-west-2" พิสูจน์ว่าตัวแปรตายเปล่า
8aws_instance.network_interface block (deprecated)ลบออก; ตั้ง subnet_id/vpc_security_group_ids ตรงบน instance — warning deprecation จริงที่ root พิสูจน์ว่าหายไปผ่าน module call

สามใน 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_interface block เดียวกันเป๊ะให้ 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 ของ output vpc_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
แต่ละ bug อยู่ตรงไหนในเส้นทางประเมินของ module นี้
ทำไม error นี้ถึงโผล่ตอน terraform init แทนที่จะเป็น terraform validate?
Error: Module is incompatible with count, for_each, and depends_on
cidrsubnet(count.index, var.subnet_newbits, var.vpc_cidr) มี argument ลำดับผิด ทำไม terraform validate ถึงยอมรับ แต่ terraform plan ปฏิเสธด้วย "Invalid value for \"netnum\" parameter: a number is required"?
root output ทำ [for m in module.app : m.vpc_id] แต่ module export แค่ vpc_arn terraform plan จริงแสดงอะไรตามลำดับ?
aws_instance block เดียวกันเป๊ะที่มี argument network_interface { } ที่ deprecated ถูกประกาศครั้งหนึ่งตรงที่ root module และอีกครั้งข้างใน module ที่ถูกเรียก terraform validate จริงแสดงอะไรสำหรับแต่ละแบบ?
aws_subnet.public ใช้ count = length(var.availability_zones) apply 3 AZ จริง แล้วลบตัวกลางออกจาก list terraform plan จริงแสดงอะไร?
provider "aws" { region = "us-east-1" } hardcode ไว้ ในขณะที่ variable "region" มีอยู่พร้อม default เป็นค่าเดียวกัน รัน terraform plan -var="region=us-west-2" -- plan จริงแสดง attribute region ของทุก resource เป็นอะไร?