Terragrunt là gì? Quản lý Terraform code trên multiple environment/tenants với Terragrunt
Mở đầu
Nếu bạn đã làm việc với Terraform một thời gian, hẳn bạn sẽ quen với cảm giác này: mọi thứ chạy rất tốt khi bạn chỉ có một môi trường. Nhưng khi dự án lớn dần, bạn cần quản lý dev, staging, prod với hàng chục module, bạn sẽ bắt đầu thấy code bị lặp đi lặp lại ở khắp nơi.
Đó chính xác là vấn đề mà Terragrunt ra đời để giải quyết.
Trong bài viết này mình sẽ giới thiệu Terragrunt là gì, lợi ích của nó so với Terraform thuần, và hướng dẫn bạn thực hành qua một lab chạy hoàn toàn trên local — không cần AWS, GCP, hay bất kỳ cloud provider nào.
1. Vấn đề khi dùng Terraform ở scale
Hãy tưởng tượng bạn có một hạ tầng gồm 3 module:
network— quản lý VPC, subnetdatabase— quản lý RDS, database instanceapp— quản lý EC2, ECS hoặc bất kỳ compute nào
Và bạn cần deploy cho 2 môi trường: dev và prod.
Với Terraform thuần, cấu trúc thư mục của bạn trông sẽ thế này:
infrastructure/
├── dev/
│ ├── network/
│ │ ├── main.tf
│ │ ├── backend.tf ← lặp lại
│ │ └── provider.tf ← lặp lại
│ ├── database/
│ │ ├── main.tf
│ │ ├── backend.tf ← lặp lại
│ │ └── provider.tf ← lặp lại
│ └── app/
│ ├── main.tf
│ ├── backend.tf ← lặp lại
│ └── provider.tf ← lặp lại
└── prod/
├── network/
│ ├── main.tf
│ ├── backend.tf ← lặp lại
│ └── provider.tf ← lặp lại
├── database/
│ ├── main.tf
│ ├── backend.tf ← lặp lại
│ └── provider.tf ← lặp lại
└── app/
├── main.tf
├── backend.tf ← lặp lại
└── provider.tf ← lặp lại
2 môi trường × 3 module = 6 bản copy của backend.tf và provider.tf. Khi bạn cần đổi S3 bucket lưu state, bạn phải sửa cả 6 file. Khi thêm môi trường mới, bạn copy thêm 3 file nữa.
Đây chính là "copy-paste infrastructure" — một trong những vấn đề lớn nhất khi scale Terraform.
2. Terragrunt là gì?

