Atlantis 与 PR 驱动的 IaC 流程

用 Atlantis 把 Terraform 变更纳入 PR 评审流程:仓库与项目映射、工作区与多环境划分、plan 与 apply 的审批门禁、并发锁与串行化、与通用 CI 的职责切分,以及凭据注入、webhook 安全、输出脱敏与常见故障排错实践。

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/appstate 天然分离、权限可按目录切目录多、重复配置
工作区隔离同一目录 + -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 路径 + 全量审计」这三条原则都不应改变。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「terraform」更多文章

  1. 引导与状态后端自举
  2. 数据平台基础设施即代码
  3. 从 CloudFormation 迁移到 Terraform