Terraform 测试策略 #

测试概述 #

测试是确保 Terraform 代码质量的重要环节。通过测试可以验证配置正确性、发现潜在问题、提高代码可靠性。

text
┌─────────────────────────────────────────────────────────────┐
│                    测试层次                                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  静态分析     terraform validate, terraform fmt             │
│  计划测试     terraform plan 验证                           │
│  集成测试     实际部署并验证                                │
│  端到端测试   Terratest 等框架                              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

静态分析 #

terraform validate #

bash
terraform init
terraform validate

输出:

text
Success! The configuration is valid.

terraform fmt #

bash
terraform fmt
terraform fmt -check
terraform fmt -recursive
terraform fmt -diff

pre-commit #

yaml
repos:
  - repo: https://github.com/antonbabenko/pre-commit-terraform
    rev: v1.83.5
    hooks:
      - id: terraform_fmt
      - id: terraform_validate
      - id: terraform_tflint
      - id: terraform_tfsec

安装:

bash
pip install pre-commit
pre-commit install

TFLint #

bash
tflint
tflint --init
tflint --recursive

配置 .tflint.hcl

hcl
plugin "terraform" {
  enabled = true
  preset  = "recommended"
}

plugin "aws" {
  enabled = true
  version = "0.23.1"
  source  = "github.com/terraform-linters/tflint-ruleset-aws"
}

rule "terraform_required_version" {
  enabled = true
}

rule "terraform_required_providers" {
  enabled = true
}

rule "terraform_naming_convention" {
  enabled = true
}

tfsec #

bash
tfsec .
tfsec . --format json
tfsec . --format sarif > results.sarif

配置 .tfsec/config.yml

yaml
minimum_severity: MEDIUM

exclude:
  - AWS002
  - AWS003

计划测试 #

基本计划测试 #

bash
terraform init
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json

使用 OPA 验证计划 #

bash
opa exec --decision terraform/analysis/authz --bundle policies/ tfplan.json

策略文件 policies/terraform.rego

rego
package terraform.analysis

import input as tfplan

deny[msg] {
    some i
    change := tfplan.resource_changes[i]
    change.type == "aws_instance"
    not change.change.after.associate_public_ip_address
    msg := sprintf("Instance %s must have public IP", [change.address])
}

deny[msg] {
    some i
    change := tfplan.resource_changes[i]
    change.type == "aws_s3_bucket"
    not change.change.after.versioning[0].enabled
    msg := sprintf("Bucket %s must have versioning enabled", [change.address])
}

使用 Conftest #

bash
conftest test tfplan.json
conftest test --policy ./policies tfplan.json

Terratest #

安装 #

bash
go mod init my-terraform-tests
go get github.com/gruntwork-io/terratest/modules/terraform

基本测试 #

go
package test

import (
    "testing"
    "github.com/gruntwork-io/terratest/modules/terraform"
    "github.com/stretchr/testify/assert"
)

func TestVPCModule(t *testing.T) {
    t.Parallel()
    
    terraformOptions := &terraform.Options{
        TerraformDir: "../examples/basic",
        Vars: map[string]interface{}{
            "vpc_cidr": "10.0.0.0/16",
            "environment": "test",
        },
    }
    
    defer terraform.Destroy(t, terraformOptions)
    
    terraform.InitAndApply(t, terraformOptions)
    
    vpcId := terraform.Output(t, terraformOptions, "vpc_id")
    assert.NotEmpty(t, vpcId)
    
    subnetIds := terraform.OutputList(t, terraformOptions, "subnet_ids")
    assert.Equal(t, 2, len(subnetIds))
}

验证 AWS 资源 #

go
package test

import (
    "testing"
    "github.com/gruntwork-io/terratest/modules/aws"
    "github.com/gruntwork-io/terratest/modules/terraform"
    "github.com/stretchr/testify/assert"
)

func TestEC2Instance(t *testing.T) {
    t.Parallel()
    
    awsRegion := "us-east-1"
    
    terraformOptions := &terraform.Options{
        TerraformDir: "../examples/ec2",
        Vars: map[string]interface{}{
            "region": awsRegion,
        },
    }
    
    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)
    
    instanceId := terraform.Output(t, terraformOptions, "instance_id")
    
    instance := aws.GetEc2InstanceById(t, instanceId, awsRegion)
    assert.Equal(t, "running", instance.State.Name)
    assert.Equal(t, "t2.micro", *instance.InstanceType)
}

验证网络连接 #

go
package test

import (
    "testing"
    "github.com/gruntwork-io/terratest/modules/terraform"
    "github.com/gruntwork-io/terratest/modules/http-helper"
    "github.com/stretchr/testify/assert"
)

func TestWebServer(t *testing.T) {
    t.Parallel()
    
    terraformOptions := &terraform.Options{
        TerraformDir: "../examples/web",
    }
    
    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)
    
    url := terraform.Output(t, terraformOptions, "url")
    
    http_helper.HttpGetWithRetry(
        t,
        url,
        nil,
        200,
        "Hello, World!",
        10,
        30*time.Second,
    )
}

测试组织 #

目录结构 #

text
project/
├── modules/
│   └── vpc/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
├── examples/
│   ├── basic/
│   │   └── main.tf
│   └── complete/
│       └── main.tf
└── test/
    ├── vpc_basic_test.go
    └── vpc_complete_test.go

运行测试 #

bash
go test -v ./test/
go test -v -run TestVPCModule ./test/
go test -v -timeout 30m ./test/

CI/CD 集成 #

GitHub Actions #

yaml
name: Terraform Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v2
      
      - name: Terraform Init
        run: terraform init
      
      - name: Terraform Validate
        run: terraform validate
      
      - name: Terraform Format
        run: terraform fmt -check
      
      - name: Run TFLint
        uses: terraform-linters/setup-tflint@v3
        with:
          tflint_version: v0.47.0
      - run: tflint --init
      - run: tflint -f compact
      
      - name: Run tfsec
        uses: aquasecurity/tfsec-action@v1.0.0

  terratest:
    runs-on: ubuntu-latest
    needs: validate
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Go
        uses: actions/setup-go@v4
        with:
          go-version: '1.21'
      
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v2
      
      - name: Run Terratest
        run: |
          cd test
          go mod init test
          go get github.com/gruntwork-io/terratest/modules/terraform
          go test -v -timeout 30m

最佳实践 #

1. 测试金字塔 #

text
┌─────────────────────────────────────────────────────────────┐
│                    测试金字塔                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  端到端测试(少量)                                         │
│  └── 部署完整环境并验证                                    │
│                                                             │
│  集成测试(适量)                                           │
│  └── 部署模块并验证资源                                    │
│                                                             │
│  单元测试(大量)                                           │
│  └── 验证配置语法和计划                                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

2. 测试隔离 #

go
func TestWithUniqueName(t *testing.T) {
    t.Parallel()
    
    uniqueID := random.UniqueId()
    name := fmt.Sprintf("test-%s", uniqueID)
    
    terraformOptions := &terraform.Options{
        TerraformDir: "../examples/basic",
        Vars: map[string]interface{}{
            "name": name,
        },
    }
    
    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)
}

3. 清理资源 #

go
func TestWithCleanup(t *testing.T) {
    terraformOptions := &terraform.Options{
        TerraformDir: "../examples/basic",
    }
    
    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)
}

下一步 #

掌握了测试策略后,接下来学习 CI/CD 集成,了解如何自动化 Terraform 工作流!

最后更新:2026-03-29