Kiến trúc Terraform chuẩn DRY với Terragrunt: Quản lý môi trường Dev, Staging và Production

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

Bối cảnh & Lý do: Bẫy trùng lặp mã nguồn trên nhiều môi trường

Việc quản lý hạ tầng đám mây trên các môi trường dev, staging và production ban đầu tưởng chừng rất đơn giản. Bạn chỉ cần viết vài file Terraform cho VPC, khởi tạo một instance RDS Postgres và triển khai cụm ECS. Thế nhưng, ngay khi hệ thống đòi hỏi sự đồng nhất tuyệt đối giữa staging và production, những bất cập bắt đầu lộ diện.

Terraform tiêu chuẩn thường đẩy bạn vào một trong hai lựa chọn đánh đổi đầy phiền toái: workspace hoặc phân nhánh thư mục. Thoạt nhìn, Terraform workspace có vẻ khá tiện lợi. Tuy nhiên, chúng dùng chung cấu hình backend và cơ chế biến số, khiến việc phân tách account hoặc ghi đè (override) cấu hình riêng cho từng môi trường tiềm ẩn rất nhiều rủi ro.

Giải pháp còn lại—tách biệt thành các thư mục dev, staging và prod—lại nhanh chóng trở thành một cơn ác mộng về bảo trì. Bạn sẽ phải sao chép lặp đi lặp lại các khối backend.tf và provider.tf giống hệt nhau vào hàng chục thư mục. Với một hệ thống gồm 15 microservices chạy trên cả 3 môi trường, bạn có thể dễ dàng phải quản lý hơn 40 định nghĩa backend trùng lặp.

Tại sao lại xảy ra vấn đề này? Thiết kế cốt lõi của Terraform không cho phép sử dụng biến động (dynamic variable) bên trong các khối backend. Bạn không thể chèn biến môi trường hoặc AWS account ID vào S3 bucket key ngay trong định nghĩa terraform { backend "s3" {...} } gốc. Chính hạn chế này buộc các kỹ sư phải hardcode đường dẫn state, nhân bản định nghĩa module và đồng bộ thủ công các khối xác thực provider.

Terragrunt giải quyết triệt để hạn chế cốt lõi nói trên. Đóng vai trò là công cụ điều phối kiêm một wrapper mỏng bao quanh Terraform, Terragrunt thúc đẩy áp dụng nguyên lý DRY (Don’t Repeat Yourself – Không lặp lại chính mình). Các cấu hình con sẽ trực tiếp kế thừa khai báo backend và thiết lập provider từ duy nhất một file gốc. Mô hình tổ chức này ngăn chặn triệt để tình trạng lệch chuẩn cấu hình (configuration drift) khi bạn triển khai hạ tầng trên các tài khoản AWS hoàn toàn biệt lập.

Cài đặt

Terragrunt yêu cầu phải có sẵn binary của Terraform trong $PATH của hệ thống. Chúng tôi khuyến nghị nên ghim (pin) phiên bản cụ thể cho môi trường production hoặc cài đặt trực tiếp file static binary vào runner CI/CD của bạn.

Bước 1: Cài đặt Terraform

Với các môi trường Debian và Ubuntu, hãy tải gói chính thức từ repository của HashiCorp:

sudo apt-get update && sudo apt-get install -y gnupg software-properties-common curl
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [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-get update && sudo apt-get install -y terraform=1.8.*

Bước 2: Cài đặt Terragrunt

Tải binary đã biên dịch sẵn từ GitHub repository của Gruntwork và chuyển nó vào thư mục binary của hệ thống:

TERRAGRUNT_VERSION="v0.58.5"
curl -L -o terragrunt https://github.com/gruntwork-io/terragrunt/releases/download/${TERRAGRUNT_VERSION}/terragrunt_linux_amd64
chmod +x terragrunt
sudo mv terragrunt /usr/local/bin/terragrunt

Kiểm tra xác nhận cả hai công cụ đã sẵn sàng:

terraform version
terragrunt --version

Cấu hình: Xây dựng cấu trúc đa môi trường gọn gàng, chuẩn xác

Để giữ mã nguồn luôn DRY, hãy chia repository của bạn thành hai vùng rõ rệt: đặt các bản thiết kế hạ tầng mẫu (blueprint) dùng chung vào các module Terraform tái sử dụng, và tách riêng các input theo từng môi trường cùng logic backend vào trong các thư mục live của Terragrunt.

Cấu trúc thư mục Repository

Dưới đây là mô hình phân cấp thư mục trực quan và tối ưu cho mô hình nhiều tài khoản AWS:

infrastructure/
├── modules/
│   └── vpc/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
└── live/
    ├── terragrunt.hcl          # Cấu hình gốc (remote state & providers)
    ├── dev/
    │   ├── env.hcl             # Biến số ở cấp độ môi trường (CIDRs, tags)
    │   └── vpc/
    │       └── terragrunt.hcl  # Triển khai VPC môi trường Dev
    ├── staging/
    │   ├── env.hcl
    │   └── vpc/
    │       └── terragrunt.hcl
    └── prod/
        ├── env.hcl
        └── vpc/
            └── terragrunt.hcl

1. File cấu hình gốc (`live/terragrunt.hcl`)

File cấu hình gốc sẽ tự động sinh ra cả remote backend lẫn khối AWS provider trong quá trình thực thi thông qua hàm path_relative_to_include() của Terragrunt.

# live/terragrunt.hcl
locals {
  # Tự động tải các biến ở cấp độ môi trường từ các thư mục con
  env_vars   = read_terragrunt_config(find_in_parent_folders("env.hcl"))
  env_name   = local.env_vars.locals.environment
  aws_region = "us-east-1"
}

# Cấu hình backend tập trung
remote_state {
  backend = "s3"
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite_terragrunt"
  }
  config = {
    bucket         = "mycompany-tf-state-${local.env_name}-${local.aws_region}"
    key            = "${path_relative_to_include()}/terraform.tfstate"
    region         = local.aws_region
    encrypt        = true
    dynamodb_table = "mycompany-tf-locks-${local.env_name}"
  }
}

