count và for_each: Cạm Bẫy Chỉ Số, Conditional, templatefile
Bài trước dynamic block lặp để sinh các block lồng. Còn để tạo nhiều resource — ba bucket, năm instance — Terraform có hai meta-argument: count và for_each. Chúng trông tương đương lúc mới dùng, nhưng chọn sai gây ra một trong những sự cố khó chịu nhất với người mới: xóa nhầm và tạo lại hàng loạt resource đang chạy tốt. Bài này dựng đúng tình huống đó để thấy tận mắt, rồi xử lý gọn bằng for_each.
Mục tiêu
Hiểu khác biệt giữa count (theo chỉ số) và for_each (theo khóa) đủ sâu để chọn đúng, thấy được cạm bẫy dịch chỉ số của count, và biết thêm hai kỹ thuật hay dùng: tạo resource có điều kiện và templatefile.
count: tạo theo chỉ số
count tạo N bản của một resource, đánh số từ 0:
variable "names" {
type = list(string)
default = ["alpha", "beta", "gamma"]
}
resource "aws_s3_bucket" "b" {
count = length(var.names)
bucket_prefix = "tf-series-bai11-${var.names[count.index]}-"
force_destroy = true
}
count.index là chỉ số hiện tại (0, 1, 2). Apply rồi xem địa chỉ:
$ terraform state list
aws_s3_bucket.b[0]
aws_s3_bucket.b[1]
aws_s3_bucket.b[2]
Mỗi bucket mang địa chỉ theo vị trí trong danh sách: b[0] là alpha, b[1] là beta, b[2] là gamma. Đó là gốc rễ của cạm bẫy.
Cạm bẫy: bỏ một phần tử giữa
Giả sử bạn không cần bucket beta nữa, sửa danh sách còn ["alpha", "gamma"]. Trực giác nói: chỉ beta bị xóa. Xem Terraform định làm gì:
$ terraform plan -var 'names=["alpha","gamma"]'
# aws_s3_bucket.b[1] must be replaced
~ bucket_prefix = "tf-series-bai11-beta-" -> "tf-series-bai11-gamma-" # forces replacement
# aws_s3_bucket.b[2] will be destroyed
- bucket_prefix = "tf-series-bai11-gamma-" -> null
Plan: 1 to add, 0 to change, 2 to destroy.
Đọc kỹ con số: 1 thêm, 2 xóa. Danh sách mới còn hai phần tử, nên chỉ số chỉ còn [0] và [1]. b[0] vẫn là alpha, không sao. Nhưng b[1] trước là beta giờ thành gamma — Terraform thấy bucket_prefix tại vị trí [1] đổi từ beta sang gamma, nên thay thế nó. Còn b[2] (gamma cũ) không còn vị trí, bị xóa. Kết cục: bucket gamma đang chạy tốt bị phá đi rồi dựng lại ở vị trí khác, dù bạn không hề định đụng vào nó.
Với bucket rỗng thì phiền; với database hay volume chứa dữ liệu, đây là mất mát thật. Vấn đề nằm ở chỗ count định danh resource bằng vị trí, mà vị trí thì dịch khi bạn thêm/bớt giữa chừng.
for_each: tạo theo khóa ổn định
for_each định danh mỗi bản bằng một khóa thay vì vị trí. Nó nhận map hoặc set chuỗi:
variable "names" {
type = set(string)
default = ["alpha", "beta", "gamma"]
}
resource "aws_s3_bucket" "b" {
for_each = var.names
bucket_prefix = "tf-series-bai11-${each.key}-"
force_destroy = true
}
each.key (và each.value) là phần tử hiện tại. Apply rồi xem địa chỉ:
$ terraform state list
aws_s3_bucket.b["alpha"]
aws_s3_bucket.b["beta"]
aws_s3_bucket.b["gamma"]
Địa chỉ giờ là tên, không phải số. Lặp lại đúng thao tác bỏ beta:
$ terraform plan -var 'names=["alpha","gamma"]'
# aws_s3_bucket.b["beta"] will be destroyed
# (because key ["beta"] is not in for_each map)
Plan: 0 to add, 0 to change, 1 to destroy.
0 thêm, 1 xóa. Chỉ đúng beta bị xóa, vì khóa "beta" không còn trong tập. Alpha và gamma giữ nguyên khóa nên Terraform không đụng tới.
count: định danh theo VỊ TRÍ for_each: định danh theo KHÓA
─────────────────────────── ─────────────────────────────
[0] alpha ["alpha"] alpha
[1] beta ← bỏ beta ["beta"] beta ← bỏ beta
[2] gamma ["gamma"] gamma
sau khi bỏ beta: sau khi bỏ beta:
[0] alpha (giữ) ["alpha"] alpha (giữ)
[1] gamma (beta→gamma: THAY THẾ) ["gamma"] gamma (giữ)
[2] — (XÓA) ["beta"] (XÓA đúng beta)
=> 2 destroy, 1 add => 1 destroy
Chọn cái nào
Tài liệu nói rõ: dùng count "khi muốn tạo các bản gần như giống hệt nhau", dùng for_each "khi một số tham số của bản phải mang giá trị riêng, không suy ra được từ một số nguyên". Quy tắc thực hành đơn giản hơn: nếu các bản phân biệt nhau bằng một định danh có nghĩa (tên, vùng, môi trường) thì dùng for_each để khóa ổn định; chỉ dùng count cho những thứ thật sự vô danh và cố định về số lượng. Khi nghi ngờ, chọn for_each — nó tránh được cạm bẫy dịch chỉ số. (Một lưu ý: for_each không tự chuyển list thành set, phải toset(...) nếu đầu vào là list.)
Tạo resource có điều kiện
count vẫn còn một công dụng: bật/tắt một resource. Vì count = 0 nghĩa là không tạo bản nào, ghép với toán tử ba ngôi cho ta cách tạo resource có điều kiện:
resource "aws_s3_bucket" "extra" {
count = var.create_extra ? 1 : 0
# ...
}
$ terraform output extra_count # create_extra = false
0
$ terraform apply -var create_extra=true && terraform output -raw extra_count
1
create_extra = false thì count = 0, không tạo gì; true thì tạo một bản. Đây là mẫu rất hay gặp để bật một tính năng chỉ ở prod (ví dụ chỉ prod mới tạo bản sao lưu chéo vùng).
templatefile: sinh nội dung file từ biến
Đôi khi cần sinh một file cấu hình từ dữ liệu Terraform — config nginx, user-data cho EC2, file JSON. templatefile(path, vars) đọc một file template và render nó với bộ biến. Template hỗ trợ nội suy ${...} và directive như vòng lặp %{ for }.
File template nginx-upstream.tftpl:
upstream backend {
%{ for addr in ip_addrs ~}
server ${addr}:${port};
%{ endfor ~}
}
Render với một danh sách IP:
locals {
rendered = templatefile("${path.module}/nginx-upstream.tftpl", {
port = 8080
ip_addrs = ["10.0.1.10", "10.0.1.11", "10.0.1.12"]
})
}
Kết quả:
upstream backend {
server 10.0.1.10:8080;
server 10.0.1.11:8080;
server 10.0.1.12:8080;
}
Vòng lặp %{ for ... ~} sinh một dòng server cho mỗi IP, dấu ~ cắt khoảng trắng thừa. Đây là cách sinh user-data, file cấu hình động mà không phải nối chuỗi thủ công.
🧹 Dọn dẹp
Mọi demo trong bài đều terraform destroy -auto-approve ngay sau khi xem kết quả; phần templatefile chỉ tính local nên không tạo gì trên AWS.
Tổng kết
count định danh resource theo vị trí ([0], [1]), nên bỏ một phần tử giữa danh sách làm dịch chỉ số và xóa-tạo lại nhầm những resource phía sau — demo cho thấy bỏ beta khiến gamma bị thay thế (2 destroy). for_each định danh theo khóa ổn định (["alpha"]), nên cùng thao tác chỉ xóa đúng phần tử cần (1 destroy). Quy tắc: phân biệt bằng định danh có nghĩa thì dùng for_each, nghi ngờ cũng chọn for_each; count để dành cho bản vô danh và cho mẫu tạo có điều kiện count = cond ? 1 : 0. templatefile render file từ biến với directive %{ for }.
Đến đây ta đã có đủ công cụ ngôn ngữ. Part IV gom chúng lại thành thứ tái dùng được thật sự: module. Bài 12 viết module đầu tiên — đóng gói một nhóm resource sau một giao diện input/output sạch, để dùng lại ở nhiều nơi mà không chép code.