search

08-for_each và dynamic block trong Terraform

calendar_today Đăng ngày: 29/04/2026

Bài này mình sẽ giải thích cách dùng for_eachdynamic block trong Terraform. Hai thứ này giúp bạn tránh viết lặp code khi cần tạo nhiều resource hoặc nhiều nested block có cấu trúc giống nhau.

1. Vấn đề trước khi có for_each

Giả sử bạn cần tạo 3 subnet. Cách viết thủ công:

resource "aws_subnet" "subnet_1" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.1.0/24"
}

resource "aws_subnet" "subnet_2" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.2.0/24"
}

resource "aws_subnet" "subnet_3" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.3.0/24"
}

3 subnet thì còn chịu được. Nhưng nếu cần 10, 20 subnet thì sao? Code bị lặp, khó maintain, thêm/bớt subnet phải sửa tay từng chỗ.

for_each sinh ra để giải quyết đúng vấn đề này.

2. for_each – Tạo nhiều resource từ một map hoặc set

Ví dụ 1: Map đơn giản

variable "public_subnets" {
  default = {
    "public_subnet_1" = "10.0.1.0/24"
    "public_subnet_2" = "10.0.2.0/24"
    "public_subnet_3" = "10.0.3.0/24"
  }
}

resource "aws_subnet" "public_subnets" {
  for_each                = var.public_subnets
  map_public_ip_on_launch = true
}

Hãy hình dung for_each như một vòng lặp for quen thuộc:

for mỗi item trong var.public_subnets:
    tạo một aws_subnet

Terraform chạy qua map, thấy 3 key → tạo đúng 3 subnet:

aws_subnet.public_subnets["public_subnet_1"]
aws_subnet.public_subnets["public_subnet_2"]
aws_subnet.public_subnets["public_subnet_3"]

Bạn muốn thêm subnet thứ 4? Chỉ cần thêm một dòng vào variable, không đụng vào resource block.

each.key và each.value là gì?

Khi dùng for_each với map, Terraform cung cấp cho bạn hai biến đặc biệt bên trong resource block:

  • each.key → key của phần tử đang được xử lý
  • each.value → value của phần tử đó
variable "subnet_config" {
  default = {
    "public_subnet_1" = "10.0.1.0/24"
    "public_subnet_2" = "10.0.2.0/24"
    "public_subnet_3" = "10.0.3.0/24"
  }
}

resource "aws_subnet" "example" {
  for_each   = var.subnet_config
  cidr_block = each.value   # "10.0.1.0/24", "10.0.2.0/24", ...

  tags = {
    Name = each.key         # "public_subnet_1", "public_subnet_2", ...
  }
}

Cứ hình dung như bạn đang viết:

for key, value in subnet_config.items():
    create_subnet(cidr_block=value, name=key)

Ví dụ 2: Map của objects – thực tế hơn

Khi mỗi item cần nhiều thuộc tính hơn, bạn dùng map of objects:

variable "security_groups" {
  type = map(object({
    name        = string
    description = string
  }))

  default = {
    sg_ssh = {
      name        = "allow_ssh"
      description = "Allow SSH traffic"
    }
    sg_http = {
      name        = "allow_http"
      description = "Allow HTTP traffic"
    }
  }
}

Lúc này value của mỗi item là một object có nhiều trường, bạn truy cập qua each.value.tên_trường:

resource "aws_security_group" "example" {
  for_each    = var.security_groups

  name        = each.value.name         # "allow_ssh" hoặc "allow_http"
  description = each.value.description  # "Allow SSH traffic" hoặc ...
  vpc_id      = aws_vpc.main.id
}

Terraform tạo ra:

aws_security_group.example["sg_ssh"]   → name = "allow_ssh"
aws_security_group.example["sg_http"]  → name = "allow_http"

3. for_each vs count – Khi nào dùng cái nào?

count là cách tạo nhiều resource cũ hơn, hoạt động với list và số nguyên:

# Với count
resource "aws_instance" "example" {
  count         = length(var.instances)
  ami           = var.instances[count.index].ami
  instance_type = var.instances[count.index].instance_type

  tags = {
    Name = var.instances[count.index].name
  }
}

Nhìn qua thì hai cái có vẻ như nhau. Nhưng có một điểm khác biệt quan trọng.

Vấn đề khi dùng count

Giả sử bạn có 3 instance, Terraform track chúng theo index:

aws_instance.example[0]  → instance-A
aws_instance.example[1]  → instance-B
aws_instance.example[2]  → instance-C

