Bài này mình sẽ giải thích cách dùng for_each và dynamic 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 quacontent {}: template cho mỗi block được tạo raiterator: đặ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ùngeach.keyvàeach.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ốngfor_eachnhưng áp dụng cho block thay vì toàn bộ resource.