Terraform Conditional, count และ for_each: ควบคุม Resource ตามเงื่อนไข

การสร้าง infrastructure ในโลกจริง มักมีเงื่อนไข เช่น dev environment ไม่ต้องการ bastion host, staging ใช้ database ขนาดเล็กกว่า prod, หรือบาง region ต้องการ replica HCL มีเครื่องมือสามอย่างที่ทำงานร่วมกันเพื่อรองรับเคสเหล่านี้: conditional expression, count และ for_each

บทความนี้จะอธิบายแต่ละเครื่องมือ เมื่อไหร่ควรใช้ count vs for_each พร้อมตัวอย่างปัญหาที่เจอบ่อยและวิธีแก้

Conditional Expression

Ternary operator condition ? true_val : false_val ใช้ได้ทุกที่ที่เป็น expression

variable "environment" {
  type = string
}

locals {
  instance_type = var.environment == "prod" ? "t3.large" : "t3.small"
  min_size      = var.environment == "prod" ? 3 : 1
  enable_https  = var.environment != "dev"
}

Conditional ซับซ้อน ให้ใช้ lookup() จาก map แทน — อ่านง่ายกว่า nested ternary

locals {
  sizes = {
    dev     = "t3.micro"
    staging = "t3.small"
    prod    = "t3.large"
  }
  instance_type = lookup(local.sizes, var.environment, "t3.micro")
}

count: Conditional Resource + Multiple Copies

Argument count รับตัวเลข สร้าง resource ตามจำนวนที่กำหนด ถ้า count=0 ก็ไม่สร้าง

# Conditional: สร้าง bastion เฉพาะ non-dev
resource "aws_instance" "bastion" {
  count         = var.environment != "dev" ? 1 : 0
  ami           = "ami-0abc1234"
  instance_type = "t3.nano"
  tags = {
    Name = "bastion-${var.environment}"
  }
}

# อ้างอิง (ต้องระวัง index)
output "bastion_ip" {
  value = length(aws_instance.bastion) > 0 ? aws_instance.bastion[0].public_ip : null
}
# Multiple copies: web server 3 ตัว
resource "aws_instance" "web" {
  count         = 3
  ami           = "ami-0abc1234"
  instance_type = "t3.small"
  tags = {
    Name = "web-${count.index + 1}"
    # → web-1, web-2, web-3
  }
}

for_each: Map/Set-Based Iteration

for_each รับ map หรือ set of strings สร้าง resource ต่อ key — แต่ละ instance มี address ที่ stable แม้ลบ/เพิ่ม entry กลางทาง

resource "aws_s3_bucket" "logs" {
  for_each = {
    prod    = "mycompany-logs-prod"
    staging = "mycompany-logs-staging"
    dev     = "mycompany-logs-dev"
  }

  bucket = each.value
  tags = {
    Environment = each.key
  }
}

# อ้างอิง
output "prod_bucket_arn" {
  value = aws_s3_bucket.logs["prod"].arn
}

ใช้ toset() เมื่อมีข้อมูลเป็น list ธรรมดา

variable "allowed_ports" {
  default = [80, 443, 22]
}

resource "aws_security_group_rule" "allow" {
  for_each    = toset([for p in var.allowed_ports : tostring(p)])
  type        = "ingress"
  from_port   = tonumber(each.value)
  to_port     = tonumber(each.value)
  protocol    = "tcp"
  cidr_blocks = ["0.0.0.0/0"]
  security_group_id = aws_security_group.web.id
}

count vs for_each: เลือกตัวไหน

  • count — เหมาะเมื่อ resource เป็น copies ที่ identical ต่างกันแค่ index (เช่น instance 5 ตัว, replica 3 ตัว)
  • for_each — เหมาะเมื่อแต่ละ instance มี property ต่างกัน และอ้างอิงด้วยชื่อที่มีความหมาย (bucket ต่อ env, SG rule ต่อ port)
  • หลีกเลี่ยง count — กรณีลำดับ list อาจเปลี่ยน เพราะ count.index เปลี่ยนจะทำให้ resource ถูก destroy+recreate

ตัวอย่างปัญหา count ที่เจอบ่อย

# ❌ ปัญหา: ลบ "staging" ออกจาก list
variable "environments" {
  default = ["dev", "staging", "prod"]
}

resource "aws_s3_bucket" "env" {
  count  = length(var.environments)
  bucket = "logs-${var.environments[count.index]}"
}

# ก่อน: [0]=dev, [1]=staging, [2]=prod
# หลังลบ staging: [0]=dev, [1]=prod
# Terraform เห็น:
#   aws_s3_bucket.env[1] เปลี่ยนจาก logs-staging เป็น logs-prod → destroy + recreate
#   aws_s3_bucket.env[2] ถูกลบ → destroy
# → ทั้ง bucket prod จะโดนลบและสร้างใหม่ที่ index อื่น!
# ✅ ใช้ for_each แทน
resource "aws_s3_bucket" "env" {
  for_each = toset(var.environments)
  bucket   = "logs-${each.key}"
}

# ลบ staging → มีแค่ aws_s3_bucket.env["staging"] ที่ถูก destroy
# ตัวอื่น (dev, prod) ไม่โดนกระทบ

Dynamic Block + for_each

บางครั้ง resource มี nested block ที่ต้องสร้างหลายครั้งตามข้อมูล ใช้ dynamic block ร่วมกับ for_each ช่วยได้ (รายละเอียดในบทความถัดไปเรื่อง dynamic block)

variable "ingress_rules" {
  default = [
    { port = 80,  cidr = ["0.0.0.0/0"] },
    { port = 443, cidr = ["0.0.0.0/0"] },
    { port = 22,  cidr = ["10.0.0.0/8"] },
  ]
}

resource "aws_security_group" "web" {
  name = "web-sg"

  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = "tcp"
      cidr_blocks = ingress.value.cidr
    }
  }
}

Best Practices

  • Default ให้ใช้ for_each กับ map/set — plan เสถียรกว่า count
  • ใช้ count = var.enabled ? 1 : 0 เมื่อต้องการ toggle resource เดียว ๆ เท่านั้น
  • อย่า mix count กับ for_each ใน resource เดียวกัน — เลือกอย่างใดอย่างหนึ่ง
  • ระวัง for_each ที่ depend on resource ที่ยังไม่ apply — tool ต้องรู้ key ตั้งแต่ plan time (ถ้าจำเป็น ใช้ -target apply ทีละส่วน)
  • ตั้งชื่อ key ใน map ให้มีความหมาย เพราะจะใช้ใน state address (เช่น resource.env["prod"] อ่านง่ายกว่า resource.env[2])

สรุป

Conditional expression, count และ for_each เป็นเครื่องมือสำคัญในการทำ configuration ที่ยืดหยุ่นและรองรับหลาย environment ส่วนใหญ่ในงานจริง for_each เป็นตัวเลือกที่ดีกว่าเพราะ address ของ resource จะ stable เมื่อมีการเพิ่ม/ลบ entry ส่วน count เหมาะกับกรณี identical copies หรือ toggle on/off เท่านั้น ในบทความถัดไปจะเจาะลึก dynamic block ซึ่งเป็นเครื่องมือคู่หูกับ for_each สำหรับสร้าง nested block แบบ programmatic