Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 12 — Terraform Modules & Workspaces

Đã đọc 0/14 mục · còn khoảng 12 phút
Cách chúng tôi ghi nhận

Một mục được tính là đã đọc khi nó nằm giữa màn hình đủ lâu so với độ dài của nó (bảng và sơ đồ tính một nửa), bạn vẫn đang cuộn hoặc bấm phím, và bạn đã thấy tới cuối mục. Lướt nhanh hoặc để tab mở không được tính. Chúng tôi chỉ lưu thời gian đọc từng mục — không lưu gì khác.

Mở đầu — vì sao bài này quan trọng

Ở bài trước, bạn đã biết viết Terraform với ba khối cơ bản: provider, resource và state. Bạn có thể tạo một VPC, một EC2 instance, một security group trong một file main.tf duy nhất. Điều đó ổn khi bạn học, nhưng sẽ trở thành cơn ác mộng khi dự án lớn lên.

Hãy tưởng tượng bạn làm DevOps cho một startup fintech ở TP.HCM. Ban đầu chỉ có một môi trường production. Sáu tháng sau, sếp yêu cầu: "Chúng ta cần thêm môi trường staging giống hệt production để test, và một môi trường dev cho anh em code." Bạn mở file Terraform ra và phát hiện mình đang có 400 dòng HCL mô tả toàn bộ hạ tầng. Bây giờ phải nhân ba lên? Copy-paste ba lần rồi sửa tên tài nguyên? Chỉ cần nghĩ đến việc đó thôi đã thấy mệt, chưa kể mỗi lần sửa một thứ chung phải sửa ở ba chỗ, rất dễ quên một chỗ và gây lệch cấu hình.

Đây chính là lý do bài học hôm nay tồn tại. Module giúp bạn đóng gói một cụm hạ tầng thành một "gói tái sử dụng" — viết một lần, dùng nhiều lần. Workspace giúp bạn quản lý nhiều state riêng biệt từ cùng một bộ code. Hai công cụ này là bước chuyển từ "viết Terraform để chạy được" sang "viết Terraform để một đội nhiều người vận hành được lâu dài". Nắm vững chúng, bạn sẽ tránh được hầu hết các bãi mìn về tổ chức code hạ tầng mà các đội DevOps hay giẫm phải.

Khái niệm cốt lõi

Module là gì

Module trong Terraform đơn giản là một thư mục chứa các file .tf. Đúng vậy, không có gì huyền bí. Ngay cả thư mục gốc nơi bạn chạy terraform apply cũng là một module — gọi là root module. Khi root module gọi đến một thư mục khác, thư mục đó trở thành child module.

Ý tưởng cốt lõi là: bạn gói một nhóm tài nguyên thường đi cùng nhau (ví dụ: một VPC với subnet, route table, internet gateway) vào một module, khai báo các input variables để tùy biến, và outputs để trả về thông tin cho bên gọi. Sau đó, bất cứ nơi nào cần một VPC, bạn chỉ việc gọi module đó với vài dòng thay vì viết lại hàng chục dòng.

Một module VPC tối giản trông như sau:

# modules/vpc/variables.tf
variable "cidr" {
  type        = string
  description = "CIDR block cho VPC, ví dụ 10.0.0.0/16"
}

variable "name" {
  type        = string
  description = "Tên định danh cho VPC"
}

# modules/vpc/main.tf
resource "aws_vpc" "this" {
  cidr_block           = var.cidr
  enable_dns_hostnames = true

  tags = {
    Name = var.name
  }
}

# modules/vpc/outputs.tf
output "vpc_id" {
  value       = aws_vpc.this.id
  description = "ID của VPC vừa tạo"
}

Và cách bên ngoài gọi module này:

# main.tf (root module)
module "vpc_production" {
  source = "./modules/vpc"
  cidr   = "10.0.0.0/16"
  name   = "vietcos-prod"
}

# Dùng output của module ở nơi khác
resource "aws_subnet" "app" {
  vpc_id     = module.vpc_production.vpc_id
  cidr_block = "10.0.1.0/24"
}

