饮墨

子安饮墨馀三斗,留与卿儿作赋来

Terragrunt 实战:3 个技巧让大规模 Terraform 代码量减少 60%

2 views

痛点:Terraform 项目膨胀后的维护噩梦

当你的 Terraform 项目从 3 个环境扩展到 10+ 个 AWS 账户、每个账户 20+ 个模块时,你会遇到这些问题:

  1. 代码重复爆炸 — 每个环境都复制一份 backend.tfprovider.tf,改一处要改十几个文件
  2. State 管理混乱 — 手动维护几十个 S3 backend 配置,bucket/key 拼写错误导致 state 丢失
  3. 依赖关系不明 — 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.tfprovider.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.tfprovider.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 之上补齐了工程化能力。代码量少了,出错概率自然低了。