「CI/CD 流水线集成」

讲解 Terraform 与 CI/CD 流水线集成:plan 与 apply 分离、远程 state 与凭据注入、plan 产物存档与 PR 评论、批准门禁、GitOps 工作流与多环境流水线。

1. Terraform 在 CI/CD 中的挑战

一句话总结: Terraform 的 apply 是「有副作用」的部署动作,流水线必须把「只读的 plan」与「可写入的 apply」严格分离,并在二者之间插入人工/自动门禁。

普通 CI 流水线(构建 → 测试 → 部署)对 Terraform 并不直接适用:terraform plan 只读安全,而 terraform apply 会真实修改云资源,二者绝不能混在一个无门禁的步骤里。

安全模式:
  PR 提交 ──► plan(只读,产出 diff)──► 人工 Review ──► apply(写入,需批准)
                                    ▲
                   门禁:plan 无破坏性变更才允许合并

1.1 破坏性操作识别

操作风险门禁
create低常规 Review
update中关注强制替换
delete高必须人工确认
replace高有停机风险
import低审计
# plan 输出中的关键标记
# Terraform will perform the following actions:
#   # aws_instance.web will be updated in-place
#   # aws_db_instance.db will be replaced   ← 高风险

2. 远程 state 与凭据注入

一句话总结: 流水线里的 Terraform 必须读远程 state,且云凭据与 state 访问权限都要通过 CI 平台的 Secret 注入,绝不落盘到仓库或镜像。

CI runner 每次都是全新环境,本地 state 无从谈起。标准做法是把 backend 指向远程(S3/GCS),并让 runner 通过环境变量拿到临时凭据。

# backend 配置(凭据由 CI 环境注入,不在代码里)
terraform {
  backend "s3" {
    bucket         = "my-infra-tfstate"
    key            = "prod/network/terraform.tfstate"
    region         = "ap-northeast-1"
    dynamodb_table = "tf-state-lock"
  }
}

2.1 CI 侧注入

# GitHub Actions / GitLab CI 通过 Secret 注入
export AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID}"
export AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY}"
export AWS_REGION="ap-northeast-1"

terraform init -backend-config="key=${ENV}/network/terraform.tfstate"
凭据类型存放位置生命周期
云厂商 AK/SKCI Secret定期轮换
OIDC 联邦CI 平台颁发每次运行临时
state 桶访问CI Role最小权限

一句话:能用 OIDC/AssumeRole 就不要用静态 AK/SK;即便用 AK/SK,也只进 CI Secret,绝不进仓库。

3. Plan 阶段设计

一句话总结: Plan 阶段产出「可审查的 diff 产物」:用 -out 把 plan 存档为二进制,把摘要贴到 PR 评论,让审查者不用本地跑命令就能看懂变更。

3.1 Plan 存档与评论

# 1. 初始化并获取最新 state
terraform init
terraform workspace select prod || terraform workspace new prod

# 2. 生成 plan 产物(二进制,apply 时复用)
terraform plan -out=tfplan.binary

# 3. 生成人类可读摘要
terraform show -json tfplan.binary | jq -r '.resource_changes[] | "\(.change.actions | join(",")) \(.address)"'

3.2 plan 评论模板

Terraform Plan 评论模板(prod/network)

| 动作 | 数量 |
|------|------|
| create | 2 |
| update | 1 |
| replace | 0 |
| delete | 1 |

- aws_vpc.main 将被 delete(请确认)
- aws_subnet.azs["a"] 将被 create
# GitHub Actions 示例:PR 上评论 plan 结果
- name: Post plan comment
  uses: actions/github-script@v7
  with:
    script: |
      const body = process.env.PLAN_SUMMARY;
      await github.rest.issues.createComment({
        owner, repo, issue_number, body
      });

3.3 Plan 阶段的职责划分

步骤是否允许写说明
init写 .terraform 缓存只读远程 state
workspace select是选择环境
plan否(只读)计算 diff
fmt / validate否静态检查

一句话:plan 阶段的产物(二进制 plan + 文本摘要)要当作「构建产物」存档或上传到 CI 平台,供 apply 阶段复用或人工审查。

4. Apply 阶段设计

一句话总结: Apply 是「有副作用」的唯一写入点,必须设计批准门禁、自动/手动模式选择、锁与超时控制,并把输出写入审计日志。

4.1 门禁设计

# 手动批准:需要人工 approval 后才执行
terraform apply tfplan.binary
门禁类型适用说明
人工批准生产环境CI 平台 approval 按钮
自动批准非生产合并即 apply
定时窗口变更窗口业务维护期
变更单绑定合规要求关联 ticket

4.2 Apply 步骤清单

# 1. 复用 plan 产物(保证与审查的完全一致)
terraform apply tfplan.binary

# 2. 记录输出
terraform output -json > output.json

# 3. 失败时保留锁信息
# Error: Error acquiring the state lock
# → 检查是否有并行流水线

4.3 锁与并发