# Khởi tạo động provider AWS
generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider "aws" {
  region = "${local.aws_region}"
  default_tags {
    tags = {
      Environment = "${local.env_name}"
      ManagedBy   = "Terragrunt"
    }
  }
}
EOF
}

2. File biến môi trường (`live/dev/env.hcl`)

Lưu trữ các giá trị đặc thù cho từng môi trường vào một file duy nhất cho mỗi tier, thay vì lặp lại account ID và các tag trong mọi module:

# live/dev/env.hcl
locals {
  environment    = "dev"
  aws_account_id = "111122223333"
}

3. Cấu hình Module con (`live/dev/vpc/terragrunt.hcl`)

Bạn có thể thấy cấu hình cho từng thành phần giờ đây trở nên cực kỳ tinh gọn. Không còn boilerplate backend. Không còn các khối provider rườm rà. Tất cả những gì bạn cần là một khai báo kế thừa và các giá trị biến đầu vào.

# live/dev/vpc/terragrunt.hcl
include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = "../../../modules/vpc"
}

inputs = {
  vpc_cidr             = "10.10.0.0/16"
  public_subnet_cidrs  = ["10.10.1.0/24", "10.10.2.0/24"]
  private_subnet_cidrs = ["10.10.10.0/24", "10.10.20.0/24"]
  enable_nat_gateway   = true
  single_nat_gateway   = true # Tiết kiệm ~$65/tháng nhờ chỉ dùng một NAT gateway ở môi trường dev
}

Để tạo môi trường staging hoặc production, bạn chỉ cần sao chép cấu trúc thư mục từ dev và điều chỉnh lại file env.hcl cùng các input tương ứng. Chẳng hạn, bạn có thể chuyển single_nat_gateway = false trên production để kích hoạt tính năng dự phòng đa vùng khả dụng (multi-AZ redundancy). Bạn đạt được tính sẵn sàng cao mà không cần phải chỉnh sửa dù chỉ một dòng mã nguồn trong module VPC cốt lõi.

Kiểm thử & Giám sát

Sau khi hoàn tất bố cục thư mục, việc kiểm thử hạ tầng diễn ra rất trực quan và dễ dàng. Bạn có thể nhắm vào từng thành phần riêng lẻ hoặc thực thi lệnh trên toàn bộ stack cùng lúc.

Bước 1: Kiểm thử một thành phần đơn lẻ

Di chuyển trực tiếp vào thư mục của thành phần cần chạy. Terragrunt sẽ tự động thiết lập remote backend, render các file provider cục bộ, tải module nguồn về và chạy Terraform ngầm bên dưới:

cd live/dev/vpc
terragrunt init
terragrunt plan

Khi kết quả plan hoàn toàn đúng như mong đợi, hãy áp dụng thay đổi:

terragrunt apply -auto-approve

Bước 2: Điều phối nhiều module an toàn và liền mạch

Các hạ tầng hiện đại luôn chứa các tài nguyên có liên kết phụ thuộc với nhau như VPC, cơ sở dữ liệu RDS và cụm EKS. Terragrunt sử dụng khối dependency tường minh để quản lý thứ tự thực thi và truyền dữ liệu đầu ra giữa các module một cách trơn tru:

# live/dev/app/terragrunt.hcl
dependency "vpc" {
  config_path = "../vpc"
}

inputs = {
  vpc_id     = dependency.vpc.outputs.vpc_id
  subnet_ids = dependency.vpc.outputs.private_subnet_ids
}

Để rà soát thay đổi trên tất cả các module trong cùng một môi trường cùng lúc, bạn chỉ cần chạy lệnh run-all ngay tại thư mục của môi trường đó:

cd live/dev
terragrunt run-all plan

Terragrunt sẽ tự động xây dựng biểu đồ phụ thuộc (dependency graph), chạy plan song song cho các module độc lập và áp dụng các tài nguyên có phụ thuộc lẫn nhau theo đúng thứ tự nghiêm ngặt.

Bước 3: Giám sát liên tục và phát hiện lệch chuẩn cấu hình (Drift Detection)

Để phát hiện sớm các thay đổi phát sinh ngoài luồng (out-of-band) trước khi chúng có nguy cơ gây gián đoạn hệ thống, bạn hãy thiết lập lịch chạy kiểm tra drift detection định kỳ trên GitHub Actions hoặc GitLab CI:

# Exit code 2 cảnh báo hạ tầng đã bị thay đổi ngoài tầm kiểm soát (drift)
terragrunt run-all plan -detailed-exitcode

Định nghĩa state tập trung kết hợp cùng các input độc lập cho từng môi trường sẽ giúp codebase của bạn luôn ngăn nắp và sạch sẽ. Bạn loại bỏ hoàn toàn thói quen copy-paste tiềm ẩn nhiều rủi ro, phân định ranh giới cấu hình rõ ràng, dễ kiểm toán và xây dựng được nền tảng hạ tầng sẵn sàng mở rộng quy mô từ mô hình thử nghiệm nhỏ đến các môi trường production đa vùng hoàn chỉnh.

Share: