1. 为什么需要 PR 驱动的 IaC
一句话总结: Terraform 的
apply是有副作用的写操作,把它从「谁的笔记本上都能跑」搬到「PR 评审 + 审批门禁」的轨道上,才能让基础设施变更像代码一样被审计。
传统的 Terraform 工作流大致有三种:
| 模式 | 执行位置 | 主要问题 |
|---|---|---|
| 本地执行 | 工程师笔记本 | state 锁竞争、凭据散落、无审计、无评审 |
| CI 触发 | 流水线 Job | 无法在 PR 里直接看 plan、审批靠分支保护间接实现 |
| PR 驱动 | 常驻服务 | 需要额外运维,但评审体验与审计能力最好 |
PR 驱动模式的核心诉求是:开发者提交 PR 后,机器人自动在 PR 评论里贴出 plan 结果;评审者看过 plan 再评论 atlantis apply,由服务端在受控环境中执行。整条链路把「谁在什么时间基于哪个 commit 改了什么资源」全部落在 Git 历史与 PR 评论里。
Atlantis 是这个模式最主流的开源实现。它不是 CI 的替代品,而是叠加在 CI 之上的 Terraform 专用编排层:CI 负责构建、测试、镜像发布,Atlantis 负责 plan/apply 的评审与执行。
开发者 push ──► PR 打开 ──► Atlantis 收到 webhook
│
├─ 检出 PR 分支
├─ 运行 terraform plan
└─ 把 plan 结果评论到 PR
│
评审者评论 "atlantis apply" ◄─────────────┘
│
└─► 校验审批 → 执行 apply → 回帖结果 → 自动合并(可选)
2. Atlantis 架构与运行模式
一句话总结: Atlantis 是一个接收 Git 平台 webhook、在本地工作目录里执行 Terraform 命令并回写 PR 评论的长驻服务,部署形态有单机 Docker、Kubernetes 与 Helm 三种。
2.1 组件构成
┌──────────────┐ webhook ┌───────────────────────────────┐
│ GitHub/GitLab├─────────────►│ Atlantis Server │
│ (PR 事件) │ │ ├─ 事件解析与命令路由 │
└──────────────┘ │ ├─ 项目/工作区发现 │
▲ │ ├─ 并发锁(按项目+工作区) │
│ PR 评论 │ ├─ Terraform 执行器 │
└─────────────────────┤ └─ 工作目录缓存(/tmp) │
└──────────────┬────────────────┘
│ 调用
▼
terraform / terragrunt / opentofu
│
▼
云 API + 远程 state
三个关键子系统:事件层接收 pull_request、issue_comment、push 三类 webhook;发现层根据 atlantis.yaml 或自动发现规则找出本次 PR 影响了哪些「项目」;执行层为每个项目准备独立工作目录,注入环境变量与凭据,执行 Terraform 并把输出裁剪后回帖。
2.2 部署形态
最简形态是单容器:
docker run -d --name atlantis \
-p 4141:4141 \
-e ATLANTIS_GH_USER=atlantis-bot \
-e ATLANTIS_GH_TOKEN="$GH_TOKEN" \
-e ATLANTIS_GH_WEBHOOK_SECRET="$WEBHOOK_SECRET" \
-e ATLANTIS_REPO_ALLOWLIST='github.com/acme/infra' \
-v /var/run/docker.sock:/var/run/docker.sock \
ghcr.io/runatlantis/atlantis:latest server
生产上更推荐 Kubernetes 部署:
# values.yaml 片段
orgAllowlist: github.com/acme/infra
github:
user: atlantis-bot
token: <从 Secret 注入>
secret: <webhook secret>
ingress:
enabled: true
hosts:
- host: atlantis.acme.internal
paths: ["/"]
replicaCount: 1 # 有状态服务,通常单副本
resources:
requests: {cpu: 500m, memory: 1Gi}
limits: {cpu: "2", memory: 4Gi}
关键取舍:
replicaCount通常保持为 1。Atlantis 的并发锁是进程内的,多副本会让同一项目被两个副本同时 apply,直接绕过锁语义。要横向扩展,得靠「按仓库/环境分片」而非简单加副本。
2.3 工作目录与缓存
Atlantis 把仓库克隆到 $ATLANTIS_DATA_DIR(默认 /tmp/atlantis),每个 PR 有独立工作区,PR 关闭后清理。因此:state 必须放远程后端而非本地;长期凭据不能写进工作目录;容器需要足够临时磁盘(大仓库 + 多个 provider 二进制会吃掉数 GB)。
3. 仓库与项目映射配置
一句话总结: 根级
atlantis.yaml定义「哪些目录算一个项目、用什么工作区、跑什么命令」,是 Atlantis 从「自动发现」升级为「精确控制」的关键。
3.1 自动发现 vs 显式配置
默认行为是自动发现:Atlantis 扫描 PR 中变更的文件,向上找最近的 terraform 目录作为项目。这在单一目录结构里够用,但真实仓库里问题很多——共享模块目录会被误当项目、多环境目录需要不同工作区、某些目录根本不该自动 apply。显式配置用一个根级 atlantis.yaml 接管:
version: 3
automerge: true
delete_source_branch_on_merge: true
parallel_plan: true
parallel_apply: false # apply 默认串行,见第 5 节
projects:
- name: network-prod
dir: envs/prod/network
workspace: prod
terraform_version: v1.9.5
autoplan:
when_modified: ["*.tf", "../../modules/network/**/*.tf"]
enabled: true
apply_requirements: [approved, mergeable]
- name: app-prod
dir: envs/prod/app
workspace: prod
terraform_version: v1.9.5
autoplan:
when_modified: ["*.tf", "../../modules/app/**/*.tf"]
apply_requirements: [approved, mergeable]
depends_on: [network-prod]
几个字段值得展开:
when_modified:防止「改模块不触发下游 plan」的核心。模块目录变更必须显式列进来,否则改了模块只有直接引用它的目录会重新 plan。apply_requirements:approved表示 PR 需至少一个 approval,mergeable表示无冲突,undiverged表示分支不能落后于 base。depends_on:声明项目间顺序,Atlantis 按拓扑序 apply,避免「网络还没建好就 apply 应用」。terraform_version:锁定版本,避免不同项目用到不同 Terraform 行为。
3.2 项目发现的边界
repo/
├── atlantis.yaml
├── modules/ ← 共享模块,不是项目,不进 projects
│ ├── network/
│ └── app/
└── envs/
├── dev/
│ ├── network/ ← project: network-dev
│ └── app/ ← project: app-dev
└── prod/
├── network/ ← project: network-prod
└── app/ ← project: app-prod
一句话: 模块目录永远不该成为项目,它是被
when_modified引用的依赖源;项目只对应「有独立 state 的目录」。
4. 工作区与多环境映射
一句话总结: Atlantis 的 workspace 概念对应 Terraform workspace,同一份配置靠不同工作区隔离环境;但生产上更推荐「目录隔离为主、工作区为辅」的混合策略。
4.1 两种隔离方式的取舍
| 方式 | 实现 | 优点 | 缺点 |
|---|---|---|---|
| 目录隔离 | envs/prod/app | state 天然分离、权限可按目录切 | 目录多、重复配置 |
| 工作区隔离 | 同一目录 + -workspace=prod | 配置零重复 | 易误操作到错环境、state 前缀耦合 |
注意一个常见陷阱:Atlantis 默认把 workspace 名作为命令的一部分(atlantis plan -w prod)。如果配置里固定了 workspace,评论里就不需要再写;混用「固定 workspace」与「评论传 -w」很容易出现「以为在 dev 跑,实际打了 prod」。
4.2 环境映射到目录的推荐布局
envs/
├── dev/
│ ├── backend.tf # key = dev/app.tfstate
│ └── app/
├── staging/
│ ├── backend.tf # key = staging/app.tfstate
│ └── app/
└── prod/
├── backend.tf # key = prod/app.tfstate
└── app/
每个环境独立目录 + 独立 backend key,workspace 全部保持 default。代价是配置重复,收益是 state、权限、锁、审批策略可以完全独立;重复部分用模块吸收,目录里只剩十几行调用。
4.3 按环境差异化审批
projects:
- name: app-dev
dir: envs/dev/app
apply_requirements: [] # dev 允许无审批,快速迭代
- name: app-staging
dir: envs/staging/app
apply_requirements: [approved]
- name: app-prod
dir: envs/prod/app
apply_requirements: [approved, mergeable, undiverged]
「低环境宽松、高环境严格」的策略直接写进版本控制,评审时可查。
5. 并发锁与串行化
一句话总结: Atlantis 对「项目 + 工作区 + 目录」加进程内锁,同一时刻只有一个命令能持有;跨 PR 的竞争靠锁排队,同 PR 的多项目靠
parallel_plan/parallel_apply控制。
5.1 锁的粒度与并发模型
锁键是 (repo, project_name, workspace, dir) 四元组。同一 PR 对同一项目重复评论 atlantis plan,第二次会提示已有锁并等待或拒绝;不同 PR 触碰同一项目,后到的排队,先到的完成后释放。锁是进程内的,所以多副本部署会破锁。
parallel_plan: true # 多个项目的 plan 并行跑,加速反馈
parallel_apply: false # apply 串行,避免资源竞争与配额打爆
parallel_plan 通常开着——plan 只读,并行安全且能显著缩短大 PR 的等待时间。parallel_apply 默认关闭,因为:云账号有 API 速率限制,并行 apply 容易触发 429;项目间可能有隐式依赖(没写 depends_on 但实际存在),并行会随机失败;配额(如 EIP、vCPU)在并行时容易撞顶。
5.2 与 Terraform 自身 state 锁的关系
Atlantis 的锁是调度层的,Terraform 的 state 锁(如 S3 后端的 DynamoDB 锁)是存储层的,两者互补:
Atlantis 锁:防止两个 PR 同时 apply 同一项目
State 锁: 防止两个 Terraform 进程同时写同一 state
如果绕过 Atlantis 在本地跑了 apply,state 锁仍会挡住并发写,但 Atlantis 不知道,可能造成 plan 与 apply 之间的状态漂移。规则:生产环境禁止本地 apply。
5.3 处理「锁卡住」
# 在 PR 里评论:释放本 PR 的所有锁
atlantis unlock
# 进程崩溃导致锁残留时,查看容器内残留进程
kubectl exec -it deploy/atlantis -- ps aux | grep terraform
kubectl rollout restart deploy/atlantis # 重启清空内存锁
重启前务必确认没有正在运行的 apply 子进程,否则会中断 apply、留下半完成的资源。
6. 与 CI 的职责划分
一句话总结: CI 管「代码正确性」(fmt、validate、测试、策略检查),Atlantis 管「基础设施变更的评审与执行」,两者通过同一套 PR 事件协作但互不越界。
| 动作 | CI(如 GitHub Actions) | Atlantis |
|---|---|---|
terraform fmt -check | ✅ | ❌ |
terraform validate | ✅ | ❌(plan 隐含) |
| 单元测试 / Terratest | ✅ | ❌ |
| 策略检查(OPA/Conftest) | ✅ | ❌ |
terraform plan 评审 | ❌ | ✅ |
terraform apply | ❌ | ✅ |
| 镜像构建与发布 | ✅ | ❌ |
| 应用层部署(K8s rollout) | ✅ | ❌ |
6.1 协作方式
两条链路并行,互不阻塞:
# .github/workflows/terraform-checks.yml
name: terraform-checks
on: [pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform fmt -check -recursive
- run: terraform init -backend=false && terraform validate
- name: 策略检查
run: |
terraform plan -out=tfplan -input=false
terraform show -json tfplan > plan.json
conftest test plan.json -p policy/
Atlantis 的 plan 结果作为评审材料,CI 的检查结果作为合并门禁。两者都要绿,PR 才能合并。
6.2 常见误区
- 让 Atlantis 跑测试:Atlantis 没有测试框架集成,硬塞进 plan 的自定义命令里会让 plan 变慢且语义混乱。
- 让 CI 跑 apply:一旦 CI 也能 apply,就绕过了 Atlantis 的锁与审批,两条路径迟早冲突。
- 重复 plan:CI 和 Atlantis 各 plan 一次,浪费额度且可能因时间差得出不同结论。建议只让 Atlantis 做面向评审的 plan,CI 用
-backend=false只做 validate。
一句话: 一个仓库只应存在一条 apply 路径,否则锁和审批形同虚设。
7. 安全加固与密钥管理
一句话总结: Atlantis 持有能改生产基础设施的凭据,它的攻击面必须按 CI 系统的最高等级设计:最小权限、短期凭据、网络隔离、审计留痕。
7.1 凭据注入
不要把长期 AK/SK 硬编码进容器环境。推荐三条路径:
# 方式一:Kubernetes ServiceAccount + IRSA(AWS)
serviceAccount:
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/atlantis
# 方式二:Vault 动态凭据(在 workflow 的 plan 步骤里取)
workflows:
prod:
plan:
steps:
- run: |
export AWS_ACCESS_KEY_ID=$(vault read -field=access_key aws/creds/terraform)
export AWS_SECRET_ACCESS_KEY=$(vault read -field=secret_key aws/creds/terraform)
- init
- plan
7.2 权限收敛与网络隔离
Atlantis 的角色权限应精确到「它能管理的资源类型」,而不是 AdministratorAccess。生产上至少要做到:按环境拆分 IAM Role,prod 的 role 只被 prod 项目的工作流使用;敏感资源(IAM、KMS、Secrets Manager)用 SCP 或 IAM 边界限制;通过 策略合规检查
在 plan 阶段拦截越权变更。
# 只允许 Git 平台 webhook 来源 IP 访问
ingress:
annotations:
nginx.ingress.kubernetes.io/whitelist-source-range: "140.82.112.0/20,192.30.252.0/22"
7.3 审计与输出脱敏
打开 ATLANTIS_LOG_LEVEL=info,把日志送到集中日志系统。PR 评论本身就是审计记录,但不要在评论里泄露敏感输出:
workflows:
secure:
plan:
steps:
- init
- plan
- run: |
# 过滤 plan 输出里的敏感值再回帖
terraform show -no-color plan.bin | sed -E 's/(password|secret)\s*=\s*".*"/\1 = "***"/'
8. 常见坑与排错
一句话总结: Atlantis 的故障大多集中在「项目发现不对、锁不释放、凭据取不到、plan 与 apply 不一致」四类,排查入口永远是 PR 评论与容器日志。
8.1 项目没被发现
现象是 PR 改了 terraform 文件,Atlantis 却回帖 No projects were modified。排查三步:when_modified 是否覆盖了改动的路径;改动目录是否在 projects 的 dir 之下;是否被 repo-level 的 autodiscovery 覆盖。
8.2 plan 与 apply 结果不一致
这是最危险的场景,原因是 apply 时重新 plan,而两次 plan 之间资源被改动了。对策是用 plan 产物而非重新 plan:
workflows:
strict:
plan:
steps:
- init
- plan:
extra_args: ["-out", "$PLANFILE"]
apply:
steps:
- apply:
extra_args: ["$PLANFILE"] # 直接应用 plan 产物
Atlantis 默认就是这么做的(plan 存到 $PLANFILE,apply 直接用它),但前提是 parallel_plan: false 或项目间无共享资源,否则产物可能在 apply 前已过期。
8.3 凭据过期
短期凭据(STS/Vault)有 TTL,长 PR 从 plan 到 apply 可能跨小时,apply 时凭据已过期:
apply:
steps:
- run: refresh-credentials.sh # 重新获取凭据
- apply
8.4 大仓库克隆慢
用 shallow clone 缩短检出时间:设置 ATLANTIS_REPO_CLONE_DEPTH=1,或让 Atlantis 复用持久化的仓库缓存卷。
9. 生产实践清单
一句话总结: 上线 Atlantis 前逐条核对下面清单,能把绝大多数事故挡在门外。
- 根级
atlantis.yaml覆盖全部项目,when_modified含所有模块路径。 - 生产项目
apply_requirements至少包含approved, mergeable。 - 全站唯一 apply 路径:CI 不 apply,本地不 apply。
- Atlantis 用短期凭据(IRSA / Vault / OIDC),无长期 AK/SK。
-
parallel_apply: false,或明确论证过并行安全。 - 远程 state 与 远程 state 管理 配置正确,无本地 state。
- webhook secret 已设置并定期轮换。
- 日志接入集中系统,PR 评论中的敏感输出已过滤。
- 与 CI/CD 流水线 的职责边界文档化。
9.1 与 GitHub Actions 的分工
如果团队已经用 GitHub Actions 管理 IaC ,引入 Atlantis 不应推翻它,而是在它之上加一层 PR 评审:Actions 继续做 lint/validate/策略检查,Atlantis 专做 plan 展示与 apply 执行。两条链路通过 PR 的 status check 汇总,任何一条失败都阻止合并。
9.2 规模上去之后的演进
当项目数量超过几十个、PR 并发变高时,单副本 Atlantis 会成为瓶颈。演进路径:按环境分片(prod 与 non-prod 各一套,凭据与权限完全隔离);按仓库分片(不同业务线各自一套,避免一个团队的巨型 PR 阻塞其他团队);引入 Terragrunt 编排(项目数量爆炸后用 run-all 在单个项目内批量处理,减少 Atlantis 需要管理的项目数)。无论怎么演进,「PR 驱动 + 单一 apply 路径 + 全量审计」这三条原则都不应改变。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。