痛点:Terraform 项目膨胀后的维护噩梦
当你的 Terraform 项目从 3 个环境扩展到 10+ 个 AWS 账户、每个账户 20+ 个模块时,你会遇到这些问题:
- 代码重复爆炸 — 每个环境都复制一份
backend.tf、provider.tf,改一处要改十几个文件 - State 管理混乱 — 手动维护几十个 S3 backend 配置,bucket/key 拼写错误导致 state 丢失
- 依赖关系不明 — VPC 模块改了 CIDR,下游 EKS/RDS 模块不知道要重新 apply
terraform -chdir 和 workspaces 能解决部分问题,但面对多账户、多区域的企业级场景远远不够。
方案:Terragrunt — Terraform 的 DRY 编排层
Terragrunt 是 Gruntwork 开源的 Terraform wrapper,核心解决三件事:
| 能力 | 解决的问题 |
|---|---|
generate + include |
消除重复的 backend/provider 配置 |
dependency |
模块间输出自动传递,无需硬编码 |
run-all |
按依赖拓扑顺序批量 plan/apply |
实操步骤
第 1 步:项目结构改造
infrastructure/
├── terragrunt.hcl # 根配置(全局 backend + provider)
├── env_vars/
│ ├── prod.hcl
│ └── staging.hcl
├── prod/
│ ├── vpc/terragrunt.hcl
│ ├── eks/terragrunt.hcl
│ └── rds/terragrunt.hcl
└── staging/
├── vpc/terragrunt.hcl
├── eks/terragrunt.hcl
└── rds/terragrunt.hcl
根目录 terragrunt.hcl 定义全局 backend,所有子模块自动继承:
# infrastructure/terragrunt.hcl
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
config = {
bucket = "company-terraform-state-${local.account_id}"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "ap-northeast-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
generate "provider" {
path = "provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOF
provider "aws" {
region = "${local.aws_region}"
default_tags {
tags = {
ManagedBy = "terragrunt"
Environment = "${local.env}"
}
}
}
EOF
}
效果: 删掉所有子目录中的 backend.tf 和 provider.tf,一处定义全局生效。
第 2 步:用 dependency 自动传递模块输出
EKS 模块需要 VPC 的 subnet IDs,传统做法是硬编码或用 data.terraform_remote_state。Terragrunt 的方式更优雅:
# prod/eks/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
dependency "vpc" {
config_path = "../vpc"
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
private_subnet_ids = dependency.vpc.outputs.private_subnet_ids
cluster_name = "prod-eks-cluster"
node_instance_type = "m6i.xlarge"
desired_capacity = 5
}
Terragrunt 会自动读取 VPC 模块的 state 获取输出值,无需手动拼 state 路径。
第 3 步:一键批量操作
# 安装 (macOS/Linux)
brew install terragrunt
# 在根目录执行,自动按依赖顺序 apply 所有模块
cd infrastructure/prod
terragrunt run-all plan
# 确认无误后批量 apply(并行执行无依赖的模块)
terragrunt run-all apply --terragrunt-parallelism 4
# 只 apply 某个模块及其依赖
cd infrastructure/prod/eks
terragrunt apply
# 自动检测:vpc 未 apply?先 apply vpc 再 apply eks
查看依赖图:
terragrunt graph-dependencies | dot -Tpng > deps.png
避坑指南
坑 1:mock_outputs 未设置导致首次 plan 失败
首次 run-all plan 时,依赖模块尚未 apply,outputs 为空会报错。解决:
dependency "vpc" {
config_path = "../vpc"
mock_outputs = {
vpc_id = "vpc-mock"
private_subnet_ids = ["subnet-mock-1", "subnet-mock-2"]
}
mock_outputs_allowed_terraform_commands = ["plan", "validate"]
}
坑 2:State 锁死时 run-all 会卡住整条链
某个模块 state 被锁(上次 apply 中断),run-all 会阻塞所有下游。务必配置 DynamoDB 锁表,且中断后及时执行:
terragrunt force-unlock <LOCK_ID>
坑 3:generate 文件被 git 提交造成冲突
generate 会在本地生成 backend.tf、provider.tf,必须加入 .gitignore:
# Terragrunt generated files
backend.tf
provider.tf
.terragrunt-cache/
总结
| 指标 | 改造前 | 改造后 |
|---|---|---|
| backend.tf 文件数 | 30+ | 1 |
| provider.tf 文件数 | 30+ | 1 |
| 模块间引用方式 | 硬编码 remote_state | 自动 dependency |
| 批量操作 | 手写脚本逐目录执行 | run-all 一键拓扑排序 |
核心建议:
- 项目超过 5 个模块 × 2 个环境就该引入 Terragrunt
- mock_outputs 一定要配,否则 CI 中 plan 必挂
- 配合 Atlantis 或 Spacelift 做 GitOps,run-all 在 CI 中用 --terragrunt-non-interactive 参数
Terragrunt 不是重新发明轮子,而是在 Terraform 之上补齐了工程化能力。代码量少了,出错概率自然低了。