概述

去年接手一个项目,前同事写的 Terraform 代码跑得好好的,某个周一上午他改了一行 instance_typeterraform 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 的原因有三个:

  1. 规则覆盖率更高:checkov 内置 1000+ 规则,tfsec 约 300 条
  2. 框架支持更广:checkov 同时扫 Terraform、K8s、Dockerfile,tfsec 只管 Terraform
  3. 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%
两者结合 + --compact1分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 validateHCL 语法和内部引用<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 的局限

我实测后发现三个问题:

  1. 断言能力有限:只能检查 plan/apply 输出中的属性,不能做 HTTP 请求验证服务是否真的能访问
  2. 不支持并行:多个 run 块串行执行,大项目跑完要十几分钟
  3. 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软告警(不阻断)
planlint+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.ymlskip-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验证Terraform200+支持(插件)必装,语法和参数验证
checkov安全+合规Terraform/K8s/CFN/Docker1000+支持(Python)必装,安全扫描
tfsec安全Terraform300+有限备选,与 checkov 功能重叠
Terrascan安全+合规Terraform/K8s/CFN500+支持备选,社区不如 checkov 活跃

我的推荐组合:tflint + checkov。tfsec 在 2023 年已被 Aqua Security 收购并合并到 Trivy,不建议新项目采用。

集成测试工具对比

工具语言学习曲线功能覆盖推荐场景
TerratestGo中等部署+验证+销毁全流程首选,社区最活跃
kitchen-terraformRuby类似 Terratest已有 Ruby/Infra 工具链的团队
terraform testHCLplan/apply 断言轻量验证,不需要外部工具
OpenTofu testHCL同 terraform testOpenTofu 用户的等价方案

我的推荐:日常用 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 接入 CIPR 自动触发,<10s 完成
第2周3 天checkov 接入 CI软告警模式,输出 SARIF
第3-4周5 天terraform test 断言覆盖核心模块的 plan 验证
第5-6周7 天Terratest 集成测试核心模块 E2E 测试通过
持续Conftest 策略即代码团队策略标准化

参考资料与致谢

  1. Terratest - GitHub — Gruntwork.io,提供了 Terraform 端到端测试框架的核心设计和 API
  2. Terraform Testing 官方文档 — HashiCorp,terraform test 命令的官方文档和 .tftest.hcl 语法规范
  3. Checkov - GitHub — Bridgecrew (Prisma Cloud),IaC 安全扫描工具,覆盖 CIS Benchmark 和云安全最佳实践
  4. TFLint - GitHub — Terraform Linters,Terraform 代码检查工具,支持云 API 验证
  5. Azure Terraform 集成测试最佳实践 — Microsoft Azure Docs,Terraform 项目集成测试方法论参考
  6. Conftest - GitHub — Open Policy Agent,基于 OPA/Rego 的策略即代码验证工具
  7. IaC 测试金字塔方法论 — CSDN,IaC 测试分层策略的中文参考资料