IaC/terraform

Terraform 코드 테스트

Luuuuu 2025. 2. 2. 17:11

https://developer.hashicorp.com/terraform/tutorials/configuration-language/test 블로그를 기반으로 작성된 블로그입니다.

소개

Terraform 테스트를 사용하면 기존 상태 파일이나 리소스에 영향을 주지 않고도 모듈 구성을 검증할 수 있습니다. 테스트는 plan이나 apply 워크플로우와는 별개의 작업으로, 임시 인프라를 구축하고 이러한 단기 리소스의 인메모리 상태에 대해 테스트를 수행합니다. 이를 통해 인프라에 영향을 주지 않고도 모듈 변경사항을 안전하게 검증할 수 있습니다.

테스트와 검증의 차이

  • 검증(Validation): 변수 검증, 사전/사후 조건, 체크 블록 등은 배포된 인프라의 유효성을 확인합니다. 검증이 실패하면 모듈 사용자가 해결해야 합니다.
  • 테스트(Testing): 구성 자체의 동작과 로직을 검증합니다. 모듈 작성자가 구성의 동작을 확인하고 변경사항이 문제를 일으키지 않도록 보장하는 데 사용됩니다.

사전 준비 사항

테스트를 시작하기 위해서는 다음이 필요합니다:

  • Terraform v1.7 이상
  • 로컬에 구성된 AWS 계정 자격 증명
  • GitHub 계정
  • HCP Terraform 계정

프로젝트 구조 이해하기

일반적인 Terraform 테스트 프로젝트는 다음과 같은 구조를 가집니다:

.
├── LICENSE
├── README.md
├── main.tf
├── outputs.tf
├── terraform.tf
├── tests
│   ├── setup
│   │   ├── main.tf
│   └── website.tftest.hcl
├── variables.tf
└── www
    ├── error.html
    └── index.html

Terraform 테스트는 크게 두 부분으로 구성됩니다:

  1. .tftest.hcl 확장자를 가진 테스트 파일
  2. 테스트별 리소스와 데이터 소스를 생성하는 선택적 헬퍼 모듈

기본적으로 terraform test 명령은 루트 디렉토리와 tests 디렉토리에서 .tftest.hcl 파일을 찾습니다. -test-directory 플래그를 사용하여 다른 디렉토리를 지정할 수 있습니다.

헬퍼 모듈 작성하기

예를 들어, 다음과 같은 헬퍼 모듈을 만들 수 있습니다:

# tests/setup/main.tf
terraform {
  required_providers {
    random = {
      source = "hashicorp/random"
      version = "3.5.1"
    }
  }
}

resource "random_pet" "bucket_prefix" {
  length = 4
}

output "bucket_prefix" {
    value = random_pet.bucket_prefix.id
}

테스트 파일 작성하기

테스트 파일은 순차적으로 실행되는 run 블록들로 구성됩니다:

# tests/website.tftest.hcl
run "setup_tests" {
    module {
        source = "./tests/setup"
    }
}

run "create_bucket" {
  command = apply

  variables {
    bucket_name = "${run.setup_tests.bucket_prefix}-aws-s3-website-test"
  }

  assert {
    condition     = aws_s3_bucket.s3_bucket.bucket == "${run.setup_tests.bucket_prefix}-aws-s3-website-test"
    error_message = "Invalid bucket name"
  }

  assert {
    condition     = aws_s3_object.index.etag == filemd5("./www/index.html")
    error_message = "Invalid eTag for index.html"
  }

  assert {
    condition     = aws_s3_object.error.etag == filemd5("./www/error.html")
    error_message = "Invalid eTag for error.html"
  }
}

run 블록은 고유한 이름을 가져야 하며, 여러 개의 assert 블록을 포함할 수 있습니다. 모든 assert 블록의 조건이 true로 평가되어야 해당 run 블록이 통과합니다.

 

HTTP 테스트 추가하기

웹사이트의 동작을 테스트하기 위해 HTTP 헬퍼 모듈을 만들 수 있습니다:

# tests/final/main.tf
terraform {
  required_providers {
    http = {
      source = "hashicorp/http"
      version = "3.4.0"
    }
  }
}

variable "endpoint" {
    type = string
}

data "http" "index" {
    url = var.endpoint
    method = "GET"
}

그리고 이를 사용하는 테스트를 추가합니다:

run "website_is_running" {
  command = plan

  module {
    source = "./tests/final"
  }

  variables {
    endpoint = run.create_bucket.website_endpoint
  }

  assert {
    condition     = data.http.index.status_code == 200
    error_message = "Website responded with HTTP status ${data.http.index.status_code}"
  }
}

 

Mock 테스트란?

