Skip to content
ioob.dev
Go back

Terraform 14편 — 테스트와 정책

· 8분 읽기
Terraform 시리즈 (14/15)
  1. Terraform 1편 — Terraform이란
  2. Terraform 2편 — 설치와 첫 배포
  3. Terraform 3편 — HCL 문법
  4. Terraform 4편 — 변수와 출력
  5. Terraform 5편 — 프로바이더
  6. Terraform 6편 — 리소스와 의존성
  7. Terraform 7편 — 데이터 소스와 Import
  8. Terraform 8편 — State 관리
  9. Terraform 9편 — 모듈
  10. Terraform 10편 — 반복과 조건
  11. Terraform 11편 — 워크스페이스와 환경 분리
  12. Terraform 12편 — Kubernetes와 Helm 프로바이더
  13. Terraform 13편 — CI/CD 통합
  14. Terraform 14편 — 테스트와 정책
  15. Terraform 15편 — 실전 패턴과 함정
Table of contents

Table of contents

왜 인프라에도 테스트가 필요한가

애플리케이션 코드에는 당연히 테스트를 쓴다. 그런데 인프라 코드에는 왜 잘 안 쓸까. “배포해보면 알지”라는 감각이 남아 있어서다. 하지만 Terraform 코드도 버그가 있고, 보안 허점이 있고, 정책 위반이 있다.

이런 문제들이 apply되기 전에 잡혀야 한다. 한 번 배포된 뒤 발견하면 이미 늦다.

flowchart LR
    Code["HCL 코드"] --> L1["단계 1:\nfmt / validate"]
    L1 --> L2["단계 2:\n정적 보안 분석\n(Checkov, tfsec)"]
    L2 --> L3["단계 3:\n정책 검증\n(OPA, Sentinel)"]
    L3 --> L4["단계 4:\n통합 테스트\n(Terratest)"]
    L4 --> Apply["apply 단계"]

비용과 속도가 다른 단계들이다. 왼쪽으로 갈수록 빠르고 싸지만 잡을 수 있는 범위가 좁고, 오른쪽으로 갈수록 느리고 비싸지만 실제 동작을 검증한다.

가장 먼저 — fmt와 validate

Terraform이 기본으로 제공하는 가장 빠른 검사다. 로컬에서도, CI에서도 항상 실행하는 게 좋다.

terraform fmt — 코드 스타일 정렬

# 전체 파일 자동 정렬
terraform fmt -recursive

# CI에서 "정렬 안 된 파일이 있는지만" 확인
terraform fmt -check -recursive

팀 전체가 같은 스타일을 쓰게 만드는 도구다. 들여쓰기, 공백, 속성 정렬을 자동으로 맞춰준다. -check는 정렬이 안 된 파일이 있으면 0이 아닌 exit code를 반환해서 CI를 실패시킨다.

terraform validate — 문법 검증

terraform validate

HCL 문법이 맞는지, 참조하는 변수나 리소스가 존재하는지, 타입이 맞는지 등을 검사한다. terraform init 이후에 실행해야 프로바이더 스키마를 알고 검증할 수 있다.

Success! The configuration is valid.
Error: Reference to undeclared input variable

  on main.tf line 5, in resource "aws_instance" "app":
   5:   instance_type = var.size

이 정도는 다 잡아주진 않는다. 오타나 논리 오류는 놓친다. 하지만 가장 빠르고 쉬운 첫 관문이다.

정적 보안 분석 — Checkov와 tfsec

HCL을 파싱해서 “이 코드가 보안 규칙을 위반하지 않는지” 검사하는 도구들이다. apply하지 않고도, state 없이도 검사할 수 있어서 개발 중 계속 돌리기 좋다.

Checkov

Bridgecrew(Palo Alto)에서 만든 도구다. Terraform뿐 아니라 CloudFormation, Kubernetes, Dockerfile까지 다양한 포맷을 지원한다.

# 설치
pip install checkov

# 실행
checkov -d .
checkov -d envs/prod --framework terraform

결과는 이런 식으로 나온다.

