본문 바로가기

IaC

테라폼 스터디 8주차 - 테라폼 코드 테스트

1-1. 수동 테스트 - 기본 수동 테스트

 

자동화된 두고 없이 사람이 직접 수행하는 테스트 방식

 

 

테라폼 코드 실행 및 검증

terraform init
terraform apply

 

해당 명령을 실행 후, 생성된 리소스를 직접 클라우드 프로바이더 (AWS나 GCP)의 콘솔에서 확인한다. 혹은 각 프로바이더가 제공하는 cli를 이용하여 리소스 상태를 점검한다.

 

혹은 아래의 테라폼 명령어를 통해 확인한다.

terraform state list

 

 

 

1-2. 테스트 후 정리

테스트 후 테스트를 위해 생성한 리소스를 정리한다.

 

terraform destroy 명령어의 -auto-approve 옵션은 보통 자동화 파이프라인(CI, 스크립트)에서 유용하게 사용된다.

# 사용자 승인 없이 즉시 제거
terraform destroy -auto-approve

# 사용자 승인 후 제거
terraform destroy

 

리소스 정리 후, 콘솔이나 cli를 이용하여 리소스 상태를 점검한다.

 

 


 

2-1. 자동화된 테스트 - 단위 테스트

-> 테라폼에서의 단위 테스트란 특정 모듈이나 리소스가 올바르게 정의되었는지 검증하는 테스트이다.

 

 

단위 테스트의 기본 사항

 

1. 테라폼 모듈의 유효성 검사

terraform validate 명령어를 실행하여 구문 오류나 기본적인 설정 오류를 확인한다.

terraform validate

 


 

 

2. 테라폼 플랜 테스트

# tfplan이라는 바이너리 파일로 테라폼 실행 계획을 저장
terraform plan -out=tfplan

 

 

tfplan 파일로 저장한 실행 계획을 이용하여 terraform apply를 실행할 수 있다.

해당 tfplan 파일로 검증을 진행한 후에 apply를 진행할 수도 있다.

terraform apply tfplan

 

 

 

단, tfplan 파일로 저장 후에 terraform state가 변경되는 순간, 해당 tfplan 파일을 더 이상 사용할 수 없다.

 

 

terraform show 명령어를 사용하면, 바이너리 파일인 tfplan 파일의 내용을 사람이 읽을 수 있는 형식으로 변환할 수 있다.

terraform show tfplan

 

출력 예시

  # aws_instance.example will be created
  + resource "aws_instance" "example" {
      + ami           = "ami-123456"
      + instance_type = "t2.micro"
      + tags = {
          + Name = "ExampleInstance"
        }
    }

 

# JSON FORMAT
terraform show -json tfplan

 

출력 예시

{
  "format_version": "1.0",
  "resource_changes": [
    {
      "change": {
        "actions": ["create"],
        "before": null,
        "after": {
          "ami": "ami-123456",
          "instance_type": "t2.micro"
        }
      }
    }
  ]
}

 

 


 

 

3. 모듈별 테스트 도구 사용

 

예시 - Terratest 이용한 테라폼 코드 출력 검증

 

EC2 인스턴스 ID 생성 여부 확인

package test

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

func TestTerraformModule(t *testing.T) {
	terraformOptions := &terraform.Options{
		TerraformDir: "../examples/",
	}
	// defer : 앞에 선언되었을지라도, 함수가 종료될 때 실행됨
	defer terraform.Destroy(t, terraformOptions)
	terraform.InitAndApply(t, terraformOptions)

	instanceID := terraform.Output(t, terraformOptions, "instance_id")
	assert.NotEmpty(t, instanceID)
}

 

init & apply -> output을 instanceID로 저장 -> instanceID가 빈값이 아닌지를 검증 -> 테스트 종료 시, destroy 진행

 


 

4. 종속성 주입

 

테라폼에선 특정 리소스가 다른 리소스에 의존하는 경우가 많기에 종속성을 주입해야 하는 경우가 있다.

 

 

4-1. depends_on

-> 명시적으로 종속성 설정

resource "aws_instance" "web" {
  ami           = "ami-123456"
  instance_type = "t2.micro"

  depends_on = [aws_security_group.web_sg]
}

 

 

4-2. 변수로 종속성 주입

 

 

리소스 - 변수를 결합하여 종속성 주입이 가능하다.

variable "instance_type" {
  default = "t2.micro"
}
resource "aws_instance" "web" {
  ami           = "ami-123456"
  instance_type = var.instance_type
}

 

 

 

terraform plan / terraform apply에서 -var 옵션을 이용한 변수 지정을 통해, 다양한 환경에서의 테스트가 가능하다.

