大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
CI/CD 与工具链
这一课你将能做到
- 设计一条包含校验、计划、评审、应用四阶段的 Terraform 流水线
- 用 OIDC 让 GitLab/GitHub 无需保存 AWS 密钥即可 AssumeRole 部署
- 说出
terraform test、Terragrunt、Atlantis 各自解决什么问题、什么时候引入
为什么要学
前 10 课的命令都在你自己电脑上跑。一旦进入团队,「某个人本机 apply」就是生产事故的温床:凭证散落在个人机器、没有评审记录、谁改了什么不可追溯。正确姿势是把 Terraform 操作收进 CI/CD:代码合并触发自动 plan,人工在合并请求里评审,apply 由有权限的流水线在手动门禁后执行,全过程留痕。
本课还解决一个反复出现的安全痛点:CI 怎么连云?把 AK/SK 存进 CI Secret 仍是长期密钥,泄露面大。AWS 与主流代码平台支持 OIDC 联邦身份,流水线用短命的临时凭证工作,本机零静态密钥。本站的基础设施仓库就运行在这套模型上。
先建立图像
一条标准流水线:
push / MR
│
▼
┌──────────┐ fmt 检查 + validate + 安全扫描(第 12 课)
│ verify │
│
▼
┌──────────┐ init -input=false + plan -out=tfplan(计划作为产物传递)
│ plan │ plan 结果贴到 MR 评论,等人评审
│
▼ 手动点击(main 分支 / protected tag)
┌──────────┐
│ apply │ apply tfplan ← 保证执行的就是评审过的计划
│
▼
云资源变更完成,输出可供后续部署阶段消费
身份链路:
GitLab Job ─签名 JWT(OIDC)─▶ AWS IAM Identity Provider
│ 校验 audience/subject
▼
AssumeRole → 临时 AK/Token(最长 1 小时)
跟着做
步骤 1:在 AWS 配置 OIDC 信任
先在 AWS IAM 新建 Identity Provider,Provider URL 填你的 GitLab 地址(如 https://gitlab.com),Audience 填 https://gitlab.com。再建部署角色,信任关系(trust policy)示例:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/gitlab.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"gitlab.com:aud": "https://gitlab.com"
},
"StringLike": {
"gitlab.com:sub": "project:your-group/coderoadmap-infra:ref_type:branch:ref:main"
}
}
}
]
}
sub 条件把角色锁死到「指定仓库的 main 分支流水线」,这是防止仓库内任意流水线冒用角色的关键。角色权限按最小集合授予(可用托管的 job 函数,apply 与 plan 拆两个角色更严格)。
步骤 2:编写 GitLab CI 流水线
在基础设施仓库根目录创建 .gitlab-ci.yml:
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
variables:
TF_IN_AUTOMATION: "true"
AWS_REGION: "ap-northeast-1"
TF_ROOT: "${CI_PROJECT_DIR}"
stages: [verify, plan, apply]
image: hashicorp/terraform:1.7
fmt:validate:
stage: verify
id_tokens:
GITLAB_OIDC_TOKEN:
aud: https://gitlab.com
script:
- terraform fmt -check -recursive
- terraform init -backend=false
- terraform validate
plan:dev:
stage: plan
id_tokens:
GITLAB_OIDC_TOKEN:
aud: https://gitlab.com
script:
- |
export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" \
$(aws sts assume-role-with-web-identity
--role-arn arn:aws:iam::123456789012:role/gitlab-tf-dev
--web-identity-token $GITLAB_OIDC_TOKEN
--role-session-name "gitlab-${CI_JOB_ID}"
--query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]'
--output text))
- terraform init -input=false
- terraform plan -input=false -out=tfplan -var-file=dev.tfvars
- terraform show tfplan
artifacts:
paths: [tfplan]
expire_in: 7 days
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
apply:dev:
stage: apply
needs: [plan:dev]
id_tokens:
GITLAB_OIDC_TOKEN:
aud: https://gitlab.com
script:
- # 同样的 assume-role-with-web-identity 导出临时凭证
- terraform init -input=false
- terraform apply -input=false tfplan
when: manual
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
environment:
name: dev
要点解读:
id_tokens让每个 Job 拿到平台签名的 OIDC JWT,无需任何 CI Secret;- plan 用
artifacts把二进制计划交给 apply Job,中间配置无法被偷改——评审与执行严格一致; - apply 加
when: manual做人工门禁,只在 main 分支出现; -input=false、TF_IN_AUTOMATION=true让命令在无 TTY 环境不挂起、错误退出码干净。
GitHub Actions 思路完全相同:流水线权限里声明 permissions: id-token: write,用官方 aws-actions/configure-aws-credentials 通过 role-to-assume 拿临时凭证。
步骤 3:把 plan 摘要变成机器可判的门禁
terraform show -json tfplan > plan.json
可用 jq 做硬规则,比如禁止无人审批的销毁:
jq -e '[.resource_changes[] | select(.change.actions | index("delete"))] | length == 0' plan.json
也可直接检查:
terraform plan -detailed-exitcode
# 退出码:0=无差异 1=出错 2=有差异
本站 CI 用详细退出码驱动「是否需要通知/是否阻塞合并」,而不是解析人类文本。
步骤 4:用 terraform test 做原生单元测试
1.6 GA 的 terraform test 让你对模块写断言而不建真实资源。在模块下建 tests/main.tftest.hcl:
run "variable_defaults" {
command = plan
assert {
condition = aws_security_group.web.name == "web-dev"
error_message = "安全组命名不符合规范。"
}
}
run "mocked_lb" {
command = plan
mock_provider "aws" {
mock_resource "aws_lb" {
defaults = { dns_name = "mock.alb.example" }
}
}
assert {
condition = aws_lb.web.dns_name == "mock.alb.example"
error_message = "应输出 ALB DNS。"
}
}
执行:
terraform test
# Success! 2 passed, 0 failed.
1.7 进一步支持子模块与模块调用的 mock,适合在 CI verify 阶段快速跑,不碰云、不收费。它替代不了集成环境的真实 apply,但能挡住绝大多数低级错误。
步骤 5:Terragrunt 与 Atlantis 的定位
- Terragrunt:解决「多环境/多账号重复 YAML、远程 state 配置样板、DRY 的 include 顶层 HCL」。当你有几十个账号、每个账号几十个环境,原生 Terraform 的目录样板成为负担时引入;小团队引入只会增加一层配置语言。本站规模暂不使用。
- Atlantis:自托管的「MR 评论触发 plan/apply」工具,
atlantis plan、atlantis apply,自带项目级锁。适合不能用 GitLab/GitHub 原生环境、或多仓库统一入口的团队。 - 其他常见配套:
terraform-docs(自动生成变量/输出文档)、tflint(provider 感知的静态检查)、infracost(plan 成本预估贴 MR)。工具引入原则不变:先有真实痛点,再增加组件。
讲清楚要点
- 流水线即唯一 apply 入口:生产凭证只给角色不给人,plan 必经评审,apply 必经门禁。
- OIDC 消除长期密钥:信任策略里 audience + 仓库 + 分支三个条件缺一不可,临时凭证短命自动失效。
- planfile 跨阶段传递:保证「看到的」和「执行的」是同一份计划,避免 TOCTOU 问题。
- plan/apply 角色分离:进一步可以 plan 角色只读、apply 角色才写,手动门禁同时切换角色。
- 原生能力优先,外挂工具按需:
terraform test、detailed-exitcode 已经覆盖大量需求,Terragrunt/Atlantis 是规模化后的选择,不是入门必需。
你可能会懵的地方
-
MR 上的 plan 是在合并前跑的,状态会不会提前写脏? plan 不改资源;但它会读取(Refresh)远程 state。配置
-lock=false可避免长占锁,正规后端读操作是安全的。 -
OIDC 报
Not authorized to perform: sts:AssumeRoleWithWebIdentity? 逐个核对:provider URL 证书指纹、audience 拼写、trust policy 的 sub 与实际流水线是否一致(分支保护、fork MR 的 sub 不同,默认拿不到)。 -
plan artifacts 过期了 apply 失败怎么办? 重新跑 plan 即可;planfile 是短期凭据,不可跨天复用本来也是安全特性。
-
为什么不用 CI Secret 存 AK,简单直接? 能用但属于长期密钥:轮转靠人、泄露窗口长、权限无法按分支收窄。OIDC 把「身份」交给平台、把「授权」留在自己账号。
-
应用部署(jar/镜像)和基础设施变更放一条流水线吗? 建议分开:基础设施变更低频高风险、应用发布高频;二者通过 output(ALB、ECS 集群名)衔接,避免一条长流水线互相阻塞。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| CI 变量里存长期 AWS AK | 泄露即长期失守 | OIDC AssumeRoleWithWebIdentity |
| apply 阶段重新 plan | 评审与执行可能不是一份变更 | plan -out 产物传递,apply planfile |
| apply 自动触发无门禁 | 错误代码直上生产 | when: manual + protected branch |
trust policy sub 用通配 * | 任何仓库流水线均可冒用 | 锁仓库 + ref main |
| 在所有 Job 用高权管理员角色 | 违反最小权限 | plan/apply 分角色、按环境分角色 |
卡点急救
- terraform 命令在 CI 卡住等待输入:全部命令加
-input=false,缺失变量应直接失败而不是提示;确认 tfvars 已随仓库提供。 - OIDC 换平台/自建 GitLab 后指纹错误:IAM Identity Provider 的 thumbprint 要与实际 TLS 链一致(AWS 文档给出常见值;自建网关用中间 CA 指纹)。
- apply Job 找不到 tfplan:检查 artifacts 路径与 runner 是否同架构(planfile 不跨平台);同一条流水线用同一镜像、同架构 runner。
面试常问
-
你们的 Terraform CI/CD 怎么设计?如何保证安全? 四阶段(校验→计划→评审→手动应用),OIDC 联邦拿临时凭证、trust policy 锁定仓库与分支,planfile 作为产物跨阶段保证一致,apply 角色与分支保护配合,扫描与测试左移到 verify 阶段。
-
CI 如何不保存云密钥? OIDC:平台签发 JWT,AWS 侧建 Identity Provider 与角色,条件限定 audience、仓库路径和分支;流水线 AssumeRoleWithWebIdentity 得到最长 1 小时的临时凭证。
-
terraform test 能测什么、不能测什么? 能对模块做 plan 级断言、mock provider/资源,快速验证逻辑与命名;不能替代真实云上的集成验证(网络连通、IAM 实际生效),二者分层使用。
自测三问
- 从代码推送开始,描述本站基础设施仓库一次完整变更经过哪几个 Job?
- 为什么说「apply 时重新 plan」破坏了评审的意义?正确做法是什么?
- 什么规模下才值得引入 Terragrunt?它解决的核心痛点是什么?
小结一句话
CI/CD 把 Terraform 从个人操作变成受控流水线:OIDC 消灭静态密钥、planfile 保证评审即执行、手动门禁与角色分离守住生产,terraform test 和外挂工具则按规模逐层引入。
延伸
- Terraform in Automation 官方指南
- GitLab CI + AWS OIDC 联邦说明
- terraform test 官方教程
- 下一课:安全与测试——Checkov/Trivy 策略扫描、密钥防泄漏、IAM 治理与整体安全基线。