概述
去年接手一个项目,前同事写的 Terraform 代码跑得好好的,某个周一上午他改了一行 instance_type,terraform plan 显示正常,apply 也成功了。然后业务炸了。
原因不是代码写错,是那个 instance_type 对应的 AMI 在目标可用区不存在。plan 能过是因为它只检查"逻辑上通不通",不检查"云上到底有没有这个资源"。这种问题在静态检查阶段就能拦住,但那个团队的 IaC 流水线里只有 terraform fmt && terraform validate,连 tflint 都没装。
很多人对 IaC 测试的认知停留在"跑一下 plan 看看输出对不对"。这不叫测试,这叫碰运气。Terraform 不是事务系统——apply 失败后基础设施会停在一个半成品状态,比没改之前更糟。我在多个生产环境中踩过这个坑,最后总结出一套四层验证体系:静态检查 → 安全扫描 → 计划验证 → 集成测试,从代码提交到生产部署逐层拦截问题。本文拆解每一层的工具选型、配置方法和踩坑细节。
本文假设你已经了解 Terraform 基本用法。如果不熟悉,先看 相关文章:Terraform 基础设施即代码入门 和 相关文章:状态文件丢了怎么办:Terraform State 灾难恢复与生产级后端架构。
一、为什么 IaC 需要测试体系
先说清楚一个认知误区:terraform plan 通过 ≠ 代码正确。
terraform plan 只验证两件事:HCL 语法是否合法、资源依赖关系是否合理。它不验证:
instance_type在目标区域是否可用(t2.nano 在某些区域不存在)- 安全组规则是否暴露了不该暴露的端口(0.0.0.0/0 开 22 端口)
- IAM 策略是否过宽(
Action: "*", Resource: "*"这种"上帝权限") - 资源命名是否符合团队规范(一个 VPC 叫
prod-vpc另一个叫test_vpc_01) - 模块间参数传递是否类型匹配
这些全是生产事故的高频原因。我在某电商平台做过统计:37% 的 IaC 相关故障,terraform plan 阶段没有任何报错,是 apply 后才暴露的。
IaC 测试金字塔
参考传统软件测试的金字塔模型,IaC 测试同样分层:
| 层级 | 验证内容 | 执行速度 | 成本 | 工具 |
|---|---|---|---|---|
| L1 静态检查 | 语法、格式、规范 | <5s | 零成本(纯本地) | tflint, terraform fmt |
| L2 安全扫描 | 安全策略、合规基线 | <30s | 零成本(不连云) | checkov, tfsec |
| L3 计划验证 | 资源逻辑、参数有效性 | 30-120s | 低(需要 API 凭证) | terraform plan, terraform validate |
| L4 集成测试 | 实际部署后行为验证 | 3-15min | 高(真实资源费用) | Terratest, kitchen-terraform |
关键原则:L1-L2 在 PR 提交时触发,3 秒内给反馈;L3 在 CI 流水线中执行,需要云 API 凭证但不创建资源;L4 在合并到主分支后的预发环境执行,创建真实资源做验证后销毁。四层逐级递进,越往上越快越便宜,能拦住的问题越基础。
二、L1:静态检查——tflint 把问题掐死在提交前
tflint 是干嘛的
tflint 是 Terraform 专属的 lint 工具,它干的事和 terraform validate 不一样。validate 只管 HCL 语法对不对(括号匹配没、资源类型名拼对没),tflint 管的是"你写的配置在云上到底能不能用"。
举个具体例子:
resource "aws_instance" "web" {
ami = "ami-12345678" # 这个 AMI 在 ap-northeast-1 存在
instance_type = "t2.nano" # 但 t2.nano 在 us-east-2 不支持
region = "us-east-2" # terraform validate 完全不报错
}
terraform validate 返回 Success! The configuration is valid.。但 tflint 会告诉你:t2.nano is not a valid instance type in us-east-2。这就是区别——tflint 会查云厂商的 API,验证你写的参数在目标区域是否合法。
安装与初始化
# macOS
brew install tflint
# Linux (amd64)
curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash
# 初始化规则集(必须执行,否则不加载任何插件)
tflint --init
tflint 从 v0.50 开始支持插件化架构,通过 --init 下载对应云厂商的规则集。目前官方维护三个插件:AWS、Azure、Google。如果你用阿里云或腾讯云,可以用社区维护的规则集或者只跑通用规则。
配置文件
在项目根目录创建 .tflint.hcl:
plugin "aws" {
enabled = true
version = "0.27.0"
source = "github.com/terraform-linters/tflint-ruleset-aws"
}
# 关闭不需要的规则(减少噪音)
rule "aws_instance_invalid_type" {
enabled = true
}
# 关闭命名规范检查(如果你有自己的规范)
rule "terraform_naming_convention" {
enabled = false
}
# 强制使用最新语法
rule "terraform_deprecated_interpolation" {
enabled = true
}
# 检查未使用的变量
rule "terraform_unused_declarations" {
enabled = true
}
生产环境踩坑:tflint 超时
在某新能源物流平台的 CI 流水线中,tflint 扫描 80+ 个 .tf 文件需要 45 秒。这不算慢,但加上 checkov 和 terraform plan,整个 PR 检查超过 3 分钟,开发同学开始抱怨。
解法:用 --var-file 预加载变量避免重复查询,配合 --format=compact 精简输出。如果你用 GitHub Actions,可以用 tflint/setup-tflint@v3 action 预装缓存:
- name: Setup tflint
uses: tflint/setup-tflint@v3
- name: Run tflint
run: tflint --config=.tflint.hcl --format=compact
实测在缓存命中时,扫描时间从 45 秒降到 12 秒。
三、L2:安全扫描——checkov 守住安全底线
为什么需要 checkov
tflint 管"代码对不对",checkov 管"代码安不安全"。两者职责不重叠。
checkov(读音 “check-off”)是 Bridgecrew 公司开源的 IaC 静态安全扫描工具,支持 Terraform、CloudFormation、Kubernetes、Dockerfile 等。它内置了 1000+ 条安全规则,覆盖 CIS Benchmark、AWS Foundational Security Best Practices、PCI-DSS 等合规标准。
我推荐 checkov 而不是 tfsec 的原因有三个:
- 规则覆盖率更高:checkov 内置 1000+ 规则,tfsec 约 300 条
- 框架支持更广:checkov 同时扫 Terraform、K8s、Dockerfile,tfsec 只管 Terraform
- CI/CD 集成更好:checkov 支持 SARIF 输出,能直接在 GitHub PR 里展示安全告警
安装与基本用法
# 安装
pip install checkov
# 基础扫描
checkov -d .
# 只扫描特定框架
checkov -d . --framework terraform
# 输出 SARIF 格式(用于 GitHub Code Scanning)
checkov -d . --output sarif > results.sarif
# 只跑特定检查(如只检查 S3 相关)
checkov -d . --check CKV_AWS_S3_*
# 跳过特定检查(误报太多时)
checkov -d . --skip-check CKV_AWS_18
实战配置:抑制噪音 + 精准告警
checkov 默认全开所有规则,在小项目上会输出大量噪音。生产环境推荐分层配置:
# .checkov.yaml(项目级配置)
framework:
- terraform
# 启用的检查类别
checks:
- CKV_AWS_18 # S3 bucket logging
- CKV_AWS_19 # S3 bucket encryption
- CKV_AWS_338 # ECR image scanning
- CKV_AWS_40 # IAM policy attached to role not user
- CKV_AWS_52 # S3 bucket MFA delete
- CKV_AWS_145 # S3 bucket kms encryption
- CKV_AWS_28 # DynamoDB KMS encryption
- CKV_AWS_21 # S3 versioning enabled
# 跳过的检查(业务上不需要或已知误报)
skip-checks:
- CKV_AWS_117 # Aurora cluster encryption(业务不需要)
- CKV_AWS_136 # ECR tag policy(团队不用 tag 策略)
# 跳过特定目录(第三方模块不扫)
skip-path:
- .terraform/
- modules/third-party/
踩坑:checkov 性能优化
checkov 在大项目(1000+ 资源)上的扫描时间会飙到 15 分钟。我实测过三个优化手段的效果:
| 优化手段 | 扫描时间(500资源) | 优化率 |
|---|---|---|
| 默认配置 | 8分15秒 | 基准 |
--skip-path 过滤 .terraform 目录 | 3分40秒 | -55% |
CHECKOV_WORKERS_NUMBER=4 并行 | 1分50秒 | -78% |
两者结合 + --compact | 1分12秒 | -85% |
结论:并行 + 路径过滤是最有效的组合拳。在 CI 中这样配:
# GitHub Actions 示例
- name: Run checkov
env:
CHECKOV_WORKERS_NUMBER: 4
run: |
checkov -d . --framework terraform \
--skip-path .terraform/ \
--skip-path modules/third-party/ \
--output cli --soft-fail
--soft-fail 参数的作用:发现安全问题不阻断 CI,只输出告警。这在团队刚引入 checkov 时很实用——先让大家看到问题,形成习惯后再切换为硬门禁(不加 --soft-fail,checkov 发现问题会返回 exit code 1,阻断 CI)。
我的建议:前两周用
--soft-fail,第三周开始硬门禁。别一上来就堵 CI,团队会找你"算账"的。
四、L3:计划验证——terraform plan 不只是看输出
terraform validate vs terraform plan
这两个命令经常被混淆,简单说:
| 命令 | 验证内容 | 需要云凭证 | 创建资源 | 耗时 |
|---|---|---|---|---|
terraform validate | HCL 语法和内部引用 | 否 | 否 | <2s |
terraform plan | 资源依赖关系和变更预览 | 是 | 否 | 30-120s |
validate 是纯本地操作,不连云,只看代码结构。plan 会连接云 API,查询当前资源状态,计算 diff,生成执行计划。plan 通过才意味着"代码在云上能跑"。
但 plan 也有盲区:它不执行,所以无法验证 apply 后的实际行为。比如 Security Group 规则的优先级冲突,plan 不会报错,但 apply 后流量可能被另一条优先级更高的规则覆盖。
terraform test:原生测试框架
从 Terraform 1.6 开始,HashiCorp 引入了原生测试框架 terraform test。它使用 .tftest.hcl 文件定义测试用例,不需要额外安装工具。
# tests/main.tftest.hcl
variables {
region = "us-east-1"
instance_type = "t3.micro"
environment = "test"
vpc_cidr = "10.0.0.0/16"
}
# 测试1:验证 VPC CIDR 配置正确
run "validate_vpc_cidr" {
command = plan
assert {
condition = aws_vpc.main.cidr_block == var.vpc_cidr
error_message = "VPC CIDR should match the variable"
}
}
# 测试2:验证子网数量正确
run "validate_subnet_count" {
command = plan
assert {
condition = length(aws_subnet.public) == 3
error_message = "Should create 3 public subnets for HA"
}
}
# 测试3:验证安全组不暴露 22 端口
run "no_ssh_to_world" {
command = plan
assert {
condition = !anytrue([for rule in aws_security_group.web.ingress : contains(rule.cidr_blocks, "0.0.0.0/0") if rule.from_port == 22])
error_message = "Security group must not allow SSH from 0.0.0.0/0"
}
}
# 测试4:实际 apply 后验证(需要真实云凭证)
run "deploy_and_verify" {
command = apply
assert {
condition = aws_instance.web.id != ""
error_message = "Instance should have an ID after creation"
}
}
运行测试:
# 运行所有测试
terraform test
# 指定测试目录
terraform test -test-directory=tests/
# JSON 格式输出(用于 CI 解析)
terraform test -json
terraform test 的局限
我实测后发现三个问题:
- 断言能力有限:只能检查
plan/apply输出中的属性,不能做 HTTP 请求验证服务是否真的能访问 - 不支持并行:多个 run 块串行执行,大项目跑完要十几分钟
- apply 测试会创建真实资源:需要独立的测试环境(AWS 账号/订阅),否则会污染生产 state
我的建议:terraform test 适合做 plan 阶段的断言验证(L3 层),要验证 apply 后的真实行为,还是得用 Terratest(L4 层)。
五、L4:集成测试——Terratest 验证"部署后到底行不行"
Terratest 解决什么问题
前面三层都在 apply 之前拦截问题。但有些问题只有 apply 后才暴露:
- EC2 实例创建后,UserData 脚本有没有正确执行
- ALB 健康检查是否通过,后端服务是否真的能接流量
- RDS 创建后,安全组规则是否允许应用层连接
- DNS 记录创建后,解析是否生效
Terratest 是 Gruntwork 公司开源的 Go 测试框架,专门做基础设施的端到端测试。它的核心流程是:部署真实资源 → 验证行为 → 销毁资源。
一个完整的 Terratest 示例
// test/infrastructure_test.go
package test
import (
"crypto/tls"
"fmt"
"testing"
"time"
"github.com/gruntwork-io/terratest/modules/aws"
http_helper "github.com/gruntwork-io/terratest/modules/http-helper"
"github.com/gruntwork-io/terratest/modules/terraform"
)
func TestWebAppInfrastructure(t *testing.T) {
t.Parallel()
// 1. 构造 Terraform 选项
terraformOptions := &terraform.Options{
// Terraform 代码路径
TerraformDir: "../modules/web-app",
// 传入测试变量
Vars: map[string]interface{}{
"region": "us-east-1",
"environment": "test",
"instance_type": "t3.micro",
"min_size": 2,
"max_size": 4,
"health_check_path": "/health",
},
// 测试结束后自动销毁(即使测试失败也会执行)
// 这是 Terratest 最关键的设计——不会留下垃圾资源
},
// 2. 部署基础设施,测试结束后销毁
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
// 3. 获取部署后的输出
albDnsName := terraform.Output(t, terraformOptions, "alb_dns_name")
url := fmt.Sprintf("http://%s/health", albDnsName)
// 4. 验证:ALB 健康检查通过,HTTP 200
// 重试 30 次,每次间隔 10 秒(给 ASG 扩容时间)
maxRetries := 30
timeBetweenRetries := 10 * time.Second
http_helper.HttpGetWithRetryWithCustomValidation(
t,
url,
&tls.Config{InsecureSkipVerify: true},
maxRetries,
timeBetweenRetries,
func(statusCode int, body string) bool {
return statusCode == 200 && body == "OK"
},
)
}
Terratest 的成本控制
Terratest 会创建真实云资源,这带来两个问题:费用和测试隔离。
费用控制:
// 强制使用最便宜的实例类型
Vars: map[string]interface{}{
"instance_type": "t3.micro", // 最小规格
"disk_size": 8, // 最小磁盘
"retention_days": 1, // 日志保留1天
},
// 设置超时,防止卡住导致资源不销毁
terraformOptions.MaxRetries = 3,
terraformOptions.RetrySleepSeconds = 10,
某出行项目的实测数据:一次 Terratest 测试创建 1 个 ALB + 2 台 EC2 + 1 个 RDS,跑一次约 8 分钟,云资源费用约 $0.03。每天 CI 跑 20 次,月费用约 $18。这个成本远低于一次生产事故的损失。
测试隔离:
// 每个测试用例加唯一后缀,避免资源名冲突
uniqueId := random.UniqueId()
terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
TerraformDir: "../modules/web-app",
Vars: map[string]interface{}{
"name_prefix": fmt.Sprintf("terratest-%s-", uniqueId),
},
})
踩坑:Terratest 超时导致资源泄漏
这是最危险的问题。如果 CI 超时杀掉了测试进程,terraform destroy 没来得及执行,资源就泄漏了。某次我们 CI 超时(设置了 15 分钟上限),Terratest 正在创建 RDS(RDS 创建本身要 5-8 分钟),结果 RDS 没销毁,默默跑了两个月,账单 $340。
解法:两层保护:
# 第一层:CI 级别,定时清理残留资源
# crontab: 每天凌晨 3 点执行
0 3 * * * aws ec2 describe-instances --filters "Name=tag:CreatedBy,Values=terratest" \
--query 'Reservations[].Instances[?State.Name==`running`].InstanceId' \
--output text | xargs -I {} aws ec2 terminate-instances --instance-ids {}
# 第二层:Terratest 代码级别,强制超时
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
go func() {
terraform.InitAndApply(t, terraformOptions)
defer terraform.Destroy(t, terraformOptions)
cancel()
}()
<-ctx.Done()
六、CI/CD 流水线集成:四层验证串联
GitHub Actions 完整流水线
把四层验证串成一条流水线,每层一个 job,前一层失败不执行后一层:
# .github/workflows/terraform-ci.yml
name: Terraform CI
on:
pull_request:
paths:
- '**.tf'
- '**.tfvars'
env:
TF_VERSION: '1.9.5'
AWS_REGION: 'us-east-1'
jobs:
# L1: 静态检查(最快,先跑)
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: terraform fmt
run: terraform fmt -check -recursive
- name: Setup tflint
uses: tflint/setup-tflint@v3
- name: tflint init
run: tflint --init --config=.tflint.hcl
- name: Run tflint
run: tflint --config=.tflint.hcl --format=compact
# L2: 安全扫描
security:
runs-on: ubuntu-latest
env:
CHECKOV_WORKERS_NUMBER: 4
steps:
- uses: actions/checkout@v4
- name: Run checkov
uses: bridgecrewio/checkov-action@v12
with:
directory: .
framework: terraform
skip_path: .terraform/,modules/third-party/
output_format: cli
soft_fail: true # 先软告警,团队习惯后改 false
# L3: 计划验证(需要云凭证)
plan:
needs: [lint, security]
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-tf
aws-region: ${{ env.AWS_REGION }}
- name: terraform init
run: terraform init -backend=false
- name: terraform validate
run: terraform validate
- name: terraform test
run: terraform test -test-directory=tests/
- name: terraform plan
run: terraform plan -no-color -out=tfplan
continue-on-error: true
- name: Post plan to PR
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const plan = fs.readFileSync('tfplan', 'utf8');
github.rest.issues.createComment({
...context.repo,
issue_number: context.issue.number,
body: `## Terraform Plan\n\`\`\`\n${plan}\n\`\`\``
});
# L4: 集成测试(仅主分支 PR 触发,成本最高)
integration:
needs: plan
if: github.base_ref == 'main'
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-tf-test
aws-region: ${{ env.AWS_REGION }}
- name: Run Terratest
run: |
cd test
go test -v -timeout 900s -parallel 2
流水线分层策略
| Job | 触发条件 | 预期耗时 | 失败行为 |
|---|---|---|---|
| lint | 每次 PR | <15s | 阻断 PR |
| security | 每次 PR | <30s | 软告警(不阻断) |
| plan | lint+security 通过后 | 60-120s | 阻断 PR |
| integration | 仅 PR 到 main 分支 | 5-15min | 阻断合并 |
关键设计:integration 测试只在 PR 合并到 main 时触发,不在每个 PR 上跑——因为 Terratest 会创建真实云资源,频繁运行既费钱又容易因为云 API 限流而误报。
GitLab CI 等价配置
如果你用 GitLab CI,核心逻辑一样,把上面四个 job 换成 .gitlab-ci.yml 的 stages:
stages:
- lint
- security
- plan
- integration
variables:
TF_VERSION: "1.9.5"
tflint:
stage: lint
image: hashicorp/terraform:$TF_VERSION
script:
- apk add --no-cache curl
- curl -sL https://github.com/terraform-linters/tflint/releases/latest/download/tflint_linux_amd64.zip -o /tmp/tflint.zip
- unzip /tmp/tflint.zip -d /usr/local/bin
- tflint --init --config=.tflint.hcl
- tflint --config=.tflint.hcl --format=compact
rules:
- changes: ["**/*.tf"]
checkov:
stage: security
image: bridgecrew/checkov:latest
script:
- checkov -d . --framework terraform --soft-fail
rules:
- changes: ["**/*.tf"]
terraform-plan:
stage: plan
image: hashicorp/terraform:$TF_VERSION
script:
- terraform init -backend=false
- terraform validate
- terraform plan -no-color
rules:
- changes: ["**/*.tf"]
七、测试策略与成本权衡
不同规模团队的推荐方案
| 团队规模 | 推荐层级 | 理由 |
|---|---|---|
| 1-3 人小团队 | L1 + L2 | 快速上手,tflint + checkov 已能拦住 80% 问题 |
| 5-15 人中型团队 | L1 + L2 + L3 | 加上 plan 验证和 terraform test 断言 |
| 15+ 人大型团队 | 全四层 | 必须有 Terratest 集成测试,否则核心模块改一行影响全局 |
| 企业级(多团队) | 全四层 + OPA 策略 | 加 Conftest/OPA 做策略即代码门禁 |
这个分层不是拍脑袋定的。在某新能源物流平台做 K8s 迁移时(相关文章:微服务限流熔断降级策略 中提到过这个项目),我们的 Terraform 代码从最初的 20 个文件膨胀到 300+ 个,团队从 3 人扩到 15 人。前三个月只有 L1,每次 PR 都有人手动 review plan 输出,平均 15 分钟/PR。引入 L2+L3 后,PR 检查时间降到 3 分钟,人工 review 只看 plan diff 和架构变更,效率提升 80%。
成本对比:测试投入 vs 事故损失
我用真实数据算过一笔账:
| 项目 | 无测试 | 有四层测试 |
|---|---|---|
| PR 检查时间 | 15min(人工) | 3min(自动) |
| 月度云资源泄漏 | $200-500 | $0-20 |
| 生产事故(IaC 相关) | 2-3 次/季度 | 0-1 次/季度 |
| 事故平均损失 | $2000-5000/次 | — |
| 测试工具成本 | $0 | $0(全开源) |
| Terratest 云资源费 | $0 | $18/月 |
| 季度总成本 | $8000-18000 | $54 |
这笔账很清楚:测试不是成本,是投资。
八、高级技巧:策略即代码门禁
用 Conftest 做策略验证
tflint 和 checkov 是"用别人定义的规则",Conftest 是"用你自己定义的规则"。它基于 OPA(Open Policy Agent)的 Rego 语言,可以自定义任何策略。
比如,你们团队规定所有 S3 bucket 必须开启版本控制和加密:
# policy/s3_policy.rego
package main
# 规则1:所有 S3 bucket 必须开启版本控制
deny[msg] {
resource := input.resource.aws_s3_bucket[_]
not resource.versioning.enabled
msg := sprintf("S3 bucket '%s' must have versioning enabled", [resource.name])
}
# 规则2:所有 S3 bucket 必须开启 KMS 加密
deny[msg] {
resource := input.resource.aws_s3_bucket[_]
not resource.server_side_encryption_configuration
msg := sprintf("S3 bucket '%s' must have KMS encryption enabled", [resource.name])
}
# 规则3:禁止创建 0.0.0.0/0 的安全组入站规则
deny[msg] {
resource := input.resource.aws_security_group_rule[_]
resource.type == "ingress"
contains(resource.cidr_blocks, "0.0.0.0/0")
msg := sprintf("Security group rule '%s' allows 0.0.0.0/0 ingress", [resource.name])
}
# 规则4:强制所有资源必须有 Environment 标签
deny[msg] {
resource := input.resource[_][_]
not resource.tags.Environment
msg := sprintf("Resource '%s' must have Environment tag", [resource.name])
}
运行方式:
# 先把 terraform plan 输出为 JSON
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json
# 用 conftest 验证
conftest test tfplan.json --policy policy/
Conftest 的价值在于:策略跟着代码走,团队任何人改 IaC 代码时,策略自动生效。不需要一个"安全审查委员会"来人工审批。
OPA + Terraform 的进阶用法
在大型组织中,策略会按环境分层:
# policy/environment_specific.rego
package main
# 生产环境的安全组不允许任何入站 22 端口
deny[msg] {
input.resource.aws_security_group_rule[_].from_port == 22
input.resource.aws_security_group_rule[_].type == "ingress"
input.variables.environment == "production"
msg := "SSH (port 22) ingress is forbidden in production"
}
# 非生产环境允许 SSH 但限制来源
warn[msg] {
rule := input.resource.aws_security_group_rule[_]
rule.from_port == 22
rule.type == "ingress"
input.variables.environment != "production"
not contains(rule.cidr_blocks[0], "10.")
msg := "SSH access should be restricted to internal IPs"
}
这种"生产环境 deny、非生产环境 warn"的策略分层,比 checkov 的 --soft-fail 精细得多。
九、常见问题与排障
问题1:terraform test 报 “no tests found”
$ terraform test
Success! 0 passed, 0 failed.
原因:测试文件不在默认的 tests/ 目录下,或者文件名不以 .tftest.hcl 结尾。
解法:检查文件路径和命名。terraform test 默认扫描当前目录和 tests/ 下的 .tftest.hcl 和 .tftest.json 文件。可以用 -test-directory 指定自定义路径。
问题2:Terratest 报 “Error acquiring the state lock”
原因:上一次测试没正常销毁,state 文件被锁。
解法:
# 查看锁信息
terraform force-unlock <LOCK_ID>
# 根因是上一次测试没跑完。检查 CI 日志,确保 terraform destroy 总是执行
# Terratest 的 defer terraform.Destroy 会保证销毁,但如果进程被 kill -9 就不行了
问题3:checkov 误报过多
原因:第三方模块的代码也被扫描,但第三方代码你改不了。
解法:用 .checkov.yml 的 skip-path 排除第三方目录:
skip-path:
- .terraform/
- modules/third-party/
- examples/
或者用行内注释跳过特定资源:
# checkov:skip=CKV_AWS_18:This is a test bucket, logging not needed
resource "aws_s3_bucket" "test" {
bucket = "my-test-bucket-12345"
}
问题4:tflint 报 “plugin not found”
原因:没有执行 tflint --init,或者 .tflint.hcl 中的 plugin source 配置错误。
解法:
# 确认插件配置
cat .tflint.hcl | grep source
# 重新初始化
tflint --init --config=.tflint.hcl
# 如果仍然失败,检查网络是否能访问 GitHub
curl -sI https://github.com/terraform-linters/tflint-ruleset-aws
十、工具链对比与选型建议
静态检查工具对比
| 工具 | 检查范围 | 支持框架 | 规则数 | 自定义规则 | 推荐场景 |
|---|---|---|---|---|---|
| tflint | 语法+最佳实践+云API验证 | Terraform | 200+ | 支持(插件) | 必装,语法和参数验证 |
| checkov | 安全+合规 | Terraform/K8s/CFN/Docker | 1000+ | 支持(Python) | 必装,安全扫描 |
| tfsec | 安全 | Terraform | 300+ | 有限 | 备选,与 checkov 功能重叠 |
| Terrascan | 安全+合规 | Terraform/K8s/CFN | 500+ | 支持 | 备选,社区不如 checkov 活跃 |
我的推荐组合:tflint + checkov。tfsec 在 2023 年已被 Aqua Security 收购并合并到 Trivy,不建议新项目采用。
集成测试工具对比
| 工具 | 语言 | 学习曲线 | 功能覆盖 | 推荐场景 |
|---|---|---|---|---|
| Terratest | Go | 中等 | 部署+验证+销毁全流程 | 首选,社区最活跃 |
| kitchen-terraform | Ruby | 高 | 类似 Terratest | 已有 Ruby/Infra 工具链的团队 |
| terraform test | HCL | 低 | plan/apply 断言 | 轻量验证,不需要外部工具 |
| OpenTofu test | HCL | 低 | 同 terraform test | OpenTofu 用户的等价方案 |
我的推荐:日常用 terraform test 做 L3 断言验证(快速、原生),核心模块用 Terratest 做 L4 端到端验证(完整、可编程)。两者不冲突,是互补关系。
总结
回到开头那个 instance_type 的问题。如果当时团队有 tflint,这个错误在 PR 提交后 5 秒内就会被拦截,根本走不到 terraform apply 那一步。
四层验证体系的核心价值不是"能发现更多 bug",而是把问题拦截在最早的阶段,用最低的成本解决。一个在 L1 静态检查阶段花 5 秒发现的问题,如果放到 L4 集成测试阶段才发现,成本是 15 分钟 + 云资源费用。如果放到生产环境才发现,成本是一次事故。
我在某电商平台落地这套体系后,IaC 相关的告警量在一个季度内下降了 80%。不是说测试消灭了所有问题,而是大部分问题在提交代码时就被挡住了,凌晨的告警自然就少了。
如果你今天只能做一件事,装一个 tflint。就这一个工具,就能帮你拦截掉大部分"代码看起来对但云上跑不了"的低级错误。然后再逐步加 checkov、terraform test、Terratest。不要一口气全上,团队消化不了。
实施路线图
| 阶段 | 时间 | 目标 | 验收标准 |
|---|---|---|---|
| 第1周 | 2 天 | tflint 接入 CI | PR 自动触发,<10s 完成 |
| 第2周 | 3 天 | checkov 接入 CI | 软告警模式,输出 SARIF |
| 第3-4周 | 5 天 | terraform test 断言 | 覆盖核心模块的 plan 验证 |
| 第5-6周 | 7 天 | Terratest 集成测试 | 核心模块 E2E 测试通过 |
| 持续 | — | Conftest 策略即代码 | 团队策略标准化 |
参考资料与致谢
- Terratest - GitHub — Gruntwork.io,提供了 Terraform 端到端测试框架的核心设计和 API
- Terraform Testing 官方文档 — HashiCorp,terraform test 命令的官方文档和
.tftest.hcl语法规范 - Checkov - GitHub — Bridgecrew (Prisma Cloud),IaC 安全扫描工具,覆盖 CIS Benchmark 和云安全最佳实践
- TFLint - GitHub — Terraform Linters,Terraform 代码检查工具,支持云 API 验证
- Azure Terraform 集成测试最佳实践 — Microsoft Azure Docs,Terraform 项目集成测试方法论参考
- Conftest - GitHub — Open Policy Agent,基于 OPA/Rego 的策略即代码验证工具
- IaC 测试金字塔方法论 — CSDN,IaC 测试分层策略的中文参考资料