terraform plan -var="instance_type=t3.micro"
terraform apply -var="instance_type=t3.micro"

 

 


 

5. 병렬로 테스트 실행

 

 

 

5-1. 테라폼의 -parallelism 옵션

 

-parallelism=N 옵션을 이용하여 최대 N개의 리소스를 동시에 생성 혹은 삭제할 수 있다.

resource "aws_instance" "example" {
  count = 5  # 5개의 인스턴스 생성
  ami           = "ami-123456"
  instance_type = "t3.micro"
}

 

terraform apply -parallelism=5

 

 

 

5-2. Terratest 이용한 병렬 실행

 

instanceType를 동적으로 할당하면서, 병렬적으로 테스트를 실행하는 예시이다.

package test

import (
	"testing"
	"time"

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

func TestTerraformParallel(t *testing.T) {
	t.Parallel() // 병렬 실행 활성화
	
    // 2개의 테스트 데이터 정의 "Test1", "Test2"
	tests := []struct {
		name          string
		instanceType  string
	}{
		{"Test1", "t3.micro"},
		{"Test2", "t3.medium"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel() // 개별 테스트도 병렬 실행

			options := &terraform.Options{
				TerraformDir: "../terraform", // Terraform 코드 경로
				Vars: map[string]interface{}{
					"instance_type": tt.instanceType,
				},
			}

			// Terraform 실행
			terraform.InitAndApply(t, options)

			// 테스트 후 리소스 정리, 함수가 종료될 때 자동으로 실행
			defer terraform.Destroy(t, options)
		})
	}
    // 테스트가 너무 빠르게 종료되지 않도록 잠시 대기 (병렬 실행을 확인하기 위함)
    time.Sleep(10 * time.Second)
}

 


terraform test

 

terraform v1.7.0부터 terraform test 실행중에 provider가 모의 데이터를 반환하는 기능이 추가되었다.

 

 

기본 문법

 

  • .tftest.hcl 또는 .tftest.json 확장자
  • 각 테스트 파일에는 다음과 같은 루트 수준 속성과 블록이 포함되어있다.
    • 1개 이상의 run블록
    • 0~1개의 variables 블록
    • 0~여러개의 provider 블록
  • variables 블록의 순서는 중요하지 않지만, 기본적으로 테스트 파일의 시작 부분에서 variables와 provider를 정의하는 것이 추천된다.

 

AWS S3 버킷 생성 예제와 테스트

 

 

# main.tf

provider "aws" {
    region = "eu-central-1"
}

variable "bucket_prefix" {
  type = string
}

resource "aws_s3_bucket" "bucket" {
  bucket = "${var.bucket_prefix}-bucket"
}

output "bucket_name" {
  value = aws_s3_bucket.bucket.bucket
}

 

# valid_string_concat.tftest.hcl

variables {
  bucket_prefix = "test"
}

run "valid_string_concat" {

  command = plan

  assert {
    condition     = aws_s3_bucket.bucket.bucket == "test-bucket"
    error_message = "S3 bucket name did not match expected"
  }

}

 

 

run 블록의 필드 혹은 블록 옵션

command An optional attribute, which is either apply or plan. apply
plan_options.mode An optional attribute, which is either normal or refresh-only. normal
plan_options.refresh An optional boolean attribute. true
plan_options.replace An optional attribute containing a list of resource addresses referencing resources within the configuration under test.  
plan_options.target An optional attribute containing a list of resource addresses referencing resources within the configuration under test.  
variables An optional variables block.  
module An optional module block.  
providers An optional providers attribute.  
assert Optional assert blocks.  
expect_failures An optional attribute.

 

 

variables

 

기본적으로 variables는 테스트 파일 내에서의 루트 레벨과 run  블록 내에서의 레벨을 모두 지원하며, 기본적으로 루트 레벨의 vairables를 run 블록내에서 사용 가능하며, run 블록 내에 이를 재정의할 수 있다.

 

 

변수 우선순위

 

기본적으로 변수 우선순위는 아래와 같다.

 

  • Environment variables
  • The terraform.tfvars file, if present.
  • The terraform.tfvars.json file, if present.
  • Any *.auto.tfvars or *.auto.tfvars.json files, processed in lexical order of their filenames.
  • Any -var and -var-file options on the command line, in the order they are provided. (This includes variables set by an HCP Terraform workspace.)

 

하지만, 테라폼 테스트 파일에서의 변수는 위의 변수들보다 더 높은 우선순위를 가지게 된다.

 

변수 참조 예시

 

variables {
  global_value = "some value"
}

run "run_block_one" {
  variables {
    local_value = var.global_value
  }

  # ...
  # Some test assertions should go here.
  # ...
}

run "run_block_two" {
  variables {
    local_value = run.run_block_one.output_one
  }

  # ...
  # Some test assertions should go here.
  # ...
}

 