# GitHub Actions:限制并发,避免多人同时 apply
concurrency:
  group: tf-apply-${{ github.ref }}
  cancel-in-progress: false
控制点配置
并发限制concurrency group
锁等待DynamoDB 锁 + 超时
失败重试有限次重试,保留现场
告警失败通知到 on-call

一句话:apply 必须「只 apply 审查过的 plan」,所以 -out 产物要从 plan 阶段一路传到 apply 阶段,不能重新 plan。

5. GitOps 工作流

一句话总结: GitOps 让 Git 成为 IaC 的唯一事实来源,PR 即变更请求,合并即 apply,配合 Atlantis/Terraform Cloud 实现「评论驱动」的自动闭环。

5.1 两种主流形态

形态机制代表
PR 驱动(Comment-driven)在 PR 里 @ 机器人跑 plan/applyAtlantis
云托管配置托管在平台,平台负责执行Terraform Cloud / HCP

5.2 Atlantis 工作流

开发者提交 PR
  └─► Atlantis 检测到变更,跑 plan,评论到 PR
      └─► 人工 Review + 评论 "atlantis apply"
          └─► Atlantis 执行 apply,更新 PR 状态
# atlantis.yaml
version: 3
projects:
  - dir: prod/network
    workspace: prod
    terraform_version: v1.7.0
    apply_requirements: [mergeable, approved]
  - dir: staging/app
    workspace: staging
    apply_requirements: []

5.3 使用 PR 的评论规范

atlantis plan        # 触发 plan
atlantis apply       # 触发 apply(需权限)
atlantis plan -p prod/network

一句话:GitOps 的关键不是工具,而是「Git 是唯一入口」:任何云资源变更都必须以 PR 形式提交,审查与审计都在 Git 里留痕。

6. 多环境流水线

一句话总结: 多环境用「同一套模块 + 每环境一个 state + promotion 链路」组织流水线,dev 自动 apply、staging 半自动、prod 全人工。

6.1 目录与 state 对应

prod/network/
  main.tf
  backend.tf
  tfplan.binary(存档)
staging/network/
  main.tf
prod/app/
  main.tf

6.2 环境流水线矩阵

环境plan 触发apply 触发门禁
devPR 提交合并即自动无
stagingPR 提交合并 + 半自动1 人批准
prodPR 提交合并 + 完全人工2 人批准 + 窗口
# 同一工作流,按环境分支不同配置
env:
  ENV: ${{ github.ref_name == 'refs/heads/main' && 'prod' || 'staging' }}

6.3 Promotion 原则

原则说明
一套模块环境间只差变量,不差代码
变量驱动tfvars / -backend-config 区分环境
先跑通 dev验证模块与计划逻辑
变更逐级上不跳级直接改 prod

一句话:多环境的本质是「一份模块 + 多份 state + 多套变量」,流水线按环境分支配置触发方式与门禁强度即可。

7. 常见 CI/CD 避坑

一句话总结: 流水线集成的大坑集中在「凭据泄露」「plan/apply 不一致」「并发锁死」「破坏性变更无人把关」四类。

坑现象对策
AK/SK 入库凭据泄露OIDC + Secret 注入
apply 前重新 plan与审查的不一致-out 产物跨阶段传递
未限并发锁冲突、互相覆盖concurrency group
delete 无门禁资源被误删高权限环境人工批准
权限过大runner 能删一切最小权限 IAM
plan 无存档无法审计上传 plan 产物
# 生产 apply 前的双重检查脚本(示例)
#!/bin/bash
set -euo pipefail

# 检查 plan 中是否存在 delete 且未经批准
DELETES=$(terraform show -json tfplan.binary | jq '.resource_changes[] | select(.change.actions[0] == "delete")')
if [ -n "$DELETES" ] && [ "${AUTO_APPROVE:-false}" != "true" ]; then
  echo "检测到删除操作,需要人工批准"
  exit 1
fi

一句话:把「plan 即审查、apply 即批准、Git 即审计」三者写进流水线,Terraform 才能安全地跑在 CI/CD 上。

8. 总结

Terraform 流水线的核心是把「只读计划」与「写入执行」彻底分离:

环节要点
原则plan 只读、apply 写入,严格分离
state远程 backend + CI 凭据注入
plan-out 产物 + PR 评论摘要
apply批准门禁 + 复用 plan 产物
GitOpsPR 驱动,Git 为唯一事实来源
多环境一份模块 + 每环境独立 state
避坑凭据不落盘、并发受限、delete 把关

一句话收尾:流水线把「谁都能改云」变成「改云必须有痕迹、有门禁」。下一篇「最佳实践与安全加固」将从敏感数据、加密、权限最小化与策略即代码角度,把整条链路的安全性兜住。

延伸阅读

继续阅读

探索更多技术文章

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

全部文章 返回首页

「terraform」更多文章

  1. 「漂移检测与收敛」
  2. 「资源重构与迁移」
  3. 「数据源与远程数据读取」