왜 Terraform 모듈에 테스트가 필요한가

Terraform 코드는 terraform validate와 plan만으로는 검증에 한계가 있다. 문법은 통과하지만 실제로 리소스가 원하는 상태로 뜨는지, 출력값이 올바른지, 조건부 로직이 의도대로 분기하는지는 apply를 해봐야만 알 수 있다. 재사용 모듈일수록 문제가 크다. 한 모듈을 여러 팀이 참조하는데, 변수 하나를 바꾼 뒤 특정 조합에서만 깨지는 경우를 사람이 리뷰로 잡아내기는 어렵다.

Terratest는 실제로 terraform apply를 실행해 리소스를 생성하고, Go 코드로 결과를 검증한 뒤 destroy로 정리하는 통합 테스트 프레임워크다. 클라우드에 실제 자원을 만들기 때문에 "plan은 되는데 apply가 깨진다"는 상황을 CI에서 미리 걸러낸다.

정적 검사와 Terratest의 역할 구분

모든 것을 Terratest로 검증할 필요는 없다. 빠른 정적 검사로 대부분을 거르고, 통합 테스트는 핵심 경로에만 쓰는 계층 구성이 비용 대비 효율이 좋다.

도구검증 대상실행 속도클라우드 자원
validate / fmt문법·포맷초 단위불필요
tflint / checkov린트·보안 정책초 단위불필요
Terratest실제 동작·출력값분 단위생성됨

기본 테스트 작성

모듈 디렉터리 아래 test/에 Go 테스트를 둔다. 아래는 VPC 모듈이 만든 서브넷 개수와 출력값을 검증하는 예시다. defer로 destroy를 예약해 테스트가 실패해도 자원이 남지 않도록 한다.

package test

import (
    "testing"

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

func TestVpcModule(t *testing.T) {
    opts := &terraform.Options{
        TerraformDir: "../examples/vpc",
        Vars: map[string]interface{}{
            "cidr_block":  "10.0.0.0/16",
            "subnet_count": 3,
        },
    }
    defer terraform.Destroy(t, opts)
    terraform.InitAndApply(t, opts)

    ids := terraform.OutputList(t, opts, "subnet_ids")
    assert.Equal(t, 3, len(ids))
    assert.NotEmpty(t, terraform.Output(t, opts, "vpc_id"))
}

examples 디렉터리 패턴

모듈 루트를 직접 테스트하지 말고 examples/ 아래에 실제 호출 예시를 두고 그것을 테스트 대상으로 삼는다. 이렇게 하면 예시가 곧 문서가 되고, 사용자가 참조하는 형태 그대로 검증된다. 테스트마다 동일 자원이 충돌하지 않도록 이름에 랜덤 접미사를 붙이는 것도 중요하다.

import "github.com/gruntwork-io/terratest/modules/random"

uniqueId := random.UniqueId()
opts := &terraform.Options{
    TerraformDir: "../examples/s3",
    Vars: map[string]interface{}{
        "bucket_name": "myapp-test-" + strings.ToLower(uniqueId),
    },
}

CI 파이프라인에 연결

GitHub Actions에서 실행하는 예시다. 실제 자원을 만들므로 격리된 테스트 계정과 짧은 수명의 인증(OIDC)을 쓰고, 타임아웃을 넉넉히 잡는다.

jobs:
  terratest:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::111122223333:role/terratest-ci
          aws-region: ap-northeast-2
      - uses: hashicorp/setup-terraform@v3
      - uses: actions/setup-go@v5
        with:
          go-version: "1.22"
      - name: Run Terratest
        run: go test -v -timeout 30m ./test/...

실무에서의 주의점

가장 흔한 실수는 destroy 누락으로 인한 자원 유출이다. defer를 반드시 apply보다 먼저 선언하고, CI에는 별도로 태그 기반 정리 잡을 두어 고아 자원을 주기적으로 회수한다. 또한 테스트 계정을 운영 계정과 분리하고, 예산 알림을 걸어 폭주를 막는다.

병렬 실행 시에는 t.Parallel()과 랜덤 네이밍을 함께 써야 상태 파일과 자원 이름 충돌을 피할 수 있다. 실행 시간이 긴 테스트는 test_structure.RunTestStage로 setup/validate/teardown 단계를 나누면, 검증 로직만 반복 실행하며 디버깅할 수 있어 반복 비용이 크게 준다.

어디까지 자동화할 것인가

Terratest는 느리고 비용이 든다. 모든 변수 조합을 통합 테스트로 덮으려 하면 파이프라인이 수십 분으로 늘어난다. 자주 바뀌고 실패 시 영향이 큰 핵심 모듈 몇 개만 Terratest로 검증하고, 나머지는 tflint·checkov 같은 정적 검사에 맡기는 편이 현실적이다. 테스트의 목적은 완벽한 커버리지가 아니라, 배포 전에 치명적 회귀를 걸러내는 안전망을 만드는 것이다.