Check: CKV_AWS_24: "Ensure no security groups allow ingress from 0.0.0.0:0 to port 22"
	FAILED for resource: aws_security_group.web
	File: /envs/prod/main.tf:45-60

		45 | resource "aws_security_group" "web" {
		...
		55 |   ingress {
		56 |     from_port   = 22
		57 |     to_port     = 22
		58 |     protocol    = "tcp"
		59 |     cidr_blocks = ["0.0.0.0/0"]
		60 |   }

		Guide: https://docs.bridgecrew.io/docs/networking_1

어떤 규칙을 위반했는지, 어느 파일 몇 번째 줄인지, 가이드 링크까지 알려준다.

특정 규칙을 무시하고 싶을 때는 파일에 주석을 남긴다.

# checkov:skip=CKV_AWS_24:내부 네트워크라 허용
resource "aws_security_group" "internal" {
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/8"]
  }
}

이유를 반드시 주석으로 남기는 게 원칙이다. skip 남발은 “아무것도 검사하지 않는 것”과 같다.

tfsec

Aqua Security의 도구다. Terraform 전용이라 좀 더 가볍다.

# 설치 (macOS)
brew install tfsec

# 실행
tfsec .
Result #1 CRITICAL Security group rule allows ingress from public internet. 
──────────────────────────────────────────
  envs/prod/main.tf:55
──────────────────────────────────────────
   53    ingress {
   54      from_port   = 22
   55  →   cidr_blocks = ["0.0.0.0/0"]
   56      protocol    = "tcp"
   57      to_port     = 22
──────────────────────────────────────────
  ID         AVD-AWS-0107
  Impact     Your port is exposed to the whole internet
  Resolution Set a more restrictive cidr range

Checkov와 tfsec은 상당 부분 중복된다. 둘 다 쓰는 팀도 있고, 한쪽만 쓰는 팀도 있다. 시작은 하나로 가볍게 해보고, 필요하면 추가하는 방식이 낫다. 개인적으로는 tfsec이 Terraform 특화된 만큼 출력이 더 깔끔하게 느껴진다.

정책 검증 — OPA

Checkov와 tfsec은 미리 정의된 규칙을 검사한다. 조직 고유의 정책(예: “모든 리소스에 Owner 태그가 있어야 한다”)은 자체 정책으로 표현해야 한다. 이럴 때 OPA(Open Policy Agent)를 쓴다.

OPA는 Rego라는 언어로 정책을 쓰는 범용 정책 엔진이다. Kubernetes, Envoy, Terraform 등 어디에나 쓸 수 있다. Terraform에는 conftest라는 래퍼를 통해 주로 쓴다.

먼저 terraform plan의 JSON을 얻는다.

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

정책을 Rego로 쓴다.

# policies/tags.rego
package main

required_tags := {"Owner", "Environment", "Service"}

deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_instance"

    tag := required_tags[_]
    not resource.change.after.tags[tag]

    msg := sprintf("인스턴스 %v에 필수 태그 %v가 누락됨", [resource.address, tag])
}

이 정책은 “모든 EC2 인스턴스에 Owner, Environment, Service 태그가 있어야 한다”는 의미다. deny에 해당하면 정책 위반이다.

# 검사
conftest test --policy policies/ plan.json
FAIL - plan.json - 인스턴스 aws_instance.app에 필수 태그 Environment가 누락됨

좀 더 복잡한 예시도 있다. “프로덕션 환경에서는 특정 인스턴스 타입만 허용”이라든가.

package main

allowed_prod_types := {"m5.large", "m5.xlarge", "r5.large"}

deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_instance"
    resource.change.after.tags.Environment == "prod"

    instance_type := resource.change.after.instance_type
    not allowed_prod_types[instance_type]

    msg := sprintf(
        "프로덕션에서 인스턴스 타입 %v는 허용되지 않음. 허용: %v",
        [instance_type, allowed_prod_types]
    )
}

조직 정책을 코드로 명문화하면, 누구든 위반 사항을 사전에 발견할 수 있다. “프로덕션 규칙 뭐였지?” 같은 질문이 없어진다.

통합 테스트 — Terratest

