CI 中的 Terraform
这一课你将能做到
- 在流水线里串起 fmt、validate、init、plan、apply 的标准作业链
- 用工作区(workspace)和环境变量在 CI 中区分 dev/prod,并理解审批门怎么设
- 配置定期漂移检测,看懂 plan 非零退出码在自动化里的含义
为什么要学
Terraform 最终不会只跑在你自己电脑上:多人协作时,每个人的本机环境、版本、凭证都不同,把 apply 收进 CI 才有统一的执行环境、完整的日志和谁在什么时候改了什么的审计记录。成熟团队的标配是:合并前 PR 自动跑 plan 贴在评论里,合并后人工点按钮 apply,每天凌晨再跑一次 plan 做漂移检测。这一课给你可以直接落地的流水线骨架,GitHub Actions 和 GitLab CI 各来一份。
人话:本机 apply 像自己在家炒菜,咸淡随缘、没人看见;CI 流水线像中央厨房——摄像头全程录像、上菜前有人尝咸淡(plan 审批)、每天还检查各门店有没有偷改配方(漂移检测)。
先建立图像
标准流水线阶段:
代码推送 / Pull Request
│
▼
fmt -check ──► validate ──► init ──► plan(产出计划,人工评审)
│
合并 + 审批门
▼
apply(执行计划文件)
│
定时 cron:plan -detailed-exitcode(漂移检测)
退出码约定(自动化全靠它):
| 命令 | 退出码 | 含义 |
|---|---|---|
| plan 普通 | 0 成功 / 非 0 失败 | — |
| plan -detailed-exitcode | 0 无差异 | 1 出错 |
| plan -detailed-exitcode | 2 有差异 | 漂移检测就看它 |
跟着做
步骤 1:本地先跑通质量三连
在任一 Terraform 项目目录:
terraform fmt -check -recursive
terraform init -backend=false
terraform validate
fmt -check只检查不改文件,格式不规范就返回非零(本机想自动修就去掉 -check)init -backend=false只装 provider 不碰后端,CI 校验阶段常用validate查语法和引用错误,但不检查凭证和云上状态
建议把这三行配成提交前钩子或 IDE 保存动作,问题不过夜。
步骤 2:GitHub Actions 骨架
在仓库建 .github/workflows/terraform.yml:
name: terraform
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
pull-requests: write
id-token: write # 用 OIDC 换取云凭证,不存长期 AK/SK
jobs:
terraform:
runs-on: ubuntu-latest
defaults:
run:
working-directory: infra
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.7.*"
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/gh-actions-tf
aws-region: ap-northeast-1
- name: fmt
run: terraform fmt -check -recursive
- name: validate
run: |
terraform init -backend=false
terraform validate
- name: plan
id: plan
run: |
terraform init
terraform plan -out=tfplan -var-file=envs/dev.tfvars
- name: apply(仅 main 分支)
if: github.ref == 'refs/heads/main'
run: terraform apply tfplan
重点不是逐行背,而是理解三处设计:凭证走 OIDC 角色(GitHub 与 AWS 信任关系,无需长期密钥);plan 产物 tfplan 在同一 job 的步骤间传递;apply 限定 main 分支并由分支保护/评审门把关。
步骤 3:GitLab CI 骨架
.gitlab-ci.yml:
stages: [check, plan, apply]
image: hashicorp/terraform:1.7
variables:
TF_ROOT: infra
TF_VARS: envs/dev.tfvars
before_script:
- cd ${TF_ROOT}
fmt:
stage: check
script: terraform fmt -check -recursive
validate:
stage: check
script:
- terraform init -backend=false
- terraform validate
plan:
stage: plan
script:
- terraform init
- terraform plan -out=tfplan -var-file=${TF_VARS}
artifacts:
paths: [infra/tfplan]
apply:
stage: apply
script: terraform apply tfplan
when: manual # 人工点按钮执行:生产环境的审批门
only: [main]
when: manual 就是 GitLab 里的人工审批;计划文件用 artifacts 从 plan 阶段传给 apply 阶段。GitHub Actions 跨 job 传文件则用 actions/upload-artifact。
步骤 4:用工作区或变量切环境
方式一:同一套配置用 workspace(适合差异小的临时环境):
terraform workspace new prod
terraform workspace select prod
terraform plan -var-file=envs/prod.tfvars
方式二(更推荐用于强隔离):CI 变量 TF_VARS=envs/prod.tfvars 指向独立 tfvars,state key 也通过不同前缀/目录隔离。生产流水线单独一条,审批人、执行窗口都与开发环境分开。
给变量传 CI 密钥:
env:
TF_VAR_db_password: ${SECRET_DB_PASSWORD}
值来自流水线加密变量/Secret,不进任何文件。
步骤 5:漂移检测
新增一个定时任务,只跑 plan 不 apply:
drift:
stage: plan
script:
- terraform init
- terraform plan -detailed-exitcode -var-file=envs/prod.tfvars
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
退出码为 2 时说明云上状态和代码不一致:可能是有人控制台手改、云厂商侧自动扩缩、或上次 apply 部分失败。任务应发告警(IM/邮件),由人判断是把现实改回代码(重新 apply),还是把现实吸收进代码。
步骤 6:把 plan 贴到 PR(协作关键)
让评审者不下载日志就能看到计划:GitHub 可用 hashicorp/setup-terraform 的输出配合评论动作;GitLab 用 terraform show tfplan 输出到 job 日志或 MR comment。团队约定:PR 没有 plan 截图/评论不批,这是把「看计划」变成肌肉记忆的流程手段。
讲清楚要点
- CI 里的 Terraform 版本也要钉:setup-terraform 指定版本或用 Docker 镜像 tag,避免 runner 升级带来意外。
- 凭证优先级:OIDC 联邦角色 > 短期 token > CI 加密变量里的 AK/SK;最后一种要定期轮换并最小权限。
- apply 必须有审批门:PR 评审 + main 分支保护 + manual job,三道里至少占两道。
- 计划文件要防篡改:严格做法是 plan 与 apply 在同一可信流水线、对 tfplan 做哈希校验;超时未执行应重新 plan。
- 不要在 CI 里频繁 -target:流水线里出现 target 通常意味着流程出了问题,应修正整体配置。
你可能会懵的地方
-
plan 和 apply 能放两个 job 吗?计划文件怎么传? 能,也是推荐做法(plan 可被多人看、apply 受门禁控)。用 artifacts 上传 tfplan,apply job 下载后执行;文件包含敏感信息,artifact 保留时间要设短。
-
OIDC 不支持 / 还没配,怎么办? 暂时用 CI 平台的加密变量存 AK/SK,配最小权限 + 轮换计划,并尽快迁移到 OIDC,这是目前云上 CI 的最佳实践。
-
workspace 为什么不被推荐当多环境主方案? 它只在 state 路径上加前缀,变量、权限、执行窗口仍是同一套,容易误把小规格 apply 到生产;独立目录/流水线 + 独立 state key 隔离更彻底。
-
漂移检测天天报差异,但没人处理怎么办? 告警疲劳等于没有检测。先收敛存量差异(吸收或修复),再把告警接入真正有人盯的频道,并区分「预期内的云侧变化」和「异常手改」。
-
plan 在 PR 上跑会写 state 吗? plan 不写 state,但远程后端会短暂加读锁;并发量极大时注意 DynamoDB 读容量,一般不是问题。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 长期 AK/SK 明文存 CI 变量还不轮换 | 泄露即全账号沦陷 | OIDC 角色或定期轮换最小密钥 |
| apply job 无 manual、无分支保护 | 合并即改生产 | 加审批门,限制触发条件 |
| fmt/validate 跳过只跑 apply | 低级错误进生产 | 质量三连设为必过门禁 |
| 漂移检测发现差异自动 apply | 把野改动「洗白」 | 只告警,人工判断后处理 |
| 一条流水线混用所有环境变量 | 误操作面大 | 每环境独立 pipeline 与 tfvars |
卡点急救
- CI 中 init 拉不到 provider:runner 出口网络受限。配置缓存目录、网络镜像,或允许访问 registry 域名。
- apply 报 state lock 且任务已失败:确认无其他流水线在跑,
terraform force-unlock;并排查为什么任务崩溃没释放锁(极少见,多为进程被强杀)。 - OIDC 角色 Assume 失败:核对信任策略的 aud/sub 条件(仓库、分支、组织),用 STS 解码错误信息;先在手动操作中验证角色可用。
面试常问
-
描述一套生产级 Terraform CI/CD 流程。 PR 触发 fmt、validate、init、plan,计划贴在 PR 中供评审;合并主分支后由人工门禁执行 apply(使用同一份 plan 文件);凭证用 OIDC 联邦短期角色;定时任务跑 plan -detailed-exitcode 做漂移检测并告警。
-
什么是漂移?怎么检测和处理? 云上真实状态偏离代码声明,多由控制台手改或外部系统自动变更引起。定期 plan(退出码 2)发现,人工评估后:若改动合理则更新代码,若违规则重新 apply 收敛,禁止盲目自动修复。
-
CI 中如何安全传递云凭证? 首选 OIDC 联邦身份(GitHub/GitLab → 云角色),每次任务拿短期令牌;退而求其次用加密 CI 变量配合最小权限和轮换;禁止写入 .tf 或镜像。
自测三问
- fmt -check、validate、plan 三者各拦得住什么类型的问题?
- plan 与 apply 拆成两个 job 时,怎么保证执行的是被评审的那份计划?
- 漂移检测退出码 2 代表什么?收到告警你的排查顺序是什么?
小结一句话
生产级 Terraform 流水线 = 质量三连 + PR plan 评审 + 门禁 apply + 定时漂移检测,凭证一律 OIDC 短期角色,apply 永远留人工开关。
延伸
- Automate Terraform with GitHub Actions 官方教程
- Running Terraform in Automation 自动化指南
- Plan 退出码 detailed-exitcode
- 下一课:Terraform vs Helm——两个工具的职责边界。