Viết Module Đầu Tiên
Part III cho ta đủ công cụ ngôn ngữ: biến, biểu thức, vòng lặp. Nhưng tới giờ mọi thứ vẫn nằm trong một thư mục phẳng, và muốn dựng cùng một cụm resource ở chỗ khác thì phải chép. Module giải đúng việc đó: gói một nhóm resource thành một khái niệm có tên, có giao diện vào-ra rõ ràng, dùng lại được. Bài này viết module đầu tiên và gọi nó hai lần để thấy giá trị tái dùng.
Mục tiêu
Hiểu module là gì, cấu trúc chuẩn của nó, và viết được một module có giao diện input/output sạch rồi gọi từ root với nhiều bộ đầu vào khác nhau.
Module là gì
Tài liệu định nghĩa gọn: module là "một vùng chứa cho nhiều resource dùng chung với nhau". Nó cho phép mô tả hạ tầng bằng khái niệm kiến trúc ("một bucket an toàn", "một cụm web") thay vì liệt kê từng resource rời rạc.
Mọi cấu hình Terraform đều đã là module rồi: các file .tf trong thư mục làm việc tạo thành root module. Root có thể gọi các child module khác qua block module, truyền output của cái này vào input của cái kia. Khác biệt duy nhất giữa root và child là root là nơi bạn chạy lệnh, còn child được gọi tới.
Cấu trúc chuẩn
Một module theo quy ước có ba file:
modules/secure-bucket/
variables.tf # đầu vào (input)
main.tf # các resource
outputs.tf # đầu ra (output)
Chia ba file là quy ước, không bắt buộc — Terraform đọc mọi file .tf trong thư mục. Nhưng tách rõ input / resource / output giúp người dùng module nhìn variables.tf và outputs.tf là hiểu ngay giao diện mà không cần đọc phần ruột.
Một module tốt, theo tài liệu, "nâng mức trừu tượng bằng cách mô tả một khái niệm mới trong kiến trúc". Cảnh báo đi kèm: đừng tạo module chỉ bọc mỏng một resource đơn lẻ — nếu không đặt được tên cho module độc lập với resource chính bên trong, thì dùng thẳng resource đó còn hơn. Ví dụ dưới đây gói bốn resource thành một khái niệm "bucket an toàn", đủ để xứng đáng là module.
Viết module secure-bucket
Đầu vào, trong variables.tf:
variable "name_prefix" {
type = string
description = "Tiền tố tên bucket"
}
variable "versioning" {
type = bool
default = true
}
variable "force_destroy" {
type = bool
default = false
}
variable "tags" {
type = map(string)
default = {}
}
Phần ruột, trong main.tf — gói bucket cùng versioning, mã hóa, chặn public (đúng bộ resource bài 6 từng viết tay, giờ thành một khối tái dùng):
resource "aws_s3_bucket" "this" {
bucket_prefix = var.name_prefix
force_destroy = var.force_destroy
tags = var.tags
}
resource "aws_s3_bucket_versioning" "this" {
bucket = aws_s3_bucket.this.id
versioning_configuration {
status = var.versioning ? "Enabled" : "Suspended"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "this" {
bucket = aws_s3_bucket.this.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_public_access_block" "this" {
bucket = aws_s3_bucket.this.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
Hai quy ước đáng để ý. Một là tên local this cho resource chính — khi một module chỉ có một resource mỗi loại, đặt tên this là cách thường gặp, vì địa chỉ đầy đủ đã có tiền tố module rồi. Hai là module không khai provider — nó kế thừa provider từ root gọi nó, để cùng một module chạy được ở region nào tùy root quyết.
Đầu ra, trong outputs.tf — đây là giao diện module trả về cho bên ngoài:
output "id" {
value = aws_s3_bucket.this.id
}
output "arn" {
value = aws_s3_bucket.this.arn
}
Gọi module từ root
Root dùng block module, chỉ source tới đường dẫn module và truyền input. Gọi cùng một module hai lần với đầu vào khác nhau:
module "logs" {
source = "./modules/secure-bucket"
name_prefix = "tf-series-bai12-logs-"
force_destroy = true
tags = { Purpose = "logs" }
}
module "data" {
source = "./modules/secure-bucket"
name_prefix = "tf-series-bai12-data-"
versioning = false
force_destroy = true
tags = { Purpose = "data" }
}
output "logs_bucket" {
value = module.logs.id
}
output "data_bucket_arn" {
value = module.data.arn
}
Output của module truy cập qua module.<tên>.<output> — ở đây module.logs.id và module.data.arn. Hai lần gọi tạo hai cụm bucket độc lập, mỗi cụm bốn resource, từ cùng một đoạn code.
Chạy thử
init giờ phải cài cả module:
$ terraform init
Initializing modules...
- data in modules/secure-bucket
- logs in modules/secure-bucket
...
Terraform has been successfully initialized!
$ terraform apply -auto-approve
Apply complete! Resources: 8 added, 0 changed, 0 destroyed.
Tám resource — bốn cho mỗi lần gọi module. Địa chỉ trong state mang tiền tố module:
$ terraform state list
module.data.aws_s3_bucket.this
module.data.aws_s3_bucket_public_access_block.this
module.data.aws_s3_bucket_server_side_encryption_configuration.this
module.data.aws_s3_bucket_versioning.this
module.logs.aws_s3_bucket.this
module.logs.aws_s3_bucket_public_access_block.this
module.logs.aws_s3_bucket_server_side_encryption_configuration.this
module.logs.aws_s3_bucket_versioning.this
$ terraform output
data_bucket_arn = "arn:aws:s3:::tf-series-bai12-data-2026..."
logs_bucket = "tf-series-bai12-logs-2026..."
Tiền tố module.data. và module.logs. chính là cơ chế Terraform giữ hai cụm tách biệt dù chúng sinh từ cùng một định nghĩa. Đây cũng là lý do bài 7 nhắc state mv dùng khi gom resource vào module: di chuyển resource từ root vào trong module nghĩa là đổi địa chỉ của nó để có tiền tố module..
🧹 Dọn dẹp
$ terraform destroy -auto-approve
Destroy complete! Resources: 8 destroyed.
Tổng kết
Module gói một nhóm resource thành một khái niệm có giao diện vào-ra rõ ràng: variables.tf là input, main.tf là resource, outputs.tf là output. Root gọi child qua block module với source và các input, đọc kết quả qua module.<tên>.<output>. Gọi cùng một module nhiều lần với đầu vào khác nhau cho ra nhiều cụm độc lập từ một đoạn code, mỗi cụm mang tiền tố module.<tên>. trong state. Module nên gói một khái niệm xứng đáng, không bọc mỏng một resource.
Module trong bài này nằm ngay trong dự án (./modules/...). Bài tới mở rộng ra: kết hợp nhiều module thành tầng lớn hơn, lấy module dùng chung từ Terraform Registry, và pin phiên bản module để dự án ổn định theo thời gian.