Terraform은 프로바이더, 리소스, 데이터 소스에 대한 Mock 테스트를 지원합니다. 이를 통해 실제로 임시 인프라를 생성하지 않고도 구성이 의존하는 모든 리소스와 속성을 시뮬레이션할 수 있습니다. Mock 테스트를 사용하면 Terraform이 리소스와 데이터 소스의 모든 계산된 필드에 대해 자동으로 값을 생성합니다.

Mock 테스트가 필요한 이유

  1. 테스트 시간 단축
  2. 비용 절감
  3. 복잡한 인프라 의존성 처리 단순화
  4. 테스트 안정성 향상

실제 예제로 알아보는 Mock 테스트

1. 백엔드 인프라 구성

먼저 테스트할 백엔드 인프라 구성을 main.tf에 정의합니다:

# Backend API
data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"] # Canonical
}

resource "aws_instance" "backend_api" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"

  tags = {
    Name = "backend"
  }
}

resource "aws_db_instance" "backend_api" {
  allocated_storage    = 10
  db_name             = "backend_api"
  engine              = "mysql"
  engine_version      = "5.7"
  instance_class      = "db.t3.micro"
  username            = "foo"
  password            = "foobarbaz"
  parameter_group_name = "default.mysql5.7"
  skip_final_snapshot = true
}

이 구성은 애플리케이션의 백엔드 API를 지원하기 위한 EC2 인스턴스와 RDS 데이터베이스를 생성합니다. 이러한 리소스들은 프로비저닝 시간이 길어 테스트 속도를 늦출 수 있습니다.

2. Mock 설정

website.tftest.hcl 파일에 다음과 같은 override_resource 블록을 추가합니다:

override_resource {
  target = aws_instance.backend_api
}

override_resource {
  target = aws_db_instance.backend_api
}

override_resource 블록은 Terraform에게 target 속성에 정의된 주소의 리소스를 Mock하도록 지시합니다. 이 경우 EC2 인스턴스와 RDS 데이터베이스를 AWS에서 실제로 프로비저닝하지 않고 Mock 처리합니다.

3. 테스트 케이스 작성

다음으로 테스트 케이스를 추가합니다:

run "check_backend_api" {
  assert {
    condition     = aws_instance.backend_api.tags.Name == "backend"
    error_message = "Invalid name tag"
  }

  assert {
    condition     = aws_db_instance.backend_api.username == "foo"
    error_message = "Invalid database username"
  }
}

4. 테스트 실행

테스트를 실행하면 다음과 같은 결과를 얻을 수 있습니다:

$ terraform test
tests/website.tftest.hcl... in progress
  run "setup_tests"... pass
  run "create_bucket"... pass
  run "website_is_running"... pass
  run "check_backend_api"... pass
tests/website.tftest.hcl... tearing down
tests/website.tftest.hcl... pass

Success! 4 passed, 0 failed.

Mock을 사용하지 않았다면, AWS가 RDS 데이터베이스와 EC2 인스턴스를 생성하기를 기다려야 하므로 테스트 완료까지 수 분이 걸렸을 것입니다. override_resource 블록을 사용하여 Mock 처리했기 때문에 Terraform이 테스트를 빠르게 완료할 수 있었습니다.

데이터 소스 Mock 처리

데이터 소스도 Mock 처리가 가능합니다. 예를 들어, 조직에 네트워킹 팀과 애플리케이션 배포 팀이 별도로 있는 경우를 생각해보겠습니다. 이런 시나리오에서 애플리케이션 팀은 tfe_outputs 데이터 소스를 사용하여 네트워킹 팀이 생성한 인프라를 참조할 수 있습니다.

테스트를 위해 override_data 블록을 사용하여 tfe_outputs 데이터 소스의 값을 Mock 처리할 수 있습니다:

override_data {
  target = data.tfe_outputs.network
  values = {
    vpc_id = "vpc-12345678"
    subnet_ids = ["subnet-1", "subnet-2"]
  }
}

Mock 테스트 모범 사례

  1. 선택적 Mock 처리: 모든 리소스를 Mock 처리할 필요는 없습니다. 테스트에 직접적으로 필요한 리소스만 실제로 생성하고 나머지는 Mock 처리하세요.
  2. 현실적인 값 사용: Mock 처리된 값이더라도 실제 환경에서 발생할 수 있는 값을 사용하세요.
  3. 예외 케이스 테스트: Mock을 통해 실제 환경에서는 테스트하기 어려운 예외 상황을 시뮬레이션할 수 있습니다.
  4. 문서화: 어떤 리소스가 Mock 처리되었는지 명확하게 문서화하여 다른 개발자들이 테스트 범위를 이해할 수 있게 하세요.

Terraform 테스트 모범 사례

  1. 모든 중요한 모듈에 대한 기본 테스트 작성
  2. CI/CD 파이프라인에 테스트 통합
  3. 시간이 오래 걸리는 리소스는 모의 테스트 사용
  4. 테스트 커버리지 점진적 확장
  5. 명확한 에러 메시지 작성