Ba khái niệm bạn cần khắc cốt ghi tâm:

  • source: chỉ ra module nằm ở đâu. Có thể là đường dẫn local (./modules/vpc), Git (git::https://...), Terraform Registry (terraform-aws-modules/vpc/aws), hoặc S3.
  • input variables: cách bên gọi truyền tham số vào module. Chính là var.cidr, var.name bên trong module.
  • outputs: cách module "trả về" giá trị. Bên gọi truy cập qua module.<tên>.<output>.

Vì sao module quan trọng

Module mang lại bốn lợi ích mà các đội trưởng thành đều theo đuổi. Thứ nhất là DRY (Don't Repeat Yourself) — viết logic hạ tầng một lần. Thứ hai là nhất quán — mọi VPC tạo ra từ module đều bật enable_dns_hostnames, không ai quên. Thứ ba là dễ review — thay đổi module là một PR nhỏ gọn thay vì rải rác khắp nơi. Thứ tư là đóng gói kiến thức — người mới chỉ cần biết truyền cidr và name, không cần hiểu chi tiết bên trong.

Workspace là gì

Workspace giải quyết một bài toán khác: quản lý nhiều state file từ cùng một bộ code. Nhớ lại bài trước — Terraform lưu trạng thái hạ tầng vào một file state. Mỗi workspace có một state riêng biệt. Khi bạn tạo workspace staging, Terraform lưu state của staging tách khỏi state của default.

terraform workspace new staging      # tạo workspace mới
terraform workspace list             # liệt kê các workspace
terraform workspace select staging   # chuyển sang staging
terraform workspace show             # xem workspace hiện tại

Trong code, bạn truy cập tên workspace hiện tại qua biến terraform.workspace:

resource "aws_instance" "app" {
  instance_type = terraform.workspace == "production" ? "t3.large" : "t3.micro"

  tags = {
    Name        = "app-${terraform.workspace}"
    Environment = terraform.workspace
  }
}

Nhờ đó, cùng một file main.tf có thể tạo t3.large cho production và t3.micro cho staging — chỉ khác nhau workspace đang chọn.

Module và Workspace khác nhau ra sao

Đây là điểm người mới hay nhầm. Module trả lời câu hỏi "tổ chức code thế nào" — đóng gói và tái sử dụng. Workspace trả lời câu hỏi "quản lý nhiều bản triển khai thế nào" — nhiều state độc lập. Chúng bổ trợ nhau: bạn dùng module để xây "khuôn", rồi có thể dùng workspace để đúc nhiều "bản" từ khuôn đó cho từng môi trường. Nhưng như phần sau sẽ bàn, workspace không phải lúc nào cũng là cách tốt nhất để tách môi trường.

Tình huống thực tế

Ví dụ 1: Startup fintech chuẩn hóa hạ tầng bằng module

Công ty MoMoPay (giả định) có 6 microservice, mỗi service cần một cụm hạ tầng gần giống nhau: một RDS PostgreSQL, một ElastiCache Redis, một ECS service. Ban đầu mỗi kỹ sư tự viết Terraform cho service của mình. Kết quả: service A dùng db.t3.medium, service B dùng db.t3.small, service C quên bật backup, service D đặt tên tài nguyên theo kiểu riêng.

Sau một sự cố mất dữ liệu vì một database không bật backup, đội DevOps quyết định gom lại thành một module chuẩn modules/service-stack. Module này nhận vào service_name, db_size, redis_size và mặc định bật backup_retention_period = 7 — không ai tắt được nếu không cố tình. Sau khi migrate, code cho mỗi service từ ~120 dòng rút xuống còn 8 dòng gọi module.

Bài học: Module không chỉ để đỡ gõ phím. Giá trị lớn nhất là ép buộc chuẩn mực an toàn vào một chỗ duy nhất. Một quyết định đúng (bật backup) được áp dụng cho toàn bộ hệ thống, thay vì phụ thuộc vào việc từng người có nhớ hay không.

Ví dụ 2: Sàn thương mại điện tử và bẫy workspace cho môi trường

Một sàn TMĐT ở Hà Nội, đội hạ tầng 3 người, dùng workspace để tách dev, staging, production — tất cả từ một bộ code và một backend S3. Ban đầu mọi thứ đẹp đẽ. Vấn đề nảy sinh khi một kỹ sư mới chạy terraform destroy mà quên kiểm tra mình đang ở workspace nào. Anh ta tưởng đang ở dev, nhưng terminal đang chọn production. Vì code giống hệt nhau giữa các workspace, không có gì cảnh báo. Kết quả: 20 phút downtime giờ cao điểm buổi tối, thiệt hại đơn hàng đáng kể.

Sau sự cố, đội chuyển sang mô hình thư mục riêng cho mỗi môi trường: environments/dev/, environments/staging/, environments/production/, mỗi thư mục có backend state riêng và gọi chung các module trong modules/. Bây giờ để đụng vào production, bạn phải cd vào đúng thư mục production — không thể "vô tình" nhầm môi trường.

Bài học: Workspace tuyệt vời cho các biến thể ngắn hạn, giống nhau (ví dụ mỗi feature branch một môi trường tạm). Nhưng với các môi trường có mức độ nghiêm trọng khác nhau như dev và production, tách bằng thư mục an toàn hơn nhiều vì nó tạo ra "rào chắn vật lý", giảm rủi ro thao tác nhầm và cho phép mỗi môi trường có backend, quyền truy cập riêng.

Ví dụ 3: Team dùng module từ Terraform Registry và bài học versioning

Một công ty gia công phần mềm ở Đà Nẵng dùng module VPC công khai từ Terraform Registry: terraform-aws-modules/vpc/aws. Họ khai báo source mà không ghim version. Một sáng, CI pipeline đỏ lòm: terraform plan báo phải xóa và tạo lại toàn bộ subnet. Nguyên nhân: module đã ra bản major mới với breaking change, và vì không ghim version, Terraform tự kéo bản mới nhất.

Họ sửa bằng cách thêm version = "5.1.2" và bật # tflint kiểm tra bắt buộc ghim version:

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.1.2"   # LUÔN ghim version với module từ registry
  # ...
}

Bài học: Với module từ Registry hoặc Git, luôn ghim version cụ thể. Module không ghim version giống như npm install không có lockfile — nó sẽ cắn bạn vào thời điểm bạn ít ngờ nhất. Với module local (./modules/...) thì không cần vì chúng đi cùng repo của bạn.

Hướng dẫn từng bước

Hãy xây một cấu trúc dự án chuẩn dùng module, tách môi trường bằng thư mục — mô hình được khuyên dùng cho hầu hết dự án production.

Bước 1 — Dựng cấu trúc thư mục:

infra/
├── modules/
│   └── vpc/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
└── environments/
    ├── dev/
    │   ├── main.tf
    │   └── backend.tf
    └── production/
        ├── main.tf
        └── backend.tf

Bước 2 — Viết module có thể tái sử dụng trong modules/vpc/. Đặt tất cả input vào variables.tf, tất cả giá trị trả về vào outputs.tf, logic vào main.tf. Nguyên tắc vàng: module không được hardcode giá trị đặc thù môi trường — mọi thứ khác nhau giữa dev và prod phải đi qua variable.

Bước 3 — Gọi module trong từng môi trường. Trong environments/production/main.tf:

module "vpc" {
  source = "../../modules/vpc"
  cidr   = "10.0.0.0/16"
  name   = "prod"
}

Trong environments/dev/main.tf gọi cùng module nhưng với cidr = "10.10.0.0/16" và name = "dev".

Bước 4 — Cấu hình backend riêng cho mỗi môi trường trong backend.tf, trỏ đến key S3 khác nhau (ví dụ prod/terraform.tfstate và dev/terraform.tfstate). Đây là chốt chặn an toàn quan trọng nhất.

Bước 5 — Khởi tạo và apply từng môi trường độc lập:

cd environments/dev
terraform init
terraform plan
terraform apply

Lưu ý: mỗi lần thêm hoặc thay đổi khối module, bạn phải chạy lại terraform init để Terraform tải/liên kết module. Đây là lỗi người mới hay gặp — sửa module xong chạy plan ngay và không thấy thay đổi vì quên init.

Bước 6 (tùy chọn) — Nếu dùng workspace cho các biến thể ngắn hạn thay vì thư mục, hãy luôn hiển thị workspace hiện tại trong prompt terminal của bạn để không bao giờ thao tác nhầm. Thêm terraform.workspace vào cấu hình shell prompt hoặc dùng plugin như terraform-prompt.

Lỗi thường gặp & mẹo

Quên terraform init sau khi đổi module. Triệu chứng: sửa code trong module mà plan không phản ánh, hoặc lỗi "module not installed". Cứ đổi source hay thêm module mới là init lại.

Không ghim version cho module từ registry/Git. Như ví dụ 3, đây là nguồn gốc của những sự cố "sáng thứ Hai bỗng dưng hỏng". Luôn version = "x.y.z" (registry) hoặc ?ref=v1.2.3 (Git).

Nhầm workspace default. Nhiều người tưởng phải có workspace tên production mới an toàn, nhưng thực ra khi mới init, bạn đang ở workspace default. Nếu vô tình apply production vào default, bạn sẽ có state lẫn lộn. Luôn terraform workspace show trước khi làm việc quan trọng.

Dùng workspace để tách môi trường có rủi ro khác nhau. Đây là cái bẫy kinh điển. Workspace chia sẻ cùng backend và code, dễ nhầm lẫn, khó phân quyền riêng dev/prod. Với môi trường nghiêm túc, ưu tiên tách bằng thư mục.

Module quá lớn hoặc quá nhỏ. Module ôm cả hạ tầng thì mất tính tái sử dụng; module chỉ bọc một resource duy nhất thì thêm phức tạp mà chẳng lợi gì. Mẹo: gom những tài nguyên luôn được tạo và xóa cùng nhau vào một module.

Mẹo hay: Luôn khai báo description cho mọi variable và output. Terraform Registry sẽ tự sinh tài liệu từ đó, và đồng đội đọc code sẽ hiểu ngay ý nghĩa từng tham số. Ngoài ra, dùng terraform.workspace trong tags để mọi tài nguyên đều biết mình thuộc môi trường nào — cực kỳ hữu ích khi truy vết chi phí và debug.

Bài tập thực hành

  • Tạo module đầu tiên: Viết một module modules/vpc nhận cidr và name, tạo một aws_vpc, và output vpc_id. Gọi nó từ root module với hai instance khác nhau (vpc_a và vpc_b). Chạy terraform init rồi plan, quan sát Terraform tạo hai VPC riêng biệt.
  • Tách môi trường bằng thư mục: Dựng cấu trúc environments/dev và environments/production, mỗi thư mục gọi module VPC với CIDR khác nhau và backend key khác nhau. Chạy init + plan ở cả hai và xác nhận state của chúng độc lập.
  • Thử nghiệm workspace: Trong một dự án, tạo workspace blue và green. Viết resource dùng terraform.workspace để đặt tag Environment. Chuyển qua lại giữa hai workspace, chạy plan, và quan sát cách state tách biệt. Sau đó viết vài dòng so sánh: với môi trường thật của bạn, workspace hay thư mục phù hợp hơn, vì sao?
  • (Nâng cao) Dùng một module VPC công khai từ Terraform Registry, ghim version cụ thể, rồi thử đổi sang bản major kế tiếp và chạy plan để tận mắt thấy breaking change trông thế nào.

Tóm tắt

Module và workspace là hai công cụ tổ chức nền tảng của Terraform ở quy mô đội nhóm. Module là một thư mục chứa file .tf, đóng gói một cụm tài nguyên với input variables và outputs, giúp bạn tái sử dụng, giữ nhất quán và ép chuẩn an toàn vào một chỗ duy nhất — nhớ luôn ghim version cho module từ registry và chạy lại init mỗi khi thay đổi module. Workspace cho phép quản lý nhiều state độc lập từ cùng một bộ code, truy cập qua terraform.workspace, rất hợp cho các biến thể ngắn hạn giống nhau.

Điểm mấu chốt cần nhớ: với các môi trường có mức độ nghiêm trọng khác nhau như dev và production, tách bằng thư mục thường an toàn hơn workspace vì nó tạo rào chắn vật lý chống thao tác nhầm và cho phép mỗi môi trường có backend, quyền hạn riêng. Ba sự cố thực tế trong bài — mất dữ liệu vì thiếu chuẩn, downtime vì nhầm workspace, và breaking change vì không ghim version — đều là những bài học mà các đội DevOps trưởng thành đã trả giá để học được. Hãy để chúng trở thành kinh nghiệm miễn phí của bạn. Ở bài tiếp theo, chúng ta sẽ đi sâu vào Terraform Best Practices & Patterns để hoàn thiện tư duy tổ chức code hạ tầng.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi