테라폼 운영 강좌 #3 코드 품질과 테스트: tflint, pre-commit, terraform test
#1의 PR 워크플로에서 사람 리뷰어가 보는 것은 코드와 plan이었습니다. 그런데 리뷰어의 시간은 팀에서 가장 비싼 자원이라, 기계가 잡을 수 있는 문제로 그 시간을 쓰는 것은 낭비입니다. 이번 편은 사람 리뷰 앞에 자동 검사를 층층이 쌓는 이야기입니다. 이미 아는 fmt와 validate에서 시작해, tflint를 거쳐, 모듈의 동작을 검증하는 terraform test까지 올라갑니다.
검사 계층: 각 도구가 잡는 것 #
네 층의 검사는 잡는 것이 다릅니다.
| 도구 | 잡는 것 | 예 |
|---|---|---|
| terraform fmt | 형식 | 들여쓰기, 정렬 |
| terraform validate | 문법·참조 | 오타 난 인수, 없는 변수 |
| tflint | provider 지식 기반 오류·컨벤션 | 존재하지 않는 인스턴스 타입 |
| terraform test | 모듈의 동작 | “이 입력이면 이 결과가 나와야 한다” |
validate와 tflint의 차이가 핵심입니다. instance_type = "t3.mcro"(오타)는 validate를 통과합니다. HCL로서는 멀쩡한 문자열이기 때문입니다. 이 오타는 apply가 AWS 에러를 받고서야 드러나는데, tflint는 AWS provider 룰셋으로 이런 값 수준의 오류를 plan 전에 잡습니다. 선언되고 안 쓰인 변수, 네이밍 컨벤션 위반 같은 정리 항목도 걸러 줍니다.
# .tflint.hcl
plugin "aws" {
enabled = true
version = "0.44.0"
source = "github.com/terraform-linters/tflint-ruleset-aws"
}pre-commit: 커밋 시점 자동화 #
이 검사들을 CI에서만 돌리면 피드백이 느립니다(푸시하고 몇 분 기다려 실패 확인). pre-commit 훅으로 커밋 시점에 로컬에서 돌리면 몇 초 만에 잡힙니다.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.99.0
hooks:
- id: terraform_fmt
- id: terraform_validate
- id: terraform_tflintpre-commit-terraform은 이 조합을 묶어 둔 사실상의 표준 저장소입니다. CI의 검사는 그대로 두어야 한다는 점만 주의합니다. 훅은 개인 환경 설정이라 안 깐 사람도 있을 수 있으므로, 로컬 훅은 빠른 피드백용이고 CI가 최종 관문입니다.
terraform test: 모듈의 동작을 검증합니다 #
fmt부터 tflint까지는 코드의 겉을 봅니다. “이 모듈에 이 입력을 주면 의도한 결과가 나오는가"라는 동작의 검증은 테라폼 1.6부터 내장된 terraform test의 몫입니다. 실전 #9에서 모듈이 팀의 공유 자산이 됐으니, 모듈을 고칠 때 기존 사용처가 깨지지 않는다는 보장이 필요해졌고, 그것이 테스트가 필요해지는 정확한 시점입니다.
테스트는 tests/ 디렉터리의 .tftest.hcl 파일로 작성합니다. 기초 #8의 static-bucket 모듈을 검증해 보겠습니다.
# modules/static-bucket/tests/naming.tftest.hcl
run "bucket_name_is_applied" {
command = plan
variables {
bucket_name = "test-bucket-name"
}
assert {
condition = aws_s3_bucket.this.bucket == "test-bucket-name"
error_message = "bucket 인수에 입력한 이름이 그대로 반영되어야 합니다."
}
}
run "versioning_is_enabled" {
command = plan
variables {
bucket_name = "test-bucket-name"
}
assert {
condition = aws_s3_bucket_versioning.this.versioning_configuration[0].status == "Enabled"
error_message = "이 모듈의 버킷은 항상 버저닝이 켜져야 합니다."
}
}$ terraform -chdir=modules/static-bucket test
tests/naming.tftest.hcl... pass
run "bucket_name_is_applied"... pass
run "versioning_is_enabled"... pass구조는 단순합니다. run 블록 하나가 테스트 케이스 하나이고, variables로 입력을 주고, assert의 condition이 참이어야 통과합니다. condition에 쓰는 문법은 기초 #4에서 배운 표현식 그대로입니다.
plan 테스트와 apply 테스트의 비용 #
command = plan이 기본값이 아니라 의도적 선택이라는 점이 중요합니다. run 블록의 기본 command는 apply로, 실제 리소스를 만들었다가 테스트 후 지웁니다. 실제 AWS 동작까지 검증되는 대신 느리고 요금이 들고 정리 실패 위험이 있습니다. 실용적인 기준은 이렇습니다.
- 기본은 plan 테스트: 입력과 계획 결과의 관계 검증은 plan으로 충분하고, 빠르고 무료라 pre-commit·CI에 부담 없이 들어갑니다.
- apply 테스트는 핵심 모듈에 선별적으로: “정말 만들어지는가"까지 봐야 하는 공유 모듈 정도에만 쓰고, CI에서는 별도 워크플로(야간 등)로 분리해 PR 피드백을 느리게 만들지 않습니다.
이 검사 전부(#1의 plan 포함)가 PR에서 자동으로 돌면, 사람 리뷰어에게 도달하는 PR은 이미 형식과 문법과 값과 동작이 검증된 상태입니다. 리뷰어는 비로소 사람만 할 수 있는 질문(이 설계가 맞는가, 이 교체는 의도인가)에 시간을 쓰게 됩니다.
정리 #
이번 글에서 다룬 내용입니다.
- 검사는 fmt(형식), validate(문법), tflint(provider 지식), terraform test(동작)의 네 층입니다
- validate는 존재하지 않는 인스턴스 타입을 잡지 못합니다. 값 수준의 오류는 tflint의 AWS 룰셋이 plan 전에 잡습니다
- pre-commit 훅은 빠른 로컬 피드백용이고, 최종 관문은 CI입니다
- terraform test는 run·variables·assert 구조의 tftest 파일로 모듈 동작을 검증합니다. 모듈이 공유 자산이 되는 순간이 테스트 도입 시점입니다
- run의 기본 command는 apply(실제 생성·비용 발생)이므로, 기본은 plan 테스트로 하고 apply 테스트는 핵심 모듈에만 선별 적용합니다
다음 글(#4 보안·비용 스캔)에서는 또 다른 자동 검사 두 축을 답니다. 보안 설정 실수를 잡는 trivy·checkov, 그리고 PR에 비용 변화를 표시하는 Infracost입니다.