가장 무거운 테스트다. 실제 클라우드 리소스를 만들어보고, 만든 게 의도대로 동작하는지 검증한다. Go로 작성한다.

// test/vpc_test.go
package test

import (
    "fmt"
    "testing"

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

func TestVpcModule(t *testing.T) {
    t.Parallel()

    terraformOptions := &terraform.Options{
        TerraformDir: "../modules/vpc",

        Vars: map[string]interface{}{
            "cidr_block":  "10.99.0.0/16",
            "environment": fmt.Sprintf("test-%d", time.Now().Unix()),
        },
    }

    // 테스트 종료 시 반드시 정리
    defer terraform.Destroy(t, terraformOptions)

    // apply 실행
    terraform.InitAndApply(t, terraformOptions)

    // 출력값 검증
    vpcId := terraform.Output(t, terraformOptions, "vpc_id")
    assert.Regexp(t, "^vpc-", vpcId)

    subnets := terraform.OutputList(t, terraformOptions, "public_subnet_ids")
    assert.Equal(t, 2, len(subnets))
}

이 테스트는 실제로 VPC와 서브넷을 만들고, 출력값이 예상대로 나오는지 확인하고, 마지막에 깨끗이 삭제한다.

cd test
go test -v -timeout 30m

장단점이 극단적이다.

장점

단점

그래서 Terratest는 자주 쓰는 핵심 모듈에만 적용한다. 공식 오픈소스 모듈이 Terratest를 얼마나 갖추고 있는지 보면 기준이 잡힌다. 전체 코드에 적용하는 건 비용 대비 효과가 낮다.

terraform test — 내장 테스트 프레임워크

Terraform 1.6부터는 자체 테스트 프레임워크가 생겼다. Go 없이 HCL만으로 테스트를 쓸 수 있다.

# tests/vpc.tftest.hcl
run "valid_cidr" {
  command = plan

  variables {
    cidr_block  = "10.99.0.0/16"
    environment = "test"
  }

  assert {
    condition     = aws_vpc.this.cidr_block == "10.99.0.0/16"
    error_message = "VPC CIDR이 입력값과 일치해야 함"
  }
}

run "creates_two_subnets_by_default" {
  command = plan

  variables {
    cidr_block  = "10.99.0.0/16"
    environment = "test"
  }

  assert {
    condition     = length(aws_subnet.public) == 2
    error_message = "기본 AZ 2개로 서브넷 2개가 생성되어야 함"
  }
}

실행은 이렇게 한다.

terraform test

command = plan이면 apply 없이 plan만 실행해서 검증한다. 실제 리소스를 만들지 않으니 빠르고 공짜다. command = apply로 바꾸면 실제로 생성해서 테스트한다(비용 발생).

이 내장 프레임워크는 간단한 검증에 이상적이다. “입력에 따라 출력이 의도대로 바뀌는가”, “조건부 로직이 맞게 동작하는가” 같은 것들. 여전히 복잡한 통합 테스트는 Terratest가 유리하지만, 일상적인 모듈 테스트는 내장 프레임워크로도 충분하다.

pre-commit 훅 — 로컬에서 먼저 잡기

CI에서 테스트하기 전에, 로컬에서 커밋 시점에 기본 검사를 돌리면 훨씬 빠른 피드백을 받을 수 있다. pre-commit 도구를 쓴다.

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/antonbabenko/pre-commit-terraform
    rev: v1.88.4
    hooks:
      - id: terraform_fmt
      - id: terraform_validate
      - id: terraform_tflint
        args:
          - --args=--only=terraform_required_version
          - --args=--only=terraform_required_providers
      - id: terraform_tfsec
      - id: terraform_docs
        args:
          - --args=--output-file README.md

설치와 활성화.

# 설치 (macOS)
brew install pre-commit

# 훅 활성화
pre-commit install

# 수동 실행 (모든 파일)
pre-commit run --all-files

이제 git commit을 칠 때마다 fmt, validate, tflint, tfsec이 자동으로 돈다. 문제가 있으면 커밋이 실패한다. CI까지 가기 전에 개발자 노트북에서 문제를 잡는다.

flowchart LR
    Edit["코드 수정"] --> Commit["git commit"]
    Commit --> Hook["pre-commit 훅"]
    Hook --> Check{"검사 통과?"}
    Check -->|"실패"| Fix["수정 후 다시 커밋"]
    Check -->|"성공"| Push["git push"]
    Push --> CI["CI 파이프라인\n(더 무거운 테스트)"]

tflint — 린터

tflint는 Terraform 전용 린터다. 공식 검사보다 훨씬 다양한 규칙을 제공한다.

brew install tflint
tflint --init
tflint

.tflint.hcl 설정으로 AWS 프로바이더용 룰셋을 켤 수 있다.

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

rule "terraform_unused_declarations" {
  enabled = true
}

rule "terraform_deprecated_interpolation" {
  enabled = true
}

tflint는 잘못된 인스턴스 타입, 없는 AMI ID, 미사용 변수 같은 것까지 잡아준다.

전체 파이프라인 구성

지금까지 다룬 도구들을 CI에 통합한 예시다.

# .github/workflows/terraform-ci.yml
name: Terraform CI

on:
  pull_request:
    paths: ['**/*.tf', '**/*.tftest.hcl']

jobs:
  checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.8.0

      - name: Format
        run: terraform fmt -check -recursive

      - name: Init (모든 envs/*)
        run: |
          for dir in envs/*; do
            (cd "$dir" && terraform init -backend=false)
          done

      - name: Validate
        run: |
          for dir in envs/*; do
            (cd "$dir" && terraform validate)
          done

      - name: tflint
        uses: terraform-linters/setup-tflint@v4
      - run: |
          tflint --init
          tflint --recursive

      - name: tfsec
        uses: aquasecurity/tfsec-action@v1.0.3

      - name: Checkov
        uses: bridgecrewio/checkov-action@master
        with:
          directory: .
          framework: terraform
          soft_fail: false

      - name: OPA Policy Check
        run: |
          for dir in envs/*; do
            (cd "$dir" && terraform plan -out=tfplan && terraform show -json tfplan > plan.json)
            conftest test --policy policies/ "$dir/plan.json"
          done

      - name: terraform test
        run: |
          for mod in modules/*; do
            if [ -d "$mod/tests" ]; then
              (cd "$mod" && terraform test)
            fi
          done

단계별로 비용이 다르다. fmt와 validate가 제일 빠르고(초 단위), 정적 분석이 다음(수십 초), OPA가 다음(분 단위, plan 포함), 실제 terratest가 가장 무거움(수십 분). 파이프라인 순서를 이렇게 배치해야 빠르게 실패하면서 전체 시간을 아낀다.

도구 선택 가이드

지금까지 나온 도구가 많다. 무엇을 써야 할까.

목적필수권장선택
문법/스타일terraform fmt, validatetflint
보안 정적 분석tfsec 또는 Checkov둘 다
조직 정책OPA/conftestSentinel (Terraform Cloud)
단위 테스트terraform test
통합 테스트Terratest (핵심 모듈만)
로컬 통합pre-commit

모든 걸 한 번에 도입할 필요는 없다. 단계적으로 쌓아간다.

  1. 먼저 fmt, validate, pre-commit 설정
  2. tfsec 또는 Checkov 하나 선택해 CI에 추가
  3. 자주 바뀌는 핵심 모듈에 terraform test 추가
  4. 조직 정책이 명확해지면 OPA 도입
  5. 진짜 핵심 모듈에만 Terratest

인프라에도 품질 기준이 있다. “돌아가면 됐지”는 한 번은 통할지 몰라도, 규모가 커지면 버티지 못한다. 자동 테스트와 정책 검증이 인프라 코드에 대한 신뢰의 기반이다.

다음 편에서는 시리즈의 마무리로, 실전 패턴과 함정들을 정리한다. 디렉토리 구조, 태깅 전략, 흔한 사고와 대규모 마이그레이션까지 한 번에 훑는다.

15편: 실전 패턴과 함정


Related Posts

Share this post on:

Comments

Loading comments...


Previous Post
Terraform 13편 — CI/CD 통합
Next Post
Terraform 15편 — 실전 패턴과 함정