Data Source, Hàm, Biểu Thức for và Dynamic Block

K
Kai··5 min read

Bài trước cấu hình đã nhận được giá trị từ ngoài qua variable. Nhưng nhiều giá trị ta cần không nên gõ tay chút nào: id của AMI Amazon Linux mới nhất đổi liên tục, danh sách vùng khả dụng tùy region, mã tài khoản thì không nên hardcode. Những thứ đó đã có sẵn trên AWS, việc của ta là đọc chúng. Bài này gom ba công cụ làm cấu hình thật sự linh hoạt: data source để đọc, biểu thức for để biến đổi dữ liệu, và dynamic block để sinh cấu hình lặp lại.

Mục tiêu

Đọc được thông tin sống từ AWS thay vì hardcode, biến đổi và lọc collection bằng for, và sinh các block lồng (như ingress của security group) bằng dynamic thay vì chép tay.

data source: đọc thay vì tạo

resource tạo và quản lý hạ tầng. data source thì đọc thông tin đã tồn tại, không tạo gì, không đưa vào quản lý. Ba data source hay dùng:

data "aws_caller_identity" "current" {}

data "aws_availability_zones" "available" {
  state = "available"
}

data "aws_ami" "al2023" {
  most_recent = true
  owners      = ["amazon"]
  filter {
    name   = "name"
    values = ["al2023-ami-2023.*-x86_64"]
  }
}

aws_caller_identity trả về danh tính bạn đang dùng (account id, ARN). aws_availability_zones liệt kê các AZ khả dụng trong region. aws_ami với most_recent = true và bộ filter tìm AMI Amazon Linux 2023 mới nhất — đây là cách đúng để luôn dùng image cập nhật thay vì pin một id sẽ cũ đi. Data source được đọc lúc plan/apply, và tham chiếu bằng data.<kiểu>.<tên>.<thuộc_tính>.

Apply rồi xem chúng trả về gì (mã tài khoản thật đã thay bằng 111122223333):

$ terraform apply -auto-approve
$ terraform output
account_id        = "111122223333"
az_names          = tolist([
  "ap-southeast-1a",
  "ap-southeast-1b",
  "ap-southeast-1c",
])
latest_al2023_ami = "ami-05b741ae2ab9f1742"

Ba giá trị này không hề có trong code — Terraform đọc thẳng từ AWS lúc chạy. Lần sau Amazon phát hành AMI mới, latest_al2023_ami tự cập nhật mà không cần ai sửa gì.

Hàm và biểu thức for: biến đổi dữ liệu

Bài 3 đã thử hàm trong terraform console. Hàm phát huy nhiều nhất khi ghép với biểu thức for, công cụ biến đổi và lọc collection. Cú pháp có hai dạng: sinh ra list dùng ngoặc vuông, sinh ra map dùng ngoặc nhọn.

variable "allowed_ports" {
  type    = list(number)
  default = [80, 443, 22]
}

locals {
  # list: lấy các cổng, lọc bỏ 22 bằng mệnh đề if
  web_ports = [for p in var.allowed_ports : p if p != 22]

  # map: cổng -> mô tả
  port_desc = { for p in var.allowed_ports : p => "cho phép cổng ${p}" }
}

Mệnh đề if ở cuối lọc phần tử. Kết quả:

web_ports = [80, 443]          # 22 đã bị lọc

port_desc = {
  "22"  = "cho phép cổng 22"
  "80"  = "cho phép cổng 80"
  "443" = "cho phép cổng 443"
}

for dạng list giữ thứ tự và lọc được; for dạng map biến một danh sách thành cặp khóa-giá trị. Đây là cách bạn nhào nặn dữ liệu đầu vào thành đúng hình dạng resource cần, ngay trong cấu hình.

dynamic block: sinh block lồng lặp lại

Nhiều resource có block lồng lặp lại — security group có nhiều block ingress, CloudFront có nhiều origin. Viết tay từng block khi số lượng thay đổi theo đầu vào là không ổn. dynamic block sinh chúng từ một collection. Tài liệu mô tả nó "hoạt động gần như một biểu thức for, nhưng tạo ra block lồng thay vì một giá trị".

resource "aws_security_group" "web" {
  name_prefix = "tf-series-bai10-"
  vpc_id      = data.aws_vpc.default.id

  dynamic "ingress" {
    for_each = local.web_ports
    content {
      description = "HTTP/HTTPS ${ingress.value}"
      from_port   = ingress.value
      to_port     = ingress.value
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    }
  }

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

Nhãn "ingress" là tên block sẽ sinh ra. for_each lặp qua local.web_ports (đã lọc còn [80, 443]). Bên trong content, ingress.value là phần tử hiện tại. Lưu ý block egress viết tĩnh bình thường — không phải block nào cũng cần dynamic. Apply rồi soi rule thật sinh ra:

$ terraform state show aws_security_group.web
        description = "HTTP/HTTPS 443"
        from_port   = 443
        to_port     = 443
        description = "HTTP/HTTPS 80"
        from_port   = 80
        to_port     = 80

Đúng hai rule cho 80 và 443, không có 22 (đã bị for lọc từ trước). Đổi var.allowed_ports là số rule tự thay đổi theo, không sửa block.

Tài liệu kèm một cảnh báo đáng nghe: "lạm dụng dynamic block khiến cấu hình khó đọc và bảo trì". Khuyến nghị là viết block lồng tĩnh bất cứ khi nào số lượng cố định, chỉ dùng dynamic khi số lượng thực sự thay đổi theo đầu vào. Một dynamic block chỉ để chép y nguyên thuộc tính của biến vào thường là dấu hiệu trừu tượng hóa thừa.

🧹 Dọn dẹp

$ terraform destroy -auto-approve
Destroy complete! Resources: 1 destroyed.

Tổng kết

Data source đọc thông tin có sẵn trên AWS (AMI mới nhất, AZ khả dụng, danh tính tài khoản) thay vì hardcode, tham chiếu qua data.<kiểu>.<tên>. Biểu thức for biến đổi và lọc collection — dạng list [for x in ... : ... if ...], dạng map {for k,v in ... : ... => ...} — để nhào dữ liệu thành hình resource cần. dynamic block sinh các block lồng lặp lại từ một collection, nhưng chỉ nên dùng khi số lượng thật sự biến thiên, còn lại viết tĩnh cho dễ đọc.

Cả dynamic block lẫn việc tạo nhiều bản của một resource đều dựa trên ý tưởng lặp. Bài tới đi vào hai cơ chế tạo nhiều resource — countfor_each — cùng những cạm bẫy thực tế khi chọn sai cái nào, và templatefile để sinh nội dung file từ biến.