Anh em làm Infra-as-Code (IaC) với Terraform chắc từng hoặc sắp trải qua cảnh mở repo ra thấy chục file *.tf rải rác, không biết đâu là config chính, đâu là resource thừa. Hầu hết các dự án đều bắt đầu gọn gàng, rồi nhanh chóng rơi vào cảnh cấu trúc lộn xộn vì không modules hóa ngay từ đầu. Kết quả là: code rườm rà, debug khó, và ai cũng ngại chạy lệnh terraform apply.
Hôm nay, mình sẽ chia sẻ về cách tổ chức project Terraform sao cho clean, dễ đọc, và quan trọng nhất là dễ scale. Mình sẽ đi thẳng vào vấn đề: từ việc dùng Module sao cho hiệu quả, cách quản lý State an toàn nhất, đến lúc nào thì cần chuyển sang Terragrunt.

Vấn đề thường gặp khi mới bắt đầu
Hầu hết bạn mới làm quen với Terraform đều có xu hướng tổ chức code theo kiểu này:
project/
├── ec2.tf
├── s3.tf
├── lambda.tf
├── variables.tf
├── outputs.tf
└── provider.tf
Nhìn thì gọn, mỗi file một loại dịch vụ service type, nhưng thực tế lại rối.
Vấn đề ở đây là gì? Hạ tầng của chúng ta không được xây dựng theo kiểu chia nhỏ theo từng service (mỗi file một loại resource như S3 hay EC2), mà nó hoạt động theo Component (thành phần logic).
Ví dụ: Một cụm Web App bao gồm: EC2, Security Group, Load Balancer và DNS. Theo cách cũ, bạn phải nhảy qua nhiều file khác nhau chỉ follow một luồng phụ thuộc đơn giản. Khi code lớn lên, việc maintain và debug sẽ rất rối.
Giải pháp tổ chức modules hóa
Cách tiếp cận tốt nhất và dễ scale nhất là sử dụng modules tổ chức code theo feature hoặc component. Bạn hãy coi modules như những building blocks có thể tái sử dụng cho hạ tầng của mình.
Cấu trúc đơn giản nhưng có khả năng scale:
project/
├── main.tf
├── variables.tf
├── outputs.tf
├── provider.tf
├── README.md
└── modules/
├── app-vpc/
├── s3-storage/
└── rds-database/
Giải thích:
main.tf: Là entry point duy nhất. Nó chỉ làm nhiệm vụ call tới các module và truyền variables cần thiết vào.modules/: Thư mục chứa các logic tái sử dụng. Mỗi module sẽ cómain.tf,variables.tf, vàoutputs.tfriêng để định nghĩa một khối chức năng hoàn chỉnhprovider.tf: Đặt cấu hình Provider (AWS, Azure, GCP) chung ở đây. Tránh lặp lại trong các module con.
Hãy coi Modules như functions trong lập trình. Khi bạn cần triển khai 3 cái VPC hay 5 cặp Load Balancer giống nhau, chỉ cần gọi cùng một Module với các tham số khác nhau.
Ví dụ:
project/main.tf
# Gọi module để tạo một Load Balancer và nhóm EC2 cho Web App
module "app_alb_group" {
source = "./modules/web_backend"
# Truyền biến cấu hình riêng
instance_count = 3
instance_type = var.app_instance_size
vpc_id = module.networking.vpc_id
}
# Gọi module để tạo một S3 Bucket lưu trữ Log
module "log_bucket" {
source = "./modules/storage_s3"
bucket_name = "project-log-storage-${var.environment}"
is_public = false
enable_kms = true
}
Với cách này, việc thêm một thành phần mới chỉ là việc thêm một block module đơn giản, không còn tình trạng copy-paste code rác nữa
Quản lý enviroment
Khi bạn bắt đầu có nhiều môi trường (dev, staging, prod), việc quản lý qua branch-based (nhánh Git) sẽ sớm bộc lộ hạn chế vì mỗi môi trường cần cấu hình khác biệt. Mình khuyên bạn nên dùng directory-based để tách biệt hoàn toàn.
project/
├── modules/ # chia module có thể tái sử dụng
│ ├── app-vpc/
│ └── web-server/
└── environments/ # Working Directory độc lập
├── dev/
│ ├── main.tf # Gọi modules với Dev
│ └── backend.tf # State riêng
├── staging/
└── prod/ # Environment Prod
Mỗi thư mục trong environments/ là một Working Directory độc lập. Chúng cùng gọi chung Module trong modules/, nhưng truyền tham số khác nhau để tạo ra sự khác biệt về cấu hình.
Ví dụ về sự khác biệt:
environments/dev/main.tf:
# Triển khai Web App ở environment Dev
module "web_app" {
source = "../../modules/web-server"
instance_type = "t3.micro" # instance nhỏ, tiết kiệm chi phí
is_public = true # mở public cho dễ debug/test
}
environments/prod/main.tf:
# Triển khai Web App ở environment Prod
module "web_app" {
source = "../../modules/web-server"
instance_type = "m5.large" # instance lớn, hiệu năng cao
is_public = false # không bật public, an toàn hơn
enable_monitoring = true # logging/monitoring
}
Tóm lại: Dùng chung module, nhưng linh hoạt cấu hình theo yêu cầu của từng environment.
Quản lý Terraform State
State file (terraform.tfstate) là linh hồn của hạ tầng (mất nó → mất hết). Nên bạn quản lý State phải cực kỳ cẩn thận.
Luôn dùng Remote Backend
Không bao giờ lưu State File trên máy cá nhân. Sử dụng Remote Backend: S3 (kèm DynamoDB cho Lock), GCS, Azure Blob Storage, hoặc các giải pháp chuyên nghiệp như Terraform Cloud, GitLab HTTP backend.
Ví dụ cấu hình S3 Backend (phân tách theo môi trường):
terraform {
backend "s3" {
# Tên Bucket chỉ chưa state file
bucket = "project-company-tf-states"
# key phải phân tách theo env
key = "prod/vpc/terraform.tfstate"
region = "ap-southeast-1"
# Dùng DynamoDB Table để lock State, tránh apply cùng lúc
dynamodb_table = "terraform-statelock"
encrypt = true
}
}
State Locking và Versioning
- State Locking: Cực kỳ quan trọng, Nếu bạn chạy
terraform applycùng lúc, file state sẽ corrupted, dẫn đến thảm họa. DynamoDB là giải pháp locking phổ biến nhất khi dùng S3. - Versioning: Bật Versioning trên S3 Bucket (hoặc tính năng tương đương trên các Backend khác). Nếu lỡ tay xóa hoặc có lỗi, bạn có thể rollback về trạng thái cũ.
Khi nào cần dùng terragrunt?
Khi bạn đã dùng Terraform thuần thục, nhưng dự án bắt đầu chạm trần về quy mô:
- Quản lý 5+ environment (Dev, QA, Staging, Prod, UAT…).
- Quản lý 3+ AWS/Azure/GCP Account.
- Cảm thấy lặp đi lặp lại việc copy/paste cấu hình Backend, Provider cho mỗi environment/module.
Terragrunt là gì?
Terragrunt là một wrapper xung quanh Terraform, được tạo ra để giải quyết sự lặp lại trong cấu hình. Nó cho phép bạn định nghĩa cấu hình Backend, Provider, và các biến chung một lần duy nhất trong file terragrunt.hcl và áp dụng cho tất cả các folder/environment con.
Cấu trúc ví dụ với Terragrunt:
infrastructure/
├── _config/ # Nơi chứa config chung (Backend, Provider)
│ └── terragrunt.hcl
└── environments/
├── dev/
│ └── app-database/
│ └── terragrunt.hcl # File này kế thừa config từ _config
└── prod/
└── app-database/
└── terragrunt.hcl # File này tự động tạo S3 key khác cho Prod
Khi nào không nên dùng Terragrunt?
- Code chưa ổn định: Terragrunt thêm một lớp abstraction vào. Nếu bạn vẫn đang loay hoay với việc viết module, Terragrunt sẽ làm mọi thứ phức tạp hơn.
- Cần tốc độ lặp lại nhanh: Lớp abstraction này đôi khi làm chậm quá trình sửa lỗi và kiểm tra.
Lời khuyên của mình: Hãy làm chủ Terraform thuần thục trước, khi quy mô của hạ tầng và số lượng environment bắt đầu phình to, đó là lúc nghĩ đến Terragrunt.
Tips triển khai môi trường Production
- Chỉ định variable rõ ràng: Không dùng giá trị mặc định cho các tham số quan trọng của Prod (như
instance_type,min_scaling_group). Luôn phải khai báo rõ ràng. - Bảo mật S3 Backend: S3 Bucket chứa State phải bị chặn truy cập public, được mã hóa bằng KMS, và Chỉ cho phép CI/CD Runner (là người chạy lệnh
apply) được phép truy cập. - Bật Log và Monitoring: Mọi thứ phải có log. Bật CloudWatch logs cho EC2, Flow Logs cho VPC, và đảm bảo mọi resouce đều có
tagsđể theo dõi chi phí. - CI/CD phải duyệt thủ công: Tự động deploy lên
dev/stagingđược, nhưngproductionbắt buộc phải có một người duyệt trước khiterraform applychạy. - Tagging: Gắn thẻ
Environment,Owner,Cost Centerlên tất cả resouce. Giúp bạn kiểm soát chi phí và dọn dẹp sau này. - Validate/Fmt trong CI/CD: Chạy
terraform validatevàterraform fmttrong pipeline. Giúp bắt lỗi cấu hình sớm và đồng bộ style code cho cả team. - Loại bỏ Secrets ra khỏi Terraform: Tuyệt đối không đưa
password,API keyvào code Terraform. Hãy sử dụng dịch vụ secrets chuyên dụng (như Secrets Manager, Vault, SSM Parameter Store). - Chia nhỏ State File: Nếu dự án quá lớn, hãy tách nó ra thành nhiều State File nhỏ (mỗi module hoặc mỗi component chính một state). Giúp
terraform planchạy nhanh hơn và cô lập rủi ro.