Bạn xóa instance-B khỏi list. List còn lại: [instance-A, instance-C]

Terraform nhìn vào thấy:

[0] = instance-A  giữ nguyên
[1] = instance-C  ← trước là instance-B, giờ khác rồi → destroy + recreate

instance-C bị recreate dù bạn không muốn đụng vào nó. Với hạ tầng production, đây là vấn đề nghiêm trọng.

for_each giải quyết điều này

aws_instance.example["instance-A"]
aws_instance.example["instance-B"]
aws_instance.example["instance-C"]

Terraform track theo key, không phải index. Xóa instance-B? Chỉ instance-B bị destroy, hai cái còn lại không bị đụng vào.

Bảng so sánh

for_each count
Kiểu dữ liệu map hoặc set list hoặc số nguyên
Track resource theo Key Index
Xóa một phần tử giữa list Chỉ xóa đúng cái đó Có thể recreate cái phía sau
Truy cập giá trị each.key, each.value count.index
Nên dùng khi Mỗi item có tên/key riêng Chỉ cần tạo N resource giống hệt nhau

Nguyên tắc chung: nếu mỗi resource cần phân biệt được với nhau, dùng for_each. Chỉ dùng count khi bạn thực sự chỉ cần “tạo N cái giống nhau” và không bao giờ cần xóa từng cái riêng lẻ.

4. dynamic block – Khi vấn đề nằm bên trong resource

for_each giúp bạn tạo nhiều resource. Nhưng đôi khi vấn đề không phải là số lượng resource, mà là số lượng block bên trong một resource.

Vấn đề cụ thể

Security group cần nhiều ingress rule. Viết cứng thì trông như này:

resource "aws_security_group" "example" {
  name   = "example"
  vpc_id = aws_vpc.main.id

  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"]
  }
}

Thêm rule mới → sửa resource block. 10 rule thì code rất dài và khó quản lý.

Giải pháp: dynamic block

variable "ingress_rules" {
  type = list(object({
    from_port   = number
    to_port     = number
    protocol    = string
    cidr_blocks = list(string)
  }))

  default = [
    {
      from_port   = 80
      to_port     = 80
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    },
    {
      from_port   = 443
      to_port     = 443
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    },
    {
      from_port   = 22
      to_port     = 22
      protocol    = "tcp"
      cidr_blocks = ["10.0.0.0/8"]
    }
  ]
}

resource "aws_security_group" "example" {
  name   = "example"
  vpc_id = aws_vpc.main.id

  dynamic "ingress" {
    for_each = var.ingress_rules

    content {
      from_port   = ingress.value.from_port
      to_port     = ingress.value.to_port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
    }
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Terraform đọc code này và tự sinh ra đúng 3 ingress {} block như version viết cứng ở trên. Về mặt kết quả là hoàn toàn giống nhau.

Cấu trúc của dynamic block

dynamic "<tên_block>" {
  for_each = <collection>
  iterator = <tên_biến>   # tuỳ chọn

  content {
    # dùng <tên_block>.value hoặc <tên_biến>.value
  }
}
  • tên_block: tên của nested block bạn muốn sinh ra (ingress, egress, tag…)
  • for_each: collection để duyệt qua
  • content {}: template cho mỗi block được tạo ra
  • iterator: đặt tên khác cho biến lặp thay vì dùng tên mặc định là tên_block

Khi nào nên dùng iterator?

Mặc định, bên trong content {} bạn dùng ingress.value (tên block làm tên biến). Nếu tên block dài hoặc gây nhầm lẫn, đặt iterator cho gọn hơn:

dynamic "ingress" {
  for_each = var.ingress_rules
  iterator = rule            # đặt tên biến là "rule"

  content {
    from_port   = rule.value.from_port    # thay vì ingress.value.from_port
    to_port     = rule.value.to_port
    protocol    = rule.value.protocol
    cidr_blocks = rule.value.cidr_blocks
  }
}

Tóm lại

  • for_each → tạo nhiều resource riêng biệt từ một map hoặc set. Dùng each.keyeach.value để truy cập từng phần tử.
  • count → tạo N resource giống nhau. Đơn giản hơn nhưng dễ gây recreate ngoài ý muốn khi xóa phần tử giữa list.
  • dynamic block → sinh ra nhiều nested block bên trong một resource. Cấu trúc giống for_each nhưng áp dụng cho block thay vì toàn bộ resource.