Mở đầu — vì sao bài này quan trọng
Ở ba bài trước, bạn đã học cách viết Terraform: provider, resource, state, module và workspace. Bạn có thể terraform apply để tạo một VPC hay một database. Nhưng có một khoảng cách rất lớn giữa "Terraform chạy được" và "Terraform chạy được trong một team 20 người, trên production, suốt ba năm mà không ai sợ nhấn nút apply".
Khoảng cách đó chính là nội dung của bài này. Best practices không phải là những quy tắc học thuộc để làm màu trong phỏng vấn. Chúng là kết tinh từ hàng nghìn sự cố thật: state bị corrupt lúc 2 giờ sáng, hai kỹ sư apply chồng lên nhau xóa mất database production, một thay đổi tưởng vô hại làm downtime cả hệ thống thanh toán. Mỗi best practice bạn học hôm nay đều có một câu chuyện đau thương đứng sau.
Tôi muốn bạn tiếp cận bài này với tâm thế của người sắp phải chịu trách nhiệm cho hạ tầng thật. Khi code Terraform của bạn quản lý cụm Kubernetes phục vụ hàng triệu người dùng, cấu trúc repo và cách bạn tổ chức state không còn là chuyện "gu cá nhân" — nó quyết định team của bạn ngủ ngon hay trực đêm. Hãy học như thể ngày mai bạn phải bàn giao codebase này cho người khác.
Khái niệm cốt lõi
1. Cấu trúc repo: tách module khỏi môi trường
Nguyên tắc nền tảng nhất: module là thành phần tái sử dụng, environment là nơi lắp ráp. Một cấu trúc repo lành mạnh thường trông như sau:
infra/
├── modules/
│ ├── vpc/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── eks/
│ └── rds/
└── envs/
├── prod/
│ ├── main.tf # gọi các module với giá trị của prod
│ ├── backend.tf # state riêng của prod
│ └── terraform.tfvars
├── staging/
└── dev/
Ý tưởng cốt lõi: modules/ chứa "công thức" không biết gì về môi trường cụ thể. envs/prod/main.tf chỉ làm nhiệm vụ gọi module và truyền tham số (số node, kích thước instance, CIDR). Nhờ vậy khi bạn nâng cấp module eks, cả prod, staging, dev đều được hưởng lợi mà không phải copy-paste code.
Một lỗi kinh điển của người mới là để chung tất cả môi trường trong một state duy nhất, hoặc dùng cấu trúc lồng module quá sâu (module gọi module gọi module bốn năm tầng). Cả hai đều làm bán kính ảnh hưởng (blast radius) khi có sự cố phình to.
2. Tách state — nguyên tắc "blast radius"
State là trái tim của Terraform, và cũng là điểm yếu chết người. Nếu toàn bộ hạ tầng (network, database, cluster, DNS) nằm chung một state file, thì mỗi lần apply bạn đang đặt cược cả hệ thống. Một terraform plan chạy hàng chục phút, và một lỗi nhỏ có thể lan ra toàn bộ.
Nguyên tắc: chia state theo ranh giới thay đổi và vòng đời. Network ít khi thay đổi thì tách riêng. Database — thứ tuyệt đối không được lỡ tay xóa — tách riêng. Application layer thay đổi hàng ngày thì tách riêng. Các state giao tiếp với nhau qua terraform_remote_state hoặc qua data source (đọc giá trị output của state khác).
3. Remote state và state locking
Không bao giờ để state file trên máy cá nhân. State phải nằm ở remote backend (S3 + DynamoDB lock, GCS, Terraform Cloud, hoặc Azure Blob). Hai lý do sống còn: chia sẻ state cho cả team, và locking — cơ chế ngăn hai người apply cùng lúc gây hỏng state. Thiếu locking là công thức chắc chắn dẫn tới thảm họa khi team đông lên.
4. Immutable, DRY và versioning cho module
Ba nguyên tắc bổ trợ:
- DRY qua module, không qua copy-paste. Nếu bạn thấy mình sao chép 50 dòng resource cho môi trường thứ hai, đó là tín hiệu cần tạo module.
- Version hóa module. Module dùng chung nên được pin theo version (git tag hoặc registry version), ví dụ
source = "git::...//modules/eks?ref=v1.4.2". Không pin version nghĩa là một thay đổi trong module có thể vô tình phá vỡ prod ngay lần apply tới. - Ưu tiên immutable. Thay vì sửa tại chỗ, hãy thay thế. Tận dụng
create_before_destroycho các resource quan trọng để tránh downtime.
5. Naming, tagging và convention
Đặt tên và gắn tag nhất quán không phải chuyện thẩm mỹ. Tag environment, owner, cost-center, managed-by = terraform là thứ giúp bạn truy vết chi phí, biết ai chịu trách nhiệm, và phân biệt tài nguyên do Terraform quản lý với tài nguyên tạo tay. Với các công ty ở Việt Nam mới lên cloud, thiếu tagging là nguyên nhân số một khiến hóa đơn AWS cuối tháng trở thành một mê cung không ai giải được.
Tình huống thực tế
Ví dụ 1: Tiki và bài học state khổng lồ
Giả định một đội hạ tầng tại một sàn thương mại điện tử lớn như Tiki, giai đoạn đầu chuyển lên AWS. Ban đầu chỉ có ba kỹ sư, họ để toàn bộ hạ tầng — VPC, 4 cụm EKS, RDS, ElastiCache, hàng chục security group — trong một state file duy nhất trên S3.
Sau một năm, state phình lên hơn 4.000 resource. Mỗi terraform plan mất gần 12 phút. Tệ hơn, một hôm một kỹ sư muốn thêm một security group nhỏ cho service mới, nhưng plan bất ngờ hiện ra thay đổi ở một RDS instance production — do drift từ một thay đổi tay của người khác. Anh ta không dám apply vì sợ ảnh hưởng database, nên cả thay đổi nhỏ bị chặn đứng suốt ba ngày chờ họp.
Bài học: Họ buộc phải làm phẫu thuật terraform state mv để tách thành các state theo layer: network, data (RDS, cache), và mỗi cụm EKS một state riêng. Sau khi tách, plan cho application layer chỉ còn dưới một phút, và không thay đổi nào ở app còn có nguy cơ đụng tới database. Bán kính ảnh hưởng thu nhỏ chính là mục tiêu.
Ví dụ 2: Một startup fintech ở Singapore và module không pin version
Một startup fintech Đông Nam Á có ba môi trường dùng chung một module eks nội bộ, tham chiếu thẳng nhánh main của repo module: source = "git::...//eks" — không có ?ref=.
Một kỹ sư cập nhật module main để đổi phiên bản Kubernetes mặc định từ 1.27 lên 1.29 cho một dự án mới. Vài ngày sau, một người khác chạy apply định kỳ trên staging — và Terraform ngay lập tức lên kế hoạch nâng cấp control plane của staging vì module đã đổi. Do fintech này chịu ràng buộc compliance (họ đang chuẩn bị SOC2), việc thay đổi ngoài kế hoạch trên môi trường gần-production là vi phạm quy trình change management.
Bài học: Họ áp dụng quy tắc bắt buộc pin version cho mọi module dùng chung: source = "git::...//eks?ref=v2.1.0". Nâng cấp module giờ trở thành một thay đổi có chủ đích — bump version trong từng env theo thứ tự dev → staging → prod, review qua pull request. Một dòng ?ref= đã biến sự cố ngẫu nhiên thành quy trình có kiểm soát.
Ví dụ 3: VNG và chuẩn hóa CI cho terraform plan
Giả định một đội platform tại một công ty công nghệ lớn của Việt Nam như VNG. Ban đầu ai cũng apply từ laptop cá nhân. Có lần hai kỹ sư apply gần như đồng thời vào cùng một môi trường không bật locking; state ghi đè lẫn nhau và một load balancer bị "mồ côi" — tồn tại trên AWS nhưng biến mất khỏi state, gây tốn tiền âm thầm suốt hai tháng.
Bài học: Đội chuyển sang mô hình GitOps-lite cho Terraform: bật DynamoDB lock, cấm apply từ laptop, mọi thay đổi phải qua pull request. CI tự động chạy terraform fmt -check, validate, và plan, rồi đính kèm output plan vào PR để reviewer đọc trước khi merge. Chỉ pipeline mới có quyền apply. Sự cố apply chồng lấn biến mất hoàn toàn, và mỗi thay đổi hạ tầng giờ có dấu vết review rõ ràng — điều mà auditor rất thích.
Hướng dẫn từng bước
Giả sử bạn đang thiết lập một repo Terraform mới cho một team, đây là trình tự tôi khuyên bạn làm:
Bước 1 — Dựng khung thư mục theo module/env. Tạo modules/ cho các thành phần tái sử dụng và envs/{dev,staging,prod}/ cho các môi trường. Đừng để môi trường lẫn vào module.
Bước 2 — Cấu hình remote backend có locking. Với AWS, tạo một S3 bucket (bật versioning) và một DynamoDB table cho lock. Mỗi env trỏ tới một key state riêng:
# envs/prod/backend.tf
terraform {
backend "s3" {
bucket = "acme-tfstate"
key = "prod/network/terraform.tfstate"
region = "ap-southeast-1"
dynamodb_table = "acme-tf-lock"
encrypt = true
}
}
Bước 3 — Viết module với interface rõ ràng. Mỗi module có variables.tf (đầu vào), main.tf (logic), outputs.tf (đầu ra). Đặt giá trị mặc định hợp lý và ghi description cho mọi biến.
Bước 4 — Lắp ráp ở tầng env. File main.tf của prod chỉ gọi module và truyền tham số riêng của prod:
module "eks" {
source = "git::https://git.acme.vn/infra//modules/eks?ref=v1.4.2"
cluster_name = "prod-main"
node_count = 6
instance_type = "m5.xlarge"
tags = {
environment = "prod"
owner = "platform-team"
managed-by = "terraform"
}
}
Bước 5 — Chuẩn hóa format và validate. Chạy terraform fmt -recursive và terraform validate. Cân nhắc thêm tflint để bắt lỗi cấu hình và checkov/tfsec để quét bảo mật.
Bước 6 — Đưa vào CI. Pipeline chạy fmt -check, validate, plan trên mỗi PR; chỉ apply sau khi merge. Bật lock để tránh apply chồng lấn.
Bước 7 — Tách state theo blast radius. Ngay từ đầu, tách ít nhất ba tầng: network, data (database/cache), application. Chia sẻ giá trị giữa các state qua data source thay vì gộp chung.
Lỗi thường gặp & mẹo
- Hardcode giá trị thay vì dùng variable. ARN, subnet id, region viết cứng khiến module không tái sử dụng được. Luôn tham số hóa.
- Không pin version provider và module. Dùng
required_providersvới ràng buộc version (ví dụ~> 5.0) và pin module theo?ref=. Thiếu điều này, một ngày đẹp trời provider tự nâng cấp và plan của bạn khác hẳn. - Chỉnh sửa state bằng tay. Đừng bao giờ mở state file lên sửa JSON. Nếu cần di chuyển resource, dùng
terraform state mv; nếu cần đưa resource có sẵn vào quản lý, dùngterraform import(hoặc blockimport). - Nhồi bí mật vào tfvars rồi commit. Password, token không bao giờ được commit. Dùng biến môi trường, secret manager, hoặc
data sourceđọc từ Vault/SSM. Nhớ rằng state file chứa giá trị dạng plaintext — nên phải bật encryption cho backend và giới hạn quyền truy cập. - Lạm dụng
countkhi nên dùngfor_each.countdùng chỉ số; xóa phần tử giữa danh sách sẽ làm dịch chỉ số và Terraform phá đi tạo lại các resource sau nó.for_eachdùng key ổn định, an toàn hơn nhiều. - Bỏ qua
plantrướcapply. Luôn đọc kỹ plan. Chú ý những dòng bắt đầu bằng-/+(destroy rồi create lại) — đó là dấu hiệu downtime tiềm ẩn. - Mẹo: Với resource sống còn như RDS production, thêm
lifecycle { prevent_destroy = true }để Terraform từ chối xóa. Đây là "chốt an toàn" đã cứu vô số kỹ sư khỏi thảm họa. - Mẹo: Dùng
terraform plan -out=plan.binrồiapply plan.binđể đảm bảo thứ được apply chính xác là thứ đã review, không bị drift chen vào giữa.
Bài tập thực hành
- Tái cấu trúc repo. Cho một file
main.tfkhoảng 150 dòng gộp cả VPC, EC2 và security group cho hai môi trường. Hãy tách thành cấu trúcmodules/+envs/{dev,prod}/, đảm bảo dev và prod dùng chung module nhưng khác tham số.
- Thiết kế tách state. Cho một hệ thống gồm: VPC, RDS, một cụm EKS, và 5 microservice. Hãy vẽ sơ đồ bạn sẽ chia thành bao nhiêu state, mỗi state chứa gì, và giải thích quyết định dựa trên blast radius và tần suất thay đổi.
- Viết module có version. Tạo module
s3-bucketnhận vào tên bucket, cờ versioning và tags; xuất rabucket_arn. Sau đó gọi module này từ hai env, pin theo một git tag, và giải thích điều gì xảy ra nếu bạn đổi module mà không bump version ở env kia.
- Bịt lỗ hổng bảo mật. Cho một cấu hình đang commit password RDS thẳng trong
terraform.tfvars. Hãy chuyển sang đọc bí mật từ AWS SSM Parameter Store hoặc Vault, và giải thích vì sao vẫn cần bật encryption cho backend dù đã bỏ password khỏi code.
Tóm tắt
Terraform best practices xoay quanh một tư duy trung tâm: giảm bán kính ảnh hưởng và tăng khả năng kiểm soát. Tách module (công thức tái sử dụng) khỏi environment (nơi lắp ráp) để tránh copy-paste. Chia state theo vòng đời và ranh giới thay đổi để một sự cố không kéo cả hệ thống theo. Đặt state ở remote backend có locking để cả team làm việc an toàn. Pin version cho provider và module để thay đổi luôn có chủ đích chứ không xảy ra ngẫu nhiên. Chuẩn hóa naming, tagging, và đưa fmt/validate/plan vào CI để mọi thay đổi đều được review.
Ba câu chuyện — state khổng lồ, module không pin version, và apply chồng lấn — đều dạy cùng một bài học: những quyết định tưởng nhỏ ở giai đoạn đầu sẽ quyết định bạn có ngủ ngon khi team lớn lên hay không. Hãy áp dụng các nguyên tắc này ngay từ dự án đầu tiên, vì tái cấu trúc một hạ tầng đang chạy production luôn đắt hơn nhiều so với làm đúng từ đầu. Ở bài tiếp theo, chúng ta sẽ so sánh Ansible và Terraform để bạn biết khi nào nên dùng công cụ nào.