Terragrunt là một thin wrapper (lớp bọc mỏng) bên trên Terraform, được phát triển bởi Gruntwork. Nó không thay thế Terraform mà bổ sung thêm các tính năng để giải quyết các điểm yếu của Terraform khi làm việc ở quy mô lớn.
Nói đơn giản: Terraform quản lý hạ tầng, còn Terragrunt quản lý các file cấu hình Terraform.
So sánh nhanh
| Terraform thuần | Terraform + Terragrunt | |
|---|---|---|
| Backend config | Lặp lại ở mỗi module | Định nghĩa 1 lần ở root |
| Provider config | Lặp lại ở mỗi module | Tự động generate cho mỗi module |
| Dependency giữa modules | Thủ công, dùng terraform_remote_state |
dependency block, tự động |
| Apply nhiều module | Phải chạy từng module theo thứ tự | terragrunt run --all apply |
| Environment variables | Copy/paste với thay đổi nhỏ | Đọc từ env.hcl dùng chung |
3. Các tính năng chính của Terragrunt
3.1 DRY — Don't Repeat Yourself
Đây là lợi ích cốt lõi. Terragrunt cho phép bạn định nghĩa backend, provider, và common inputs ở một file root duy nhất (live/terragrunt.hcl). Tất cả module con sẽ kế thừa thông qua include "root".
3.2 run --all — Apply/Destroy nhiều module cùng lúc
# Apply TẤT CẢ module trong thư mục hiện tại và subdirectories
terragrunt run --all apply
# Destroy TẤT CẢ
terragrunt run --all destroy
Terragrunt tự phân tích dependency graph và apply theo đúng thứ tự.
3.3 Dependency management
dependency "network" {
config_path = "../network"
}
inputs = {
# Lấy output của module network và truyền vào module database
network_id = dependency.network.outputs.network_id
}
Không cần terraform_remote_state. Terragrunt đọc output trực tiếp và đảm bảo thứ tự apply đúng.
3.4 generate block
Tự động tạo file .tf trong working directory của từng module khi chạy. Thường dùng để generate provider.tf và backend.tf.
3.5 Built-in functions
Terragrunt cung cấp nhiều hàm tiện ích:
find_in_parent_folders("root.hcl")— tìm fileroot.hcltrong thư mục charead_terragrunt_config()— đọc file HCL bất kỳ thành localsget_terragrunt_dir()— lấy absolute path của thư mục chứaterragrunt.hclhiện tạipath_relative_to_include()— path tương đối so với root include
4. Kiến trúc lab
Lab này mô phỏng việc deploy một ứng dụng gồm 3 thành phần lên 2 môi trường (dev và prod). Để không phụ thuộc cloud, mỗi "resource" chỉ đơn giản là tạo ra một file JSON trên local — nhưng cấu trúc và workflow hoàn toàn giống thực tế.
4.1 Cấu trúc thư mục
terragrunt-lab/
├── terraform-modules/ # Terraform modules thuần (không biết về Terragrunt)
│ ├── network/
│ │ ├── main.tf # Tạo network.json
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── database/
│ │ ├── main.tf # Tạo database.json
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── app/
│ ├── main.tf # Tạo app.json
│ ├── variables.tf
│ └── outputs.tf
│
└── live/ # Terragrunt configuration layer
├── root.hcl # ROOT: backend + provider + common inputs
├── dev/
│ ├── env.hcl # Biến riêng cho dev
│ ├── network/terragrunt.hcl
│ ├── database/terragrunt.hcl
│ └── app/terragrunt.hcl
└── prod/
├── env.hcl # Biến riêng cho prod
├── network/terragrunt.hcl
├── database/terragrunt.hcl
└── app/terragrunt.hcl
4.2 Dependency graph
network ──────────────────┐
└──→ database ────────┤
└──→ app
Module database phụ thuộc vào network (cần network_id).
Module app phụ thuộc vào cả network và database.
Khi chạy terragrunt run --all apply, Terragrunt tự động resolve graph này và apply theo đúng thứ tự.
5. Cài đặt
Cài Terraform
# Ubuntu / Debian
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
# macOS
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
Cài Terragrunt
# macOS / Linux (dùng brew)
brew install terragrunt
# Hoặc download binary trực tiếp từ GitHub Releases
# https://github.com/gruntwork-io/terragrunt/releases
# Ví dụ cho Linux amd64:
curl -Lo terragrunt https://github.com/gruntwork-io/terragrunt/releases/latest/download/terragrunt_linux_amd64
chmod +x terragrunt
sudo mv terragrunt /usr/local/bin/
Kiểm tra
terraform version # >= 1.0
terragrunt --version # >= 0.50
6. Bước 1 — Tạo Terraform modules
Các module này là Terraform thuần — chúng không biết gì về Terragrunt. Đây là best practice: viết module độc lập, sau đó dùng Terragrunt để orchestrate.
Module network
Module này nhận vào các thông số network và "tạo" cấu hình mạng (ghi ra file JSON).
terraform-modules/network/main.tf
resource "local_file" "network_config" {
content = jsonencode({
network_id = "${var.app_name}-${var.environment}-network"
cidr_block = var.cidr_block
subnets = var.subnets
environment = var.environment
})
filename = "${var.output_path}/network.json"
file_permission = "0644"
}
terraform-modules/network/variables.tf
variable "environment" {
description = "Environment name (dev, staging, prod)"
type = string
}
variable "app_name" {
description = "Application name"
type = string
}
variable "cidr_block" {
description = "CIDR block for the virtual network"
type = string
default = "10.0.0.0/16"
}
variable "subnets" {
description = "List of subnet CIDR blocks"
type = list(string)
default = ["10.0.1.0/24", "10.0.2.0/24"]
}
variable "output_path" {
description = "Local path to write the generated config file"
type = string
}
terraform-modules/network/outputs.tf
output "network_id" {
value = "${var.app_name}-${var.environment}-network"
}
output "cidr_block" {
value = var.cidr_block
}
Module database
Module database phụ thuộc vào network_id — trong Terraform thuần đây chỉ là một variable. Terragrunt sẽ tự động lấy giá trị này từ output của module network.
terraform-modules/database/main.tf
resource "local_file" "database_config" {
content = jsonencode({
db_id = "${var.app_name}-${var.environment}-db"
db_name = var.db_name
db_engine = var.db_engine
db_size = var.db_size
network_id = var.network_id
environment = var.environment
})
filename = "${var.output_path}/database.json"
file_permission = "0644"
}
terraform-modules/database/outputs.tf
output "db_id" {
value = "${var.app_name}-${var.environment}-db"
}
output "db_endpoint" {
value = "${var.app_name}-${var.environment}-db.local:5432"
}
Module app
Module app nhận network_id và db_id, db_endpoint từ 2 module trên.
terraform-modules/app/main.tf
resource "local_file" "app_config" {
content = jsonencode({
app_id = "${var.app_name}-${var.environment}"
app_version = var.app_version
instance_type = var.instance_type
replicas = var.replicas
network_id = var.network_id
db_id = var.db_id
db_endpoint = var.db_endpoint
environment = var.environment
app_url = "http://${var.app_name}-${var.environment}.local"
})
filename = "${var.output_path}/app.json"
file_permission = "0644"
}
7. Bước 2 — Cấu hình Terragrunt
7.1 root.hcl — Trái tim của DRY
Đây là file quan trọng nhất. Định nghĩa một lần, kế thừa ở mọi nơi.
live/root.hcl
# =============================================================================
# ROOT root.hcl — Shared configuration inherited by ALL modules
# =============================================================================
locals {
app_name = "myapp"
}
# DRY BENEFIT #1 — Remote state backend defined ONCE
# Không cần backend.tf trong từng module nữa!
# Path: live/<env>/<module>/terraform.tfstate
remote_state {
backend = "local"
config = {
path = "${get_parent_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate"
}
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
}
# DRY BENEFIT #2 — Provider configuration generated ONCE
# Terragrunt tự tạo file provider.tf cho mỗi module
generate "provider" {
path = "provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<-EOF
terraform {
required_version = ">= 1.0"
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
EOF
}
# DRY BENEFIT #3 — Common inputs truyền tự động xuống ALL modules
inputs = {
app_name = local.app_name
}
Hãy chú ý một số Terragrunt functions được sử dụng ở đây:
| Function | Ý nghĩa |
|---|---|
get_parent_terragrunt_dir() |
Absolute path của thư mục chứa root terragrunt.hcl (tức là live/) |
path_relative_to_include() |
Path của module hiện tại tương đối so với root (ví dụ: dev/network) |
Kết hợp lại, state file của live/dev/network sẽ được lưu tại: live/dev/network/terraform.tfstate ✓
7.2 env.hcl — Biến theo từng môi trường
live/dev/env.hcl
locals {
environment = "dev"
cidr_block = "10.0.0.0/16"
instance_type = "small"
db_size = "small"
replicas = 1
}
live/prod/env.hcl
locals {
environment = "prod"
cidr_block = "10.1.0.0/16"
instance_type = "large"
db_size = "large"
replicas = 3
}
Hai file này có cấu trúc hoàn toàn giống nhau, chỉ khác giá trị. Đây là cách Terragrunt đảm bảo environment parity.
7.3 Module terragrunt.hcl — Kế thừa và mở rộng
live/dev/network/terragrunt.hcl
locals {
# Đọc env.hcl của môi trường hiện tại
env_vars = read_terragrunt_config(find_in_parent_folders("env.hcl"))
}
# Kế thừa toàn bộ config từ live/root.hcl
include "root" {
path = find_in_parent_folders("root.hcl")
}
# Chỉ định Terraform module nào sẽ dùng
terraform {
source = "../../../terraform-modules/network"
}
# Inputs riêng của module này + common inputs từ root
inputs = {
environment = local.env_vars.locals.environment
cidr_block = local.env_vars.locals.cidr_block
subnets = ["${cidrsubnet(local.env_vars.locals.cidr_block, 8, 1)}", "${cidrsubnet(local.env_vars.locals.cidr_block, 8, 2)}"]
output_path = "${get_terragrunt_dir()}/output"
}
live/dev/database/terragrunt.hcl
locals {
env_vars = read_terragrunt_config(find_in_parent_folders("env.hcl"))
}
include "root" {
path = find_in_parent_folders("root.hcl")
}
terraform {
source = "../../../terraform-modules/database"
}
# DEPENDENCY: database cần biết network đã được tạo chưa
# Terragrunt sẽ đảm bảo network apply TRƯỚC database
dependency "network" {
config_path = "../network"
# Mock values dùng khi chạy validate/plan/destroy trước khi network được apply
mock_outputs = {
network_id = "mock-network-id"
}
mock_outputs_allowed_terraform_commands = ["validate", "plan", "destroy"]
}
inputs = {
environment = local.env_vars.locals.environment
db_size = local.env_vars.locals.db_size
# Lấy output của network module — Terragrunt tự đọc state của network
network_id = dependency.network.outputs.network_id
output_path = "${get_terragrunt_dir()}/output"
}
live/dev/app/terragrunt.hcl
locals {
env_vars = read_terragrunt_config(find_in_parent_folders("env.hcl"))
}
include "root" {
path = find_in_parent_folders("root.hcl")
}
terraform {
source = "../../../terraform-modules/app"
}
# App phụ thuộc vào CẢ HAI: network và database
dependency "network" {
config_path = "../network"
mock_outputs = {
network_id = "mock-network-id"
}
mock_outputs_allowed_terraform_commands = ["validate", "plan", "destroy"]
}
dependency "database" {
config_path = "../database"
mock_outputs = {
db_id = "mock-db-id"
db_endpoint = "mock-db.local:5432"
}
mock_outputs_allowed_terraform_commands = ["validate", "plan", "destroy"]
}
inputs = {
environment = local.env_vars.locals.environment
instance_type = local.env_vars.locals.instance_type
replicas = local.env_vars.locals.replicas
network_id = dependency.network.outputs.network_id
db_id = dependency.database.outputs.db_id
db_endpoint = dependency.database.outputs.db_endpoint
output_path = "${get_terragrunt_dir()}/output"
}
Lưu ý: File
terragrunt.hclcủaprod/network,prod/database,prod/appcó cấu trúc và nội dung y hệt vớidev/. Không có gì khác biệt! Vì sao? Vì sự khác biệt giữa 2 môi trường được tách hoàn toàn raenv.hcl. Đây là sức mạnh của Terragrunt.
8. Bước 3 — Chạy lab
8.1 Clone repo và kiểm tra cấu trúc
git clone <repo-url>
cd terragrunt-lab
8.2 Apply môi trường dev
cd live/dev
# Apply tất cả module (network → database → app) chỉ với 1 lệnh
terragrunt run --all apply
Terragrunt sẽ hỏi xác nhận:
Are you sure you want to run 'terragrunt apply' in each folder of the stack described above? (y/n)
Nhập y và quan sát thứ tự apply: network trước, rồi database, cuối cùng app.
8.3 Kiểm tra output
Sau khi apply xong, bạn sẽ thấy các file được tạo:
cat live/dev/network/output/network.json
{
"network_id": "myapp-dev-network",
"cidr_block": "10.0.0.0/16",
"subnets": ["10.0.1.0/24", "10.0.2.0/24"],
"environment": "dev"
}
cat live/dev/database/output/database.json
{
"db_id": "myapp-dev-db",
"db_name": "appdb",
"db_engine": "postgres",
"db_size": "small",
"network_id": "myapp-dev-network",
"environment": "dev"
}
cat live/dev/app/output/app.json
{
"app_id": "myapp-dev",
"app_version": "1.0.0",
"instance_type": "small",
"replicas": 1,
"network_id": "myapp-dev-network",
"db_id": "myapp-dev-db",
"db_endpoint": "myapp-dev-db.local:5432",
"environment": "dev",
"app_url": "http://myapp-dev.local"
}
Chú ý network_id và db_id trong app.json khớp với output của 2 module trước — đây là kết quả của dependency management.
8.4 Apply môi trường prod
cd live/prod
terragrunt run --all apply
cat live/prod/app/output/app.json
{
"app_id": "myapp-prod",
"app_version": "1.0.0",
"instance_type": "large",
"replicas": 3,
"network_id": "myapp-prod-network",
"db_id": "myapp-prod-db",
"db_endpoint": "myapp-prod-db.local:5432",
"environment": "prod",
"app_url": "http://myapp-prod.local"
}
instance_type: "large" và replicas: 3 — đúng với prod/env.hcl. Cùng module, khác cấu hình, không cần copy code.
8.5 Các lệnh Terragrunt hữu ích khác
# Xem plan cho tất cả module
terragrunt run --all plan
# Validate cấu hình (dùng mock_outputs cho dependencies)
terragrunt run --all validate
# Xem output của một module
cd live/dev/network
terragrunt output
# Destroy tất cả (prod trước, dev sau nếu muốn an toàn)
cd live/prod && terragrunt run --all destroy
cd live/dev && terragrunt run --all destroy
9. Tổng kết — Những gì bạn đã học
Qua lab này, bạn đã thấy Terragrunt giải quyết được những vấn đề gì:
| Vấn đề | Giải pháp Terragrunt |
|---|---|
backend.tf lặp lại ở 6 module |
remote_state block ở root, auto-generate |
provider.tf lặp lại ở 6 module |
generate "provider" block ở root |
| Phải apply thủ công theo thứ tự | run --all apply với dependency graph tự động |
| Copy env config khi thêm môi trường | env.hcl chứa biến, module terragrunt.hcl dùng chung |
| Lấy output của module khác | dependency block, không cần terraform_remote_state |
Khi nào nên dùng Terragrunt?
- Bạn có nhiều hơn 1 môi trường (dev/staging/prod)
- Bạn có nhiều Terraform module cần orchestrate
- Bạn muốn enforce consistency giữa các môi trường
- Team muốn giảm thiểu copy-paste trong IaC
Khi nào KHÔNG cần Terragrunt?
- Project nhỏ, chỉ có 1 môi trường
- Đang dùng Terraform Cloud/Enterprise với workspace management
- Team chưa quen Terraform — học Terraform thuần trước
Kết
Qua bài viết này, mình đã giới thiệu Terragrunt và hướng dẫn bạn thực hành với một lab hoàn chỉnh chạy trên local. Hy vọng bạn đã thấy rõ giá trị mà Terragrunt mang lại khi quản lý infrastructure ở quy mô lớn với nguyên tắc DRY.
Nếu bài viết có ích cho bạn hãy Follow và Upvote để ủng hộ mình nhé. Cảm ơn bạn ❤️
Nếu như bạn đang gặp khó khăn trong vấn đề chuyên môn, cần người hỗ trợ về mặt hệ thống, DevOps tools hay cần định hướng trong công việc thì mình tự tin có thể hỗ trợ được bạn. Liên hệ với mình để trao đổi thêm nhé https://hoangviet.io.vn/, mình rất vui khi được trao đổi và cộng tác với bạn.
All rights reserved