0

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, subnet
  • database — quản lý RDS, database instance
  • app — 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: devprod.

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.tfprovider.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ì?

image.png

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.tfbackend.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 file root.hcl trong thư mục cha
  • read_terragrunt_config() — đọc file HCL bất kỳ thành locals
  • get_terragrunt_dir() — lấy absolute path của thư mục chứa terragrunt.hcl hiện tại
  • path_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ả networkdatabase.

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_iddb_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.hcl của prod/network, prod/database, prod/appcấu trúc và nội dung y hệt với dev/. 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 ra env.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_iddb_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"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 FollowUpvote để ủ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

Viblo
Let's register a Viblo Account to get more interesting posts.