count và for_each: Cạm Bẫy Chỉ Số, Conditional, templatefile

K
Kai··6 min read

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: countfor_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][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.