Resource บางตัวใน HCL มี nested block ที่ซ้ำ ๆ กันหลายชุด เช่น ingress rule ใน security group, statement ใน IAM policy, หรือ environment variables ใน Lambda function การเขียน nested block เดิมซ้ำหลายครั้งทำให้ config ยาวและแก้ไขยาก
Dynamic block ช่วยแก้ปัญหานี้โดยให้สร้าง nested block แบบ programmatic จาก list หรือ map ข้อมูล บทความนี้จะอธิบายวิธีใช้งาน dynamic block พร้อมตัวอย่างที่เจอบ่อย และข้อควรระวัง
โครงสร้างพื้นฐาน
Dynamic block มีโครงสร้าง 3 ส่วน: ชื่อ block ที่ต้องการสร้าง, for_each กำหนดข้อมูลที่วนซ้ำ, และ content คือ template ของ block แต่ละตัว
resource "aws_example" "name" {
# ... normal arguments ...
dynamic "block_name" {
for_each = var.items
content {
attribute1 = block_name.value.field1
attribute2 = block_name.value.field2
}
}
}
ภายใน content ใช้ block_name.value เข้าถึงค่าแต่ละ item และ block_name.key เข้าถึง key (ถ้า for_each เป็น map)
ตัวอย่าง 1: Security Group Ingress Rules
Security group ที่มี ingress rule หลายข้อเป็น use case ยอดนิยม เขียนแบบปกติต้อง copy-paste block ซ้ำ
# แบบเดิม — ingress rule 3 ข้อ ต้องเขียน 3 block
resource "aws_security_group" "web" {
name = "web-sg"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["10.0.0.0/8"]
}
}
# แบบ dynamic block — ตัว rule อยู่ใน variable
variable "ingress_rules" {
type = list(object({
port = number
protocol = string
cidr_blocks = list(string)
description = string
}))
default = [
{ port = 80, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"], description = "HTTP" },
{ port = 443, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"], description = "HTTPS" },
{ port = 22, protocol = "tcp", cidr_blocks = ["10.0.0.0/8"], description = "SSH admin" },
]
}
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 = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
description = ingress.value.description
}
}
}
เพิ่ม/ลด rule เพียงแก้ที่ variable เดียว ไม่ต้องแตะ resource block
ตัวอย่าง 2: IAM Policy Statements
IAM policy มี statement block หลายข้อ ซึ่งสามารถประกอบจาก list ของ permission ได้
variable "policy_statements" {
default = [
{
sid = "AllowS3Read"
actions = ["s3:GetObject", "s3:ListBucket"]
resources = [
"arn:aws:s3:::mybucket",
"arn:aws:s3:::mybucket/*",
]
},
{
sid = "AllowCloudWatchLogs"
actions = ["logs:CreateLogStream", "logs:PutLogEvents"]
resources = ["arn:aws:logs:*:*:*"]
},
]
}
data "aws_iam_policy_document" "app" {
dynamic "statement" {
for_each = var.policy_statements
content {
sid = statement.value.sid
actions = statement.value.actions
resources = statement.value.resources
effect = "Allow"
}
}
}
resource "aws_iam_policy" "app" {
name = "app-policy"
policy = data.aws_iam_policy_document.app.json
}
ตัวอย่าง 3: Lambda Environment Variables
Lambda รับ environment variables เป็น block ที่มี variables map สามารถ generate จาก locals ตาม environment ได้
locals {
common_env = {
LOG_LEVEL = "info"
AWS_REGION = "ap-southeast-1"
ENVIRONMENT = var.environment
}
env_specific = var.environment == "prod" ? {
DB_POOL_SIZE = "20"
CACHE_TTL = "3600"
} : {
DB_POOL_SIZE = "5"
CACHE_TTL = "60"
}
all_env = merge(local.common_env, local.env_specific)
}
resource "aws_lambda_function" "api" {
function_name = "api-handler"
# ...
dynamic "environment" {
for_each = length(local.all_env) > 0 ? [1] : []
content {
variables = local.all_env
}
}
}
สังเกต trick: Lambda environment block ยอมให้มีแค่ 0 หรือ 1 ชุด ใช้ for_each กับ list [1] เมื่อต้องการสร้าง block, หรือ list ว่าง [] เมื่อไม่ต้องการ — เป็น idiom สำหรับ “conditional block”
Conditional Dynamic Block
สร้าง nested block เฉพาะเมื่อตรงเงื่อนไข ใช้ for_each กับ list ว่างหรือ list หนึ่ง item
resource "aws_s3_bucket" "example" {
bucket = "my-logs-bucket"
# สร้าง versioning block เฉพาะ prod
dynamic "versioning" {
for_each = var.environment == "prod" ? [1] : []
content {
enabled = true
}
}
}
Nested Dynamic Block
บาง resource มี block ซ้อน block เช่น CloudFront distribution ที่มี ordered_cache_behavior ซึ่งภายในมี forwarded_values สามารถซ้อน dynamic ได้ตามต้องการ
resource "aws_cloudfront_distribution" "main" {
# ...
dynamic "ordered_cache_behavior" {
for_each = var.cache_behaviors
content {
path_pattern = ordered_cache_behavior.value.path
target_origin_id = ordered_cache_behavior.value.origin
dynamic "forwarded_values" {
for_each = ordered_cache_behavior.value.forward_config != null ? [1] : []
content {
query_string = ordered_cache_behavior.value.forward_config.query_string
headers = ordered_cache_behavior.value.forward_config.headers
cookies {
forward = "none"
}
}
}
}
}
}
ข้อควรระวัง
- อย่าใช้ dynamic block เกินความจำเป็น — ถ้ามี block คงที่ 1-2 ตัว เขียนตรง ๆ อ่านง่ายกว่า
- Error message อ่านยาก — เมื่อเกิดข้อผิดพลาดใน dynamic block, error จะชี้ที่ resource ไม่ใช่ item ที่ผิด ต้องตรวจข้อมูลใน list เอง
- Type ต้องตรงกัน — ทุก item ใน
for_eachต้องมี structure เดียวกัน ไม่งั้น HCL จะ fail ตั้งแต่ plan - Nested dynamic ซับซ้อนเร็วมาก — ถ้าต้อง nest 3 ชั้นขึ้นไป ให้พิจารณา refactor เป็น module หรือใช้
localsช่วย flatten ข้อมูล
Flatten ข้อมูลก่อนใส่ dynamic block
บางครั้งโครงสร้างข้อมูลต้นทางไม่เหมาะกับ for_each โดยตรง เช่น map ซ้อน map — ต้อง flatten ด้วย for expression ก่อน
locals {
# ต้นทาง: map ของ env แต่ละตัวมี rule หลายข้อ
env_rules = {
prod = [
{ port = 443, cidr = ["0.0.0.0/0"] },
{ port = 22, cidr = ["10.0.0.0/8"] },
]
dev = [
{ port = 80, cidr = ["0.0.0.0/0"] },
]
}
# Flatten เป็น list ของ { env, port, cidr }
flat_rules = flatten([
for env, rules in local.env_rules : [
for r in rules : {
env = env
port = r.port
cidr = r.cidr
}
]
])
}
resource "aws_security_group_rule" "all" {
count = length(local.flat_rules)
type = "ingress"
from_port = local.flat_rules[count.index].port
to_port = local.flat_rules[count.index].port
protocol = "tcp"
cidr_blocks = local.flat_rules[count.index].cidr
security_group_id = aws_security_group.web[local.flat_rules[count.index].env].id
}
Best Practices
- ใช้ dynamic block เมื่อ nested block มีจำนวนไม่แน่นอน หรือ depends on variable
- ตั้งชื่อ iterator ให้สื่อความหมาย (default ใช้ชื่อ block เอง ซึ่งพอใช้ได้) —
iterator = ruleช่วยให้rule.valueอ่านง่ายขึ้น - ประกาศ
variableด้วยtypeชัดเจน — ช่วยให้ error ตอน validate ก่อน apply - ถ้า dynamic block เริ่มซับซ้อน (nested หลายชั้น, conditional หลายตัว) — แยกเป็น module ช่วยให้ reuse ได้
- ทดสอบด้วย
terraform planดู diff ก่อน apply — dynamic block มักสร้าง resource หลายชิ้นพร้อมกัน
สรุป
Dynamic block เป็นเครื่องมือสำคัญในการสร้าง nested block แบบ data-driven ลดการเขียน code ซ้ำและทำให้ config ยืดหยุ่นต่อการเปลี่ยนแปลง เหมาะอย่างยิ่งสำหรับ security group rules, IAM policy statements, และ optional block ที่ต้องการสร้างตามเงื่อนไข แต่ควรใช้เมื่อจำเป็นและหลีกเลี่ยงการซ้อนหลายชั้นซึ่งทำให้ config อ่านยาก ในบทความถัดไปจะพูดถึง provisioner ซึ่งเป็นเครื่องมือสำหรับรัน script หลังสร้าง resource