이와 같이 블록 간에 값을 전달할 수 있다.

 


 

provider

 

기본적으로 테스트 파일에서 정의하지 않으면, 기본 구성의 provider를 사용하며, provider 또한 테스트 파일에서 재정의하여 사용할 수 있다.

# main.tf

terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
    }
  }
}

variable "bucket_prefix" {
  type = string
}

resource "aws_s3_bucket" "bucket" {
  bucket = "${var.bucket_prefix}-bucket"
}

output "bucket_name" {
  value = aws_s3_bucket.bucket.bucket
}

 

 

aws provider를 아래와 같이 재정의할 수 있다.

# customised_provider.tftest.hcl

provider "aws" {
    region = "eu-central-1"
}

variables {
  bucket_prefix = "test"
}

run "valid_string_concat" {

  command = plan

  assert {
    condition     = aws_s3_bucket.bucket.bucket == "test-bucket"
    error_message = "S3 bucket name did not match expected"
  }

}

 

 

예시2. 복잡한 provider 구성 / alias와 테스트 내에서 여러 공급자 사용

 

# main.tf

terraform {
  required_providers {
    aws = {
      source                = "hashicorp/aws"
      configuration_aliases = [aws.secondary]
    }
  }
}

variable "bucket_prefix" {
  default = "test"
  type    = string
}

resource "aws_s3_bucket" "primary_bucket" {
  bucket = "${var.bucket_prefix}-primary"
}

resource "aws_s3_bucket" "secondary_bucket" {
  provider = aws.secondary
  bucket   = "${var.bucket_prefix}-secondary"
}

 

 

# customised_providers.tftest.hcl

provider "aws" {
  region = "us-east-1"
}

provider "aws" {
  alias  = "secondary"
  region = "eu-central-1"
}

run "providers" {

  command = plan

  assert {
    condition     = aws_s3_bucket.primary_bucket.bucket == "test-primary"
    error_message = "invalid value for primary S3 bucket"
  }

  assert {
    condition     = aws_s3_bucket.secondary_bucket.bucket == "test-secondary"
    error_message = "invalid value for secondary S3 bucket"
  }
}

 

 

module

  • terraform test 명령어를 통해 현재 디렉토리의 설정을 테스트 한다.
  • run 블록 안에 module 블록을 추가하면 특정 모듈 대상으로 테스트가 가능하다.
  • 테스트에서의 module 블록에서는 sourceversion만 사용 가능
  • source는 local 또는 terraform registry만 가능하다.

 

모듈과 테스트 예제

 

1. 생성된 S3 버킷에 여러 파일을 생성하고 로드하는 모듈

# main.tf

variable "bucket" {
  type = string
}

variable "files" {
  type = map(string)
}

data "aws_s3_bucket" "bucket" {
  bucket = var.bucket
}

resource "aws_s3_object" "object" {
  for_each = var.files

  bucket = data.aws_s3_bucket.bucket.id
  key = each.key
  source = each.value

  etag = filemd5(each.value)
}

 

 

2. 테스트 대상 구성에서 사용할 수 있도록 S3버킷을 생성하는 모듈

# testing/setup/main.tf

variable "bucket" {
  type = string
}

resource "aws_s3_bucket" "bucket" {
  bucket = var.bucket
}

 

 

3. S3 버킷에 있는 파일을 로드하는 모듈

# testing/loader/main.tf

variable "bucket" {
  type = string
}

data "aws_s3_objects" "objects" {
  bucket = var.bucket
}

 

 

만들어진 모듈을 테스트하는 예제

# file_count.tftest.hcl

variables {
  bucket = "my_test_bucket"
  files = {
    "file-one.txt": "data/files/file_one.txt"
    "file-two.txt": "data/files/file_two.txt"
  }
}

provider "aws" {
  region = "us-east-1"
}

run "setup" {
  # Create the S3 bucket we will use later.

  module {
    source = "./testing/setup"
  }
}

run "execute" {
  # This is empty, we just run the configuration under test using all the default settings.
}

run "verify" {
  # Load and count the objects created in the "execute" run block.

  module {
    source = "./testing/loader"
  }

  assert {
    condition = length(data.aws_s3_objects.objects.keys) == 2
    error_message = "created the wrong number of s3 objects"
  }
}

 


 

 

모듈과 상태 파일

 

  • 메인 구성 상태 파일 : module이 없는 run 블록끼리 공유하는 상태 파일
  • 모듈 전용 상태 파일 : module이 지정된 경우, 모듈마다 따로 관리(단, 같은 모듈은 공유)

 

 

모듈 상태 파일 예제

 

