4.16 IaC 进阶——Pulumi / CDKTF / Terragrunt 实战
预计阅读时间:16 分钟
📖 目录
4.15:Terraform 入门 介绍了 Terraform 的基础用法。当基础设施规模增长、多环境管理变复杂、团队对编程语言有更高诉求时,Terraform 原生的 HCL 可能不够灵活。本文覆盖四种进阶方案:Pulumi(Python/Go 替代 HCL)、CDKTF(CDK + Terraform 生态)、Terragrunt(多环境 DRY 管理)和 Terraform Cloud/Enterprise(远程执行与协作)。
学习目标
学完本章后,你将能够:
- 用 Pulumi 以 Python/Go 等通用编程语言编写 IaC,并管理 Stack 的导出值与状态
- 用 CDKTF 结合 TypeScript 与 Terraform Provider 生态定义基础设施
- 用 Terragrunt 的根级配置、
dependency与mock_outputs实现多环境 DRY 管理 - 配置 Terraform Cloud 远程 Backend、State 锁与 Sentinel 策略
- 对比 Pulumi / CDKTF / Terragrunt / Terraform Cloud 的适用场景并制定选型决策
前置知识
- 4.15:Terraform 入门 Terraform 入门——HCL 语法、Provider 与 State 管理基础
- 4.14:运维故障案例集 运维故障案例集——基础设施变更引发的故障复盘经验
- finops01 FinOps-1:云成本优化——多云资源成本权衡意识
- 4.1:Ansible 自动化 Ansible 自动化——基础设施自动化配置管理思维
1. Pulumi——用通用编程语言写 IaC
Pulumi 支持 Python、TypeScript、Go、C# 和 Java 等通用编程语言定义基础设施,可利用循环、条件、函数组合和 IDE 智能提示。
# 安装
curl -fsSL https://get.pulumi.com | sh
# 初始化 Python 项目(AWS)
mkdir pulumi-demo && cd pulumi-demo
pulumi new aws-python
Python 示例——EC2 + Security Group
import pulumi
from pulumi_aws import ec2
sg = ec2.SecurityGroup("web-sg",
description="Allow HTTP and SSH",
ingress=[
ec2.SecurityGroupIngressArgs(protocol="tcp", from_port=80, to_port=80, cidr_blocks=["0.0.0.0/0"]),
ec2.SecurityGroupIngressArgs(protocol="tcp", from_port=22, to_port=22, cidr_blocks=["10.0.0.0/8"]),
],
egress=[ec2.SecurityGroupEgressArgs(protocol="-1", from_port=0, to_port=0, cidr_blocks=["0.0.0.0/0"])]
)
# AMI 别硬编码:用 SSM 参数按区域查询最新 Ubuntu 24.04 镜像(示例 id 已退役):
# aws ssm get-parameter --name /aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3/ami-id --query Parameter.Value --output text
server = ec2.Instance("web-server",
instance_type="t3.micro", ami="<上一步查询到的 ami-id>",
vpc_security_group_ids=[sg.id], tags={"Name": "web-server"}
)
pulumi.export("public_ip", server.public_ip)
Go 示例——循环生成多环境 S3 Bucket
package main
import (
"github.com/pulumi/pulumi-aws/sdk/v6/go/aws/s3"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
// 通用语言的循环能力——HCL 中需要 count 或复制粘贴
for _, env := range []string{"dev", "staging", "prod"} {
_, err := s3.NewBucket(ctx, env+"-assets", &s3.BucketArgs{
Bucket: pulumi.String("myapp-" + env + "-assets"),
Tags: pulumi.StringMap{"env": pulumi.String(env)},
})
if err != nil {
return err
}
}
ctx.Export("bucketCount", pulumi.Int(3))
return nil
})
}
状态后端与 Stack 管理
# Pulumi 状态可存于 Pulumi Cloud、S3、Azure Blob 或本地
pulumi login s3://pulumi-state-bucket # S3 后端(类似 Terraform 的 s3 backend)
pulumi login azureblob://pulumi-state # Azure Blob 后端
pulumi login --local # 仅本地实验使用,禁止生产
# Stack 即「同名项目 + 不同状态 + 不同配置」的组合
pulumi stack init dev # 创建 stack(类似 terraform workspace)
pulumi stack select prod
pulumi stack ls
# 查看导出值与配置(密钥在 state 中加密存储)
pulumi stack output public_ip
pulumi config set aws:region ap-northeast-1
pulumi config set --secret db_password # 交互式输入,绝不写进代码
Import 已有资源
# 手工创建的存量资源可以用 pulumi import 接管(类似 terraform import)
pulumi import aws:ec2/instance:Instance web-server i-0a1b2c3d4e5f67890
# import 生成的资源定义会追加到 __main__.py,确认无误后执行
pulumi up
# 状态管理常用命令
pulumi state list # 列出当前 state 中的全部资源
pulumi state unprotect web-server # 解除保护后可删除该资源
pulumi refresh --target aws:ec2/instance:Instance # 按资源刷新漂移
与 Terraform 的核心区别
| 维度 | Terraform (HCL) | Pulumi |
|---|---|---|
| 语言 | HCL(专用 DSL) | Python / Go / TypeScript |
| 状态存储 | S3 / Terraform Cloud | Pulumi Cloud / S3 |
| 生态 | Terraform Registry(最大 Provider 库) | Pulumi Registry + 可直接调用 SDK |
| 复用方式 | Module(HCL 封装) | 原生包管理(pip / go mod) |
| IDE 支持 | HCL 插件(有限) | 通用语言 LSP(完整智能提示) |
| 学习曲线 | 低 | 中(需掌握编程语言) |
2. CDKTF——AWS CDK + Terraform 生态
CDKTF(Cloud Development Kit for Terraform)让开发者用 TypeScript/Python/Go 编写 IaC,生成 Terraform 配置并复用 Terraform Provider 生态。
# 安装与初始化
npm install -g cdktf-cli
cdktf init --template=typescript
cdktf get --force # 生成 Provider bindings
TypeScript 示例——VPC + EC2
import { App, TerraformStack } from "cdktf";
import { Construct } from "constructs";
import { AwsProvider } from "./providers/aws";
import { Vpc } from "@cdktf/provider-aws/lib/vpc";
import { Instance } from "@cdktf/provider-aws/lib/instance";
class MyStack extends TerraformStack {
constructor(scope: Construct, id: string) {
super(scope, id);
new AwsProvider(this, "aws", { region: "ap-northeast-1" });
const vpc = new Vpc(this, "vpc", { cidrBlock: "10.0.0.0/16" });
new Instance(this, "web", {
ami: "ami-0c55b159cbfafe1f0", instanceType: "t3.micro"
});
}
}
new MyStack(new App(), "cdktf-demo");
cdktf.out/terraform/)。它不是 Terraform 的替代品,而是上层抽象——底层仍由 Terraform CLI 执行。与 Terraform Provider 的关系
CDKTF 本身不直接调用云厂商 API,而是通过 cdktf get 把 Terraform Provider 的 schema 编译成对应的语言绑定(bindings)。因此:① Provider 生态完全复用 Terraform Registry;② 每次升级 Provider 版本后必须重新执行 cdktf get,否则新资源类型不会出现在语言层类型提示中;③ 语言层与执行层之间存在「生成 → 转换」的间接层。
# cdktf.json——声明 Provider 来源与后端
{
"language": "typescript",
"app": "npx ts-node main.ts",
"projectId": "demo",
"terraformProviders": ["aws@~>5.0"],
"terraformModules": [],
"context": { "excludeStackIdFromLogicalIds": "true" }
}
# 本地调试流程:代码 → 生成 Terraform JSON → 交给 Terraform 执行
cdktf synth # 生成 cdktf.out/ 下的 Terraform JSON 配置
cdktf diff # 等价于 terraform plan
cdktf deploy # 等价于 terraform apply
cdktf destroy
# 远程后端同样支持(与 Terraform 共用同一套 backend 配置)
# 在 cdktf.json 或每个 stack 的 synth 配置中声明 s3 backend 即可,
# state 与纯 Terraform 项目完全互通
CDKTF vs Pulumi
| 维度 | CDKTF | Pulumi |
|---|---|---|
| Provider 生态 | 复用 Terraform Registry(最大) | Pulumi Registry(较小,可桥接 Terraform) |
| 输出格式 | 生成 .tf JSON(兼容 Terraform 工具链) | 自有 State 格式 |
| 渐进迁移 | 可与已有 Terraform 代码共存 | 需完整迁移 State |
3. Terragrunt——多环境 DRY 管理
Terragrunt 是 Terraform 的包装器,解决多环境、多模块场景下的 State 管理和配置复用问题。核心原则是 DRY。
典型目录结构
infrastructure/
├── terragrunt.hcl # 根级公共配置
├── dev/
│ ├── env.hcl # dev 环境变量
│ ├── vpc/terragrunt.hcl
│ ├── ecs/terragrunt.hcl
│ └── rds/terragrunt.hcl
├── staging/
│ ├── env.hcl
│ └── ...
└── prod/
├── env.hcl
└── ...
根级配置与模块依赖
# infrastructure/terragrunt.hcl
remote_state {
backend = "s3"
generate = { path = "backend.tf", if_exists = "overwrite_terragrunt" }
config = {
bucket = "my-terraform-state-${local.env}"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "ap-northeast-1"
dynamodb_table = "terraform-locks"
}
}
# infrastructure/dev/ecs/terragrunt.hcl
include "root" { path = find_in_parent_folders() }
terraform { source = "../../modules//ecs" }
dependency "vpc" { config_path = "../vpc" }
inputs = {
env = local.env
vpc_id = dependency.vpc.outputs.vpc_id
subnet_ids = dependency.vpc.outputs.private_subnet_ids
}
# 常用命令
terragrunt apply # 单模块执行
cd infrastructure/dev && terragrunt run-all apply # 按依赖顺序执行全部
terragrunt run-all plan # 批量预览
terragrunt run-all destroy # 批量销毁
环境变量分层与依赖管理
# infrastructure/terragrunt.hcl(根级)——locals 为子目录提供公共默认值
locals {
region = "ap-northeast-1"
}
generate "provider" {
path = "provider.tf"
if_exists = "overwrite"
contents = <<EOF
provider "aws" {
region = "${local.region}"
default_tags {
tags = { managed_by = "terragrunt" }
}
}
EOF
}
# infrastructure/dev/env.hcl——环境专用变量(新增环境 = 新增目录 + 此文件)
locals {
env = "dev"
region = "ap-northeast-1"
vpc_cidr = "10.10.0.0/16"
ec2_type = "t3.medium"
}
# infrastructure/dev/ecs/terragrunt.hcl——子模块消费环境变量
locals {
env_vars = read_terragrunt_config(find_in_parent_folders("env.hcl"))
env = local.env_vars.locals.env
}
include "root" { path = find_in_parent_folders() }
terraform { source = "../../modules//ecs" }
dependency "vpc" {
config_path = "../vpc"
mock_outputs = {
vpc_id = "vpc-00000000"
private_subnet_ids = ["subnet-00000000"]
}
}
inputs = {
env = local.env
vpc_id = dependency.vpc.outputs.vpc_id
subnet_ids = dependency.vpc.outputs.private_subnet_ids
instance_type = local.env_vars.locals.ec2_type
}
terragrunt plan 会因拿不到 outputs 而失败。为 dependency 配置 mock_outputs 后可用占位值完成预览,首次 run-all apply 时 Terragrunt 会按依赖拓扑顺序(VPC → 子网 → ECS)自动执行。env.hcl + 引用模块,无需复制粘贴。4. Terraform Cloud / Enterprise
Terraform Cloud 提供远程执行、State 锁定、团队协作和策略即代码能力。
核心功能对比
| 功能 | Free | Team & Governance | HCP Terraform(企业版) |
|---|---|---|---|
| 远程执行 / State 远程存储 | 支持 | 支持 | 支持 |
| State 锁定与协作 | 支持 | 支持 | 支持 |
| VCS 集成(Git 触发) | 支持 | 支持 | 支持 |
| Policy as Code(Sentinel) | — | 支持 | 支持 |
| Private Registry / 审计日志 | — | 支持 | 支持 |
| Agent(私有网络执行) | — | — | 支持 |
远程 Backend 配置
terraform {
cloud {
organization = "my-org"
workspaces { name = "prod-vpc" }
}
}
Sentinel 策略示例
# 限制 EC2 实例类型必须是 t3 系列
import "tfplan/v2" as tfplan
ec2_instances = filter tfplan.resource_changes as _, rc {
rc.type is "aws_instance" and (rc.change.actions contains "create")
}
main = rule {
all ec2_instances as _, inst { inst.change.after.instance_type matches "t3\\..*" }
}
5. 选型对比与适用场景
| 工具 | 核心价值 | State 管理 | 适用场景 |
|---|---|---|---|
| Pulumi | 通用编程语言写 IaC | Pulumi Cloud / S3 | 需要复杂逻辑、有编程背景的团队 |
| CDKTF | CDK 抽象 + Terraform 生态 | Terraform State | 已有 Terraform 代码库想渐进迁移 |
| Terragrunt | 多环境 DRY 管理 | Terraform State | 多环境、多模块、配置复用 |
| Terraform Cloud | 远程执行 + 协作 + 策略 | 托管 State + 锁 | 团队协作、合规审计 |
| Atlantis | 开源 PR 驱动执行 | Terraform State | 预算敏感的团队协作 |
选型决策流程
- 已有 Terraform? 配远程 backend + State 锁(Terraform Cloud 或 S3 + DynamoDB)
- 多环境复杂? 引入 Terragrunt 抽取公共配置
- HCL 表达力不足? 选 Pulumi 或 CDKTF(已有 Terraform 生态优先 CDKTF)
- 需要合规审计? Terraform Cloud Enterprise 或 Sentinel / OPA
- 预算有限? Atlantis 替代 Terraform Cloud,Terragrunt 替代 Enterprise 的 DRY 能力
6. 三框架核心差异对比
| 维度 | Pulumi | CDKTF | Terragrunt |
|---|---|---|---|
| 编程范式 | 命令式为主(代码执行后得出资源图) | 声明式(代码编译为 Terraform 配置) | 纯声明式(HCL + 目录结构) |
| 状态管理 | Pulumi 自有 state(S3 / Pulumi Cloud) | Terraform state(完全复用) | Terraform state(每模块独立 key) |
| 多云支持 | AWS/Azure/GCP/K8s/私有云 SDK | 全部 Terraform Provider | 全部 Terraform Provider |
| 逻辑表达 | 循环/条件/函数/第三方库全可用 | TypeScript/Python 语言级逻辑 | 仅 HCL 表达式(循环能力有限) |
| 适用团队规模 | 10+ 人、有专职开发能力 | 5+ 人、已有 Terraform 资产 | 2+ 人即可,纯运维团队友好 |
| 调试方式 | 语言调试器 + 堆栈跟踪 | 查看生成的 JSON + terraform plan | terragrunt debug + 检查 HCL |
选型经验:纯运维团队(不写代码)从 Terragrunt 开始;有开发背景且需要强逻辑(批量生成、动态查询)选 Pulumi;已有 Terraform 代码库又想升级到编程语言时选 CDKTF——它不破坏现有 state。
7. 迁移路径:从 Terraform 到 Terragrunt 目录重构
存量 Terraform 仓库迁移到 Terragrunt 不需要改模块代码,只重构目录与 backend 声明:
# 迁移前——每个环境复制一份完整配置,改一个参数要改三处
terraform/
├── dev/
│ ├── main.tf # 与 staging 完全相同的模块引用
│ ├── backend.tf # bucket 名不同
│ └── terraform.tfvars
├── staging/
│ ├── main.tf # 复制粘贴
│ └── ...
└── prod/
├── main.tf
└── ...
# 迁移后——模块只保留一份,环境目录只放差异
terraform/
├── modules/
│ ├── vpc/
│ └── ecs/
├── terragrunt.hcl # 根级:backend + provider + 公共 locals
├── dev/
│ ├── env.hcl # 环境差异(cidr、实例规格、副本数)
│ ├── vpc/terragrunt.hcl
│ └── ecs/terragrunt.hcl
├── staging/...
└── prod/...
- 第一步:抽取模块——把各环境
main.tf里相同的资源定义抽到modules/,子目录用terraform.source引用。 - 第二步:建立根级 terragrunt.hcl——把
backend.tf的 S3 配置迁入remote_state块,各环境自动生成 backend.tf。 - 第三步:接管 state——每个模块目录执行
terragrunt init -reconfigure,state key 与旧 backend 保持一致即可无缝接管,无需terraform state mv。 - 第四步:验证依赖——
terragrunt run-all plan全量预览,确认依赖图输出与手工执行顺序一致后再 apply。
Pulumi 与 Terraform 代码量对比
以下对比创建相同 AWS 基础设施(VPC + 2 个子网 + EC2 实例)的代码行数:
| 组件 | Terraform (HCL) | Pulumi (Python) | Pulumi (Go) |
|---|---|---|---|
| VPC | 5 行 | 3 行 | 8 行 |
| 子网 ×2 | 12 行 | 6 行(循环) | 12 行 |
| EC2 实例 | 8 行 | 5 行 | 10 行 |
| 输出 | 3 行 | 1 行 | 2 行 |
| 总计 | 28 行 | 15 行 | 32 行 |
渐进式采用路径
阶段 1:评估期(1-2 周)
├─ 在现有 Terraform 项目中引入 Terragrunt 做 DRY 管理
├─ 保持现有 .tf 文件不变,仅重构目录结构
└─ 验证 terragrunt run-all plan 输出与原始 terraform plan 一致
阶段 2:试点期(1 个月)
├─ 选一个非生产模块(如 VPC)迁移到 Terragrunt 管理
├─ 团队学习 Terragrunt 语法和工作流
└─ 积累踩坑经验,建立内部文档
阶段 3:推广期(2-3 个月)
├─ 逐步将所有环境迁移到 Terragrunt
├─ 建立模块库,标准化新模块的目录结构
└─ 配置 CI/CD 流水线自动执行 terragrunt plan/apply
阶段 4:评估 Pulumi/CDKTF(可选)
├─ 对需要复杂逻辑的模块评估 Pulumi/CDKTF
├─ 选 1-2 个模块做 POC
└─ 团队投票决定是否迁移
常见错误补充
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Pulumi import 后资源属性不匹配 | import 生成的代码缺少某些 computed 属性 | 执行 pulumi up 让 Pulumi 自动补齐缺失属性 |
| CDKTF 生成的 Terraform JSON 语法错误 | cdktf get 后未更新 Provider 版本 | 执行 cdktf get --force 重新生成绑定 |
| Terragrunt apply 顺序与预期不符 | dependency 块配置错误 | 用 terragrunt graph-dependencies 可视化依赖图 |
| Terraform Cloud 超时 | Agent 在私有网络中无法连接 TFC | 部署 Terraform Cloud Agent 在私有网络内执行 |
State 管理安全检查清单
| 检查项 | 要求 | 验证方式 |
|---|---|---|
| State 远程存储 | 禁止本地 state 文件 | 检查 backend 配置是否指向 S3/TFC |
| State 加密 | S3 后端启用 SSE-S3 或 SSE-KMS | aws s3api head-object 检查加密配置 |
| State 锁定 | 启用 DynamoDB 锁 | 尝试并发 apply 验证锁生效 |
| 访问控制 | 限制 state 文件的 IAM 权限 | 检查 IAM 策略是否遵循最小权限 |
| 版本控制 | S3 开启版本历史 | 检查 S3 bucket 版本配置 |
| Secret 不入代码库 | 敏感变量使用 Vault/SSM | git grep -i password 检查代码库 |
常见错误
| 问题 | 原因 | 解决方案 |
|---|---|---|
Terraform apply 报 Error: State lock | 上一次 apply 异常中断,锁未释放 | 确认无并发操作后执行 terraform force-unlock <LOCK_ID>;S3 后端需检查 DynamoDB 条目 |
Terragrunt dependency 循环依赖 | 模块 A 依赖 B、B 又依赖 A | 重新划分模块边界,引入第三个模块解耦,或使用 mock_outputs 打破循环 |
CDKTF cdktf get 生成的 Provider bindings 过时 | Provider 版本升级后未重新生成 | 执行 cdktf get --force 并锁定 Provider 版本号 |
| Pulumi stack 导出值找不到 | pulumi.export() 名称拼写错误或 stack 未正确选中 | 运行 pulumi stack ls 确认当前 stack,检查 pulumi stack output 输出 |
| Sentinel 策略误拦截合法变更 | 正则匹配规则过于宽泛(如 instance_type matches "t3\\..*" 未覆盖 t3a) | 审查策略规则,补充边界条件;用 policies/ 目录下的测试用例验证后再上线 |
最佳实践
- State 远程存储 + 锁定:生产环境必须使用远程 backend(S3 + DynamoDB / Terraform Cloud),禁止本地 state 文件。启用 State 锁防止并发修改导致数据丢失。
- 模块版本固定:引用 Terraform 模块或 Pulumi 包时锁定版本号,避免上游变更导致基础设施漂移。定期用
terraform init -upgrade评估升级影响。 - 环境隔离:每个环境使用独立的 state 文件和变量文件(
terraform.tfvars/ Terragrunt 的env.hcl)。生产环境的 state bucket 与开发环境分离并限制访问权限。 - Secret 不入代码库:敏感变量(数据库密码、API Key)使用 Vault、AWS SSM Parameter Store 或 Pulumi 的
pulumi.Config().requireSecret()注入,绝不硬编码在.tf或__main__.py中。 - 渐进式采用:已有 Terraform 代码库的团队不必整体迁移。可先引入 Terragrunt 做 DRY 管理,再评估 Pulumi/CDKTF 是否值得部分模块迁移,保持新旧共存。
故障排查案例:Terragrunt 循环依赖
现象:执行 terragrunt run-all plan 报错 Circular dependency detected: vpc → ecs → vpc。
排查:检查各模块的 dependency 块,发现 vpc 模块中引用了 ecs 模块的输出(用于注入安全组规则),而 ecs 模块又依赖 vpc 的输出。
根因:vpc 模块不应该引用下游模块的输出。安全组规则的配置应放在 ecs 模块中,而非 vpc 模块。
修复:① 将安全组规则从 vpc 模块移出到独立的 security-group 模块;② 调整依赖拓扑为 vpc → security-group → ecs;③ 使用 mock_outputs 在 vpc 模块中引用安全组的输出时提供占位值。
Terragrunt 常用调试命令
# 查看依赖拓扑(确认是否有循环)
terragrunt graph-dependencies
# 只预览单个模块(不触发依赖执行)
cd infrastructure/dev/vpc && terragrunt plan
# 强制重新初始化 backend
terragrunt init -reconfigure
# 查看当前状态中的资源
terragrunt state list
# 批量执行但跳过依赖检查(紧急修复用)
terragrunt run-all apply --terragrunt-ignore-external-dependencies
terragrunt graph-dependencies),再检查是否违反「上游不应引用下游输出」的原则。练习题
- 多环境 Terragrunt 项目:搭建一个包含 dev / staging / prod 三个环境的 Terragrunt 项目,每个环境有独立的 VPC 和 RDS 模块。要求:使用根级
terragrunt.hcl统一配置 S3 backend 和 Provider 版本,通过env.hcl注入环境变量,用dependency块串联 VPC → RDS 的依赖关系。 - Pulumi 组件封装:用 Pulumi(Python)封装一个可复用的 WebServer 组件,支持传入
instance_type、ami、subnet_id、security_group_ids参数,返回实例 ID 和公网 IP。编写测试验证该组件在 dev 和 prod 环境下创建的资源属性不同。 - Sentinel 策略编写:为 Terraform Cloud 编写两条 Sentinel 策略:① 禁止创建
ami-开头的 EC2 实例(要求使用固定 AMI ID);② 要求所有 S3 Bucket 必须启用服务端加密(sse_configuration)。用sentinel test命令验证策略的通过与失败场景。
本章总结
当 HCL 无法满足规模与协作需求时,Pulumi 与 CDKTF 让 IaC 回归通用编程语言,Terragrunt 用 DRY 结构消除多环境重复,Terraform Cloud 提供远程执行与策略门禁。四者互补而非互斥,渐进式引入——先 Terragrunt 管理多环境,再按需评估 Pulumi / CDKTF 迁移部分模块——是最稳妥的演进路径。
延伸阅读
- 4.15:Terraform 入门 Terraform 入门(HCL 基础与 Provider 使用)
- 4.1:Ansible 自动化 Ansible 自动化配置管理(与 Terraform 互补:建 vs 配)
- Pulumi 官方文档
- CDKTF 官方文档
- Terragrunt 官方文档
- Terraform Cloud 文档
- Atlantis —— 开源的 Terraform PR 工作流工具