생성, 관리되는 상태 파일 목록

  • module 블록이 없는 테스트 기본 구성 상태 파일
  • setup 관련 상태 파일
  • loader 관련 상태 파일
run "setup" {

  # This run block references an alternate module and is the first run block
  # to reference this particular alternate module. Therefore, Terraform creates
  # and populates a new empty state file for this run block.

  module {
    source = "./testing/setup"
  }
}

run "init" {

  # This run block does not reference an alternate module, so it uses the main
  # state file for the configuration under test. As this is the first run block
  # to reference the main configuration, the previously empty state file now
  # contains the resources created by this run block.

  assert {
    # In practice we'd do some interesting checks and tests here but the
    # assertions aren't important for this example.
  }

  # ... more assertions ...
}

run "update_setup" {

  # We've now re-referenced the setup module, so the state file that was created
  # for the first "setup" run block will be reused. It will contain any
  # resources that were created as part of the other run block before this run
  # block executes and will be updated with any changes made by this run block
  # after.

  module {
    source = "./testing/setup"
  }

  variables {
    # In practice, we'd likely make some changes to the module compared to the
    # first run block here. Otherwise, there would be no point recalling the
    # module.
  }
}

run "update" {

  # As with the "init" run block, we are executing against the main configuration
  # again. This means we'd load the main state file that was initially populated
  # by the "init" run block, and any changes made by this "run" block will be
  # carried forward to any future run blocks that execute against the main
  # configuration.

  # ... updated variables ...

  # ... assertions ...
}

run "loader" {

  # This run block is now referencing our second alternate module so will create
  # our third and final state file. The other two state files are managing
  # resources from the main configuration and resources from the setup module.
  # We are getting a new state file for this run block as the loader module has
  # not previously been referenced by any run blocks.

  module {
    source = "./testing/loader"
  }
}

 

 


모듈 정리

terraform은 run 블록의 역순으로 리소스를 destroy한다.

 

바로 위의 예제에서 destroy 순서는 다음과 같다.

  1. ./testing/loader
  2. 주 상태 파일 (update 블록에서 참조되었기 때문)
  3. ./testing/setup

 


Expecting failures

 

기본적으로, 테스트는 모든 상황을 통과하는 것이 일반적이지만, 일부 테스트가 실패하는 것을 테스트하고 싶은 경우에 expect_failures를 사용한다.

 

  • 잘못된 입력 값이 에러를 발생시키는지
  • 권한이 없는 사용자가 리소스에 접근했을 때 차단되는지 등

 

 

따라서, excpet_failures에 지정된 리소스가 성공하게 된다면, 테스트는 실패로 간주된다.

 

 

예제

variables {
  input = 0
}

run "zero" {
  # input 값이 0 (짝수)이므로 validation이 성공해야 함.
  command = plan
}

run "one" {
  # input 값을 1 (홀수)로 설정했기 때문에 validation이 실패해야 정상.
  command = plan

  variables {
    input = 1
  }

  expect_failures = [
    var.input,
  ]
}

 

 

 

apply command와 expect_failues의 주의사항

 

기본적으로 expect_failures는 plan 명령어와 함께 사용하는 것이 권장된다.

apply command를 사용하려면 주의해야 할 부분이 있다.

run "invalid_apply_test" {
  command = apply

  variables {
    input = 1
  }

  expect_failures = [
    var.input
  ]
}

 

  • apply 실행하려 하지만, plan 단계에서 실패하는 경우
  • apply는 실행되지 못하며, 테스트가 실패 처리 됨
  • 실패를 기대(expect)했는데, 테스트가 실패했다는 것에 의문을 가질 수 있음

-> command = apply 는 plan이 성공해야 실행이 가능

 

 

 

command = apply와 expect_failures를 함께 사용할 수 있는 경우

-> plan 단계에서 검증하지 않고, 실제 리소스를 생성한 후에 검증하는 경우

 

run "post_apply_check" {
  command = apply

  expect_failures = [
    aws_instance.invalid_instance
  ]
}

 

EC2 인스턴스 생성 후에만 검증할 수 있는 조건이기에 apply와 함께 사용이 가능하다.

 

 

추가 주의사항

 

  • expect_failures는 사용자 정의 조건(custom conditions)에만 적용 가능
    • 타입 오류 같은 Terraform 자체 에러는 무조건 테스트 실패로 처리됨
    • 예: bool 타입 변수에 string을 넣으면 expect_failures로 무시할 수 없음
  • 의도한 실패만 허용됨
    • expect_failures에 명시하지 않은 실패는 테스트를 실패로 처리함
  • 복잡한 의존성은 depends_on으로 제어
    • 특정 검증이 반드시 수행되도록 하려면 depends_on을 사용해서 순서를 제어할 수 있음