테스트

테스트 (Tests)

참고: 이 테스트 프레임워크는 Terraform v1.6.0 이상에서 사용할 수 있어요.

출처: 문서

본문

Terraform 테스트는 작성자가 모듈 구성 업데이트가 호환성을 깨는 변경을 도입하지 않는지 검증할 수 있게 해줘요. 테스트는 테스트 전용의 수명이 짧은 리소스를 대상으로 실행되므로, 기존 인프라나 상태에 어떤 위험도 주지 않아요.

통합 테스트 또는 단위 테스트

기본적으로 Terraform 안의 테스트는 실제 인프라를 만들고 그 인프라에 대해 단언(assertion)과 검증을 실행할 수 있어요. 이것은 작업을 실행하고 Terraform이 만드는 인프라를 검증함으로써 Terraform의 핵심 기능을 테스트하는 것이므로 통합 테스트와 유사해요.

run 블록 안의 command 속성을 업데이트하면(아래 예시) 기본 테스트 동작을 재정의할 수 있어요. 기본적으로 각 run 블록은 command = apply로 실행되어 Terraform이 구성에 대해 완전한 apply 작업을 실행하도록 지시해요. command 값을 command = plan으로 바꾸면 이 run 블록에 대해 새 인프라를 만들지 말라고 Terraform에 지시해요. 이렇게 하면 테스트 작성자가 인프라 안의 논리적 작업과 커스텀 조건을 단위 테스트와 유사한 과정으로 검증할 수 있어요.

Terraform v1.7.0은 terraform test 실행 중에 프로바이더가 반환하는 데이터를 모킹(mock)하는 기능을 도입했어요. 이를 사용해 더 상세하고 완전한 단위 테스트를 작성할 수 있어요.

문법

각 Terraform 테스트는 테스트 파일에 들어 있어요. Terraform은 파일 확장자(.tftest.hcl 또는 .tftest.json)에 따라 테스트 파일을 찾아요. 각 테스트 파일은 다음과 같은 루트 레벨 속성과 블록을 포함해요:

  • 0개 또는 1개의 test 블록.
  • 1개 이상의 run 블록.
  • 0개 또는 1개의 variables 블록.
  • 0개 이상의 provider 블록.

기본적으로 Terraform은 run 블록을 순차적으로 실행해요. run 블록을 병렬로 실행하려면 병렬 실행을 참고하세요. 각 run 블록은 구성 디렉터리 안에서 직접 실행되는 일련의 Terraform 명령을 시뮬레이션해요. variablesprovider 블록의 순서는 중요하지 않아요. Terraform은 테스트 작업 시작 시 이 블록들의 모든 값을 처리해요. variablesprovider 블록을 테스트 파일의 시작 부분, 맨 앞에 정의하는 것을 권장해요.

예시

다음 예시는 입력 변수를 사용해 이름을 수정하는 AWS S3 버킷을 만드는 간단한 Terraform 구성을 보여줘요. 우리는 버킷 이름이 기대한 대로 생성되는지 검증하는 예시 테스트 파일(아래)을 만들 거예요.

# 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
}

다음 테스트 파일은 S3 버킷을 만드는 단일 Terraform plan 명령을 실행한 다음, 실제 이름이 예상 이름과 일치하는지 확인해 이름 계산 논리가 올바른지 검증해요.

# 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"
  }

}

테스트(test) 블록

선택적인 test 블록은 테스트 파일의 구성을 정의하며, 프레임워크가 실행(run)을 어떻게 실행할지 구성할 수 있게 해줘요. 이 블록에는 다음 필드가 있어요:

| 필드 또는 블록 이름 | 설명 | 기본값 | | parallel | 선택적 불리언 속성. true이면 Terraform이 자격이 되는 모든 run 블록을 동시에 실행해요. 자세한 내용은 병렬 실행을 참고하세요. | false |

사용 예시

# with_config.tftest.hcl
test {
  parallel = true
}

Run 블록

run 블록에는 다음 필드와 블록이 있어요:

| 필드 또는 블록 이름 | 설명 | 기본값 | | command | 선택적 속성. apply 또는 plan 중 하나. | apply | | plan_options.mode | 선택적 속성. normal 또는 refresh-only 중 하나. | normal | | plan_options.refresh | 선택적 불리언 속성. | true | | plan_options.replace | 테스트 중인 구성 안의 리소스를 참조하는 리소스 주소 목록을 포함하는 선택적 속성. | | | plan_options.target | 테스트 중인 구성 안의 리소스를 참조하는 리소스 주소 목록을 포함하는 선택적 속성. | | | variables | 선택적 variables 블록. | | | module | 선택적 module 블록. | | | providers | 선택적 providers 속성. | | | assert | 선택적 assert 블록. | | | expect_failures | 선택적 속성. | | | state_key | 선택적 속성. | | | parallel | 선택적 불리언 속성. | false |

command 속성과 plan_options 블록은 각 run 블록에 대해 어떤 명령과 옵션을 실행할지 Terraform에 알려줘요. command 속성이나 plan_options 블록을 지정하지 않으면 기본 작업은 일반적인 Terraform apply 작업이에요.

command 속성은 작업이 plan인지 apply인지를 나타내요. plan_options 블록은 테스트 작성자가 일반적으로 명령줄 플래그와 옵션으로 편집해야 하는 계획 모드(planning mode)옵션을 커스터마이즈할 수 있게 해줘요. -var-var-file 옵션은 Variables 섹션에서 다뤄요.

state_key는 주어진 run 블록에 대해 Terraform이 사용할 내부 상태 파일을 세밀하게 제어할 수 있게 해줘요. 자세한 내용은 모듈 상태(Modules State)를 참고하세요.

parallel 속성은 여러 run 블록을 병렬로 실행할 수 있게 해줘요. 기본적으로 이 속성은 false로 설정돼요. true로 설정하면 Terraform은 그 run 블록을, parallel 속성도 true로 설정되어 있고 서로 의존하지 않는 다른 run 블록과 병렬로 실행하려고 해요.

사용 예시

# with_config.tftest.hcl
test {
  parallel = true
}

variables {
  bucket_prefix = "test"
}

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

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

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

위 예시에서 firstsecond run 블록은 test 블록이 병렬 실행을 활성화하므로 암시적으로 paralleltrue로 설정돼요. third run 블록은 parallelfalse로 설정해 전역 설정을 재정의해요.

단언 (Assertions)

Terraform run 블록의 단언 인자는 구성을 검증하는 데 도움이 돼요. 각 assert 블록에는 condition 인자error_message 인자가 포함돼요. Terraform 테스트 명령 실행이 끝나면 Terraform은 실패한 단언을 테스트 통과 또는 실패 상태의 일부로 표시해요.

단언 참조 (Assertion References)

테스트 안의 단언은 기본 Terraform 구성 안에서 사용할 수 있는 기존 명명된 값(named values)을 참조할 수 있어요. 또한 테스트 단언은 현재 및 이전 run 블록의 출력을 직접 참조할 수 있어요. 이전 예시를 사용하면 condition = output.bucket_name == "test_bucket"라는 조건은 유효한 조건이에요.

변수 (Variables)

구성 안의 입력 변수(Input Variables) 값을 테스트 파일에서 직접 제공할 수 있어요. 테스트 파일 문법은 루트 레벨과 run 블록 안 모두에서 variables 블록을 지원해요. Terraform은 테스트 파일의 모든 변수 값을 파일 안의 모든 run 블록에 전달해요. 특정 run 블록의 변수 값을 그 run 블록 안에서 직접 제공한 값으로 재정의할 수 있어요.

예시의 테스트 파일에 다음 내용을 추가해 보세요:

# variable_precedence.tftest.hcl

variables {
  bucket_prefix = "test"
}

run "uses_root_level_value" {

  command = plan

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

}

run "overrides_root_level_value" {

  command = plan

  variables {
    bucket_prefix = "other"
  }

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

}

두 번째 run 블록을 추가해 bucket_prefix 변수 값을 other로 지정했어요. 이것은 첫 번째 run 블록에서 사용된 테스트 파일이 제공한 test 값을 재정의해요.

명령줄이나 정의 파일로 변수 지정하기

테스트 파일로 변수 값을 지정하는 것 외에도, Terraform test 명령은 변수 값을 지정하는 다른 일반적인 메커니즘도 지원해요. 명령줄변수 정의 파일(Variable definition files)로 모든 테스트에서 변수 값을 지정할 수 있어요.

주 구성 방향과 마찬가지로 Terraform은 테스트 디렉터리 안의 자동 변수 파일에 정의된 변수를 자동으로 로드해요. 자동 변수 파일은 terraform.tfvars, terraform.tfvars.json, 그리고 .auto.tfvars 또는 .auto.tfvars.json으로 끝나는 모든 파일이에요.

참고: 테스트 디렉터리 안의 자동 변수 파일에서 로드된 변수 값은 같은 테스트 디렉터리 안에 정의된 테스트에만 적용돼요. 다른 모든 방식으로 정의된 변수는 주어진 테스트 실행의 모든 테스트에 적용돼요.

이것은 민감한 변수 값을 사용하고 프로바이더를 구성할 때 특히 유용해요. 그렇지 않으면 테스트 파일이 그 민감한 값을 직접 노출할 수 있기 때문이에요.

변수 정의 우선순위

테스트 파일이 제공하는 변수 값을 제외하면 변수 정의 우선순위는 테스트 안에서도 동일해요. 테스트 파일에 정의된 변수는 환경 변수, 변수 파일, 명령줄 입력보다 우선하는 가장 높은 우선순위를 가져요. 테스트 디렉터리에 정의된 테스트의 경우, 테스트 디렉터리의 자동 변수 파일에 정의된 변수 값은 주 구성 디렉터리의 자동 변수 파일에 정의된 값을 재정의해요.

변수 참조

run 블록 안에 정의한 변수는 이전 run 블록에서 실행된 모듈의 출력과 더 높은 우선순위 레벨에서 정의된 변수를 참조할 수 있어요. 파일 레벨 variables 블록 안에 정의된 변수는 전역 변수만 참조할 수 있어요.

예를 들어 다음 코드 블록은 변수가 더 높은 우선순위 변수와 이전 run 블록을 참조하는 방법을 보여줘요:

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.
  # ...
}

위에서 run_block_onelocal_valueglobal_value 변수에서 값을 얻어요. 이 패턴은 여러 변수에 같은 값을 할당하려 할 때 유용해요. 변수 값을 파일 레벨에서 한 번 지정한 다음 다른 변수들과 공유할 수 있어요.

이에 비해 run_block_twolocal_valuerun_block_oneoutput_one 출력 값에서 값을 가져와요. 이 패턴은 run 블록 사이에 값을 전달할 때 유용하며, 특히 Modules 섹션에서 설명하는 것처럼 run 블록이 서로 다른 모듈을 실행할 때 그렇습니다.

프로바이더 (Providers)

providerproviders 블록 및 속성을 사용해 테스트 파일에서 주 구성 안의 필수 프로바이더를 설정하거나 재정의할 수 있어요.

Terraform 테스트 파일의 루트 레벨에서 provider 블록을 Terraform이 주 구성 안에서 만드는 것처럼 정의할 수 있어요. 그러면 Terraform이 각 run 블록이 실행될 때 이 provider 블록을 구성으로 전달해요. 기본적으로 지정한 각 프로바이더는 각 run 블록 안에서 직접 사용할 수 있어요. 특정 run 블록 안에서 프로바이더의 가용성을 providers 속성으로 커스터마이즈할 수 있어요. 이 블록의 동작과 문법은 providers 메타 인자의 동작과 일치해요.

테스트 파일 안에 프로바이더 구성을 제공하지 않으면 Terraform은 프로바이더의 기본 설정을 사용해 구성 안의 프로바이더를 초기화하려고 해요. 예를 들어 프로바이더를 구성하기 위한 환경 변수는 여전히 사용할 수 있고, Terraform은 그것을 사용해 기본 프로바이더를 만들 수 있어요.

아래에서 구성 대신 테스트가 리전을 지정할 수 있도록 이전 예시를 확장해요. 이 예시에서 우리는 다음 구성 파일을 테스트할 거예요:

# 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
}

이제 다음 테스트 파일 안에 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"
  }

}

여러 프로바이더와 별칭(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"
  }
}

특정 run 블록에서 사용할 특정 프로바이더를 정의하는 것도 가능해요:

# main.tf

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

data "aws_region" "primary" {}

data "aws_region" "secondary" {
  provider = aws.secondary
}

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

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

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

테스트 파일은 각각 다른 run 블록에 특정 프로바이더를 전달할 수 있어요:

# customised_providers.tftest.hcl

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

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

provider "aws" {
  alias  = "tertiary"
  region = "eu-west-2"
}

run "default_providers" {

  command = plan

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

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

run "customised_providers" {

  command = plan

  providers = {
    aws           = aws
    aws.secondary = aws.tertiary
  }

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

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

참고: command = apply로 테스트를 실행할 때 run 블록 사이에서 프로바이더를 전환하면 작업과 테스트가 실패할 수 있어요. 한 프로바이더 정의가 만든 리소스를 두 번째 정의가 수정하면 사용할 수 없게 되기 때문이에요.

Terraform v1.7.0부터 provider 블록은 테스트 파일 변수와 run 블록 출력도 참조할 수 있어요. 즉 테스트 프레임워크가 한 프로바이더에서 자격 증명과 기타 설정 정보를 검색하고 두 번째 프로바이더를 초기화할 때 이를 사용할 수 있어요.

다음 예시에서 vault 프로바이더가 먼저 초기화된 다음 설정 모듈 안에서 aws 프로바이더의 자격 증명을 추출하는 데 사용돼요. 설정 모듈에 대한 자세한 내용은 Modules를 참고하세요.


provider "vault" {
  # ... vault configuration ...
}

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

  # The `aws` provider can reference the outputs of the "vault_setup" run block.
  access_key = run.vault_setup.aws_access_key
  secret_key = run.vault_setup.aws_secret_key
}

run "vault_setup" {
  module {
    # This module should only include reference to the Vault provider. Terraform
    # will automatically work out which providers to supply based on the module
    # configuration. The tests will error if a run block requires access to a
    # provider that references outputs from a run block that has not executed.
    source = "./testing/vault-setup"
  }
}

run "use_aws_provider" {
  # This run block can then use both the `aws` and `vault` providers, as the
  # previous run block provided all the data required for the `aws` provider.
}

모듈 (Modules)

주어진 run 블록이 실행하는 모듈을 수정할 수 있어요. 기본적으로 Terraform은 각 run 블록에 대해 테스트 중인 구성에 대해 주어진 명령을 실행해요. Terraform은 terraform test 명령을 실행한 디렉터리(또는 -chdir 인자로 가리킨 디렉터리) 안의 구성을 테스트해요. 각 run 블록은 module 블록으로 대상 구성을 바꿀 수도 있어요.

전통적인 module 블록과 달리 테스트 파일 안의 module 블록은 source 속성과 version 속성만 지원해요. 전통적인 module 블록으로 일반적으로 제공되는 나머지 속성은 run 블록 안의 대체 속성과 블록으로 제공해야 해요.

참고: Terraform 테스트 파일은 source 속성에서 로컬레지스트리 모듈만 지원해요.

대체 모듈을 실행할 때 run 블록 안의 다른 모든 블록과 속성도 지원되며, assert 블록은 대체 모듈의 값을 대상으로 실행돼요. 이것은 모듈 상태(Modules State)에서 더 자세히 다뤄요.

테스트 파일 안의 modules 블록에 대한 두 가지 예시 사용 사례는 다음과 같아요:

  • 테스트에 필요한 인프라를 주 구성이 요구하는 설정 모듈(setup module).
  • 주 구성이 직접 만들지 않는 보조 인프라(데이터 소스 같은)를 로드하고 검증하는 로딩 모듈(loading module).

다음 예시들은 두 사용 사례를 모두 보여줘요. 먼저 이미 만들어진 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)
}

둘째, 테스트 중인 구성에서 사용할 수 있도록 S3 버킷을 만드는 설정 모듈이 있어요.

# testing/setup/main.tf

variable "bucket" {
  type = string
}

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

셋째, 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"
  }
}

모듈 상태 (Modules state)

Terraform이 terraform test 명령을 실행하는 동안 Terraform은 각 테스트 파일에 대해 메모리에 최소 하나, 많으면 여러 개의 상태 파일을 유지해요. Terraform은 각 내부 상태 파일에 상태 키(state key)를 할당하고, 이를 내부적으로 상태 파일을 추적하는 데 사용해요. 상태 키는 상태 파일의 고유 식별자이며 run 블록의 state_key 속성으로 재정의할 수 있어요.

항상 테스트 중인 주 구성을 유지하는 상태 파일이 최소 하나는 있어요. 이 상태 파일은 대체 모듈을 지정하는 module 블록이 없는 모든 run 블록이 공유해요. 기본적으로 Terraform이 로드하는 대체 모듈당 상태 파일도 하나씩 있어요. 대체 모듈 상태 파일은 주어진 모듈을 실행하는 모든 run 블록이 공유해요.

state_key 속성으로 이 기본 동작을 재정의하고 특정 run 블록에 특정 상태 파일을 사용하도록 강제할 수 있어요. 이것은 같은 모듈을 참조하지 않는 run 블록 사이에 상태를 공유하려 할 때 유용해요.

다음 예시는 주석을 사용해 기본 동작으로 각 run 블록의 상태 파일이 어디서 비롯되는지 설명해요. 아래 예시에서 Terraform은 총 세 개의 상태 파일을 만들고 관리해요. 첫 번째 상태 파일은 테스트 중인 주 구성을 위한 것이고, 두 번째는 설정 모듈, 세 번째는 로딩 모듈을 위한 것이에요.

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"
  }
}

다음 예시는 state_key 속성을 사용해 서로 다른 run 블록에 같은 상태 파일을 사용하도록 강제해요. 아래 예시에서 Terraform은 "setup"과 "init" run 블록이 별도의 소스에서 구성을 로드하더라도 둘 다 공유하는 단일 상태 파일을 만들고 관리해요.

run "setup" {
  state_key = "main"

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

run "init" {

  # By setting the state key to "main" we are telling Terraform to use the same
  # state file for this run block as the "setup" run block. This means that the
  # resources created by the "setup" run block will be available to the
  # configuration in this run block.
  state_key = "main"

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

  # ... more assertions ...
}
모듈 정리 (Modules Cleanup)

테스트 파일이 끝나면 Terraform은 테스트 파일 실행 중에 만든 모든 리소스를 파괴하려고 해요. Terraform이 대체 모듈을 로드할 때 그 객체를 파괴하는 순서는 중요해요. 예를 들어 첫 번째 Modules 예시에서 Terraform은 "execute" run 블록에서 만든 객체보다 먼저 "setup" run 블록에서 만든 리소스를 파괴할 수 없어요. "setup" 단계에서 만든 S3 버킷은 객체를 포함하는 동안에는 파괴할 수 없기 때문이에요.

Terraform은 리소스를 역순 run 블록 순서로 파괴해요. 가장 최근 예시에는 상태 파일이 세 개 있어요. 하나는 주 상태용, 하나는 ./testing/loader 모듈용, 하나는 ./testing/setup 모듈용이에요. ./testing/loader 상태 파일은 마지막 run 블록이 가장 최근에 참조했으므로 가장 먼저 파괴돼요. 주 상태 파일은 "update" run 블록이 참조했으므로 두 번째로 파괴돼요. 그러면 ./testing/setup 상태 파일이 마지막으로 파괴돼요.

참고로 처음 두 run 블록 "setup"과 "init"은 그 상태 파일이 이후 run 블록에서 사용되고 이미 파괴됐기 때문에 파괴 작업 중에 아무것도 하지 않아요. 단일 설정 모듈을 대체 모듈로 사용하고 먼저 실행하거나, 대체 모듈을 사용하지 않는다면 파괴 순서는 여러분에게 영향을 주지 않아요. 더 복잡한 경우에는 리소스 파괴가 자동으로 완료될 수 있도록 주의 깊게 고려해야 할 수도 있어요.

실패 예상하기 (Expecting failures)

기본적으로 check 블록 단언을 포함한 어떤 검증(validation)이 Terraform 테스트 파일 실행 중에 실패하면 전체 명령은 그 테스트를 실패로 보고해요. 하지만 실패 사례를 테스트하고 싶어하는 것은 일반적인 테스트 패러다임이에요. Terraform은 이 사용 사례를 위해 expect_failures 속성을 지원해요.

run 블록에서 expect_failures 속성은 커스텀 조건이 실패해야 하는 검사 가능한(checkable) 객체(리소스, 데이터 소스, check 블록, 입력 변수, 출력) 목록을 제공할 수 있어요. 지정한 검사 가능한 객체가 문제를 보고하면 테스트는 통과하고, 그렇지 않으면 전체 테스트가 실패해요.

expect_failures 블록과 함께 단언을 사용할 수 있지만, check 블록을 제외한 모든 검증 기능은 실패 시 Terraform 실행을 중단한다는 점을 고려하세요. 이것은 테스트 실행 중에도 적용되므로, 여러분의 단언은 검사 가능한 객체가 실패할 차례가 되기 전에 확실히 계산되는 값만 평가해야 해요. 참조를 사용하거나 주 구성 안의 depends_on 메타 인자로 이를 관리할 수 있어요.

이것은 또한 check 블록을 제외하고는 신뢰할 수 있게 포함할 수 있는 검사 가능한 객체가 하나뿐이라는 뜻이기도 해요. 우리는 check 블록을 위해서만 expect_failures 속성 안에서 검사 가능한 객체 목록을 지원해요.

아래 빠른 예시는 입력 변수의 validation 블록을 테스트하는 방법을 보여줘요. 이 구성 파일은 짝수여야 하는 단일 입력 변수를 받아요.

# main.tf

variable "input" {
  type = number

  validation {
    condition = var.input % 2 == 0
    error_message = "must be even number"
  }
}

테스트 파일에는 두 개의 run 블록이 있어요. 하나는 우리의 커스텀 조건이 짝수에서 통과하는지 검증하고, 하나는 홀수에서 우리의 커스텀 조건이 실패하는지 검증해요.

# input_validation.tftest.hcl

variables {
  input = 0
}

run "zero" {
  # The variable defined above is even, so we expect the validation to pass.

  command = plan
}

run "one" {
  # This time we set the variable is odd, so we expect the validation to fail.

  command = plan

  variables {
    input = 1
  }

  expect_failures = [
    var.input,
  ]
}

참고: Terraform은 run 블록의 command 속성으로 지정된 작업에서만 실패를 예상해요. command = applyrun 블록에서 expect_failures를 사용할 때는 주의하세요. 커스텀 조건 실패를 예상하는 command = apply run 블록은 그 커스텀 조건이 계획 중에 실패하면 전체적으로 실패해요.

이것은 논리적으로 일관돼요. run 블록이 apply 작업을 실행할 수 있기를 기대하지만 계획이 실패했기 때문이에요. 그것은 또한 잠재적으로 혼란스러울 수 있는데, 그 실패가 예상된 것으로 표시됐더라도 진단에서 그 실패를 테스트 실패의 원인으로 볼 수 있기 때문이에요.

Terraform이 계획 단계에서 커스텀 조건을 실행하지 않는 경우가 있는데, 그 조건이 Terraform이 참조 리소스를 만든 후에만 사용할 수 있는 계산된(computed) 속성에 의존할 때예요. 이런 경우 command = apply 속성과 값과 함께 expect_failures 블록을 사용할 수 있어요. 하지만 대부분의 경우 command = plan 작업과 함께만 expect_failures를 사용하는 것을 권장해요.

참고: 예상 실패는 사용자 정의 커스텀 조건에만 적용돼요. 지정된 예상 실패 외의 다른 종류의 실패는 여전히 전체 테스트를 실패시킵니다. 예를 들어 불리언 값을 입력으로 기대하는 변수는, 그 변수가 expect_failures 속성에 포함돼 있더라도 Terraform이 잘못된 종류의 값을 제공하면 주변 테스트를 실패시켜요.

expect_failures 속성은 작성자가 자신의 구성과 그 안에 정의된 논리를 테스트할 수 있도록 포함된 것이에요. 이전 예시의 타입 불일치는 Terraform 작성자가 테스트해야 할 걱정이 없는 것입니다. Terraform 자체가 타입 제약을 적용하기 때문이에요. 따라서 커스텀 조건에서만 expect_failures를 사용할 수 있어요.

병렬 실행 (Parallel execution)

기본적으로 Terraform은 run 블록을 순차적으로 실행해요. 하지만 선택적인 test 블록이나 개별 run 블록에서 parallel 속성을 true로 설정해 병렬 실행을 활성화할 수 있어요.

두 개 이상의 run 블록은 다음 조건을 만족하면 병렬로 실행될 수 있어요:

  • 서로의 출력을 참조하지 않는다.
  • 같은 상태 파일을 공유하지 않는다. 상태 파일은 상태 키 또는 상태 키가 설정되지 않았을 때는 모듈 소스에 의해 결정돼요. 두 run 블록이 같은 모듈 구성을 공유한다면 병렬 실행을 위해서는 서로 다른 상태 키를 지정해야 해요.
  • 둘 다 parallel 속성이 true로 설정돼 있다.

사용 예시

# parallel.tftest.hcl

test {
  // This would set the parallel flag to true in all runs
  parallel = true
}

variables {
  foo = "foo"
}

run "primary_db" {
  // This is the first run block, and it is available to be executed right away.
  state_key = "primary"
  module {
    source = "./setup"
  }

  variables {
    input = "foo"
  }

  assert {
    condition = output.value == var.foo
    error_message = "bad"
  }
}

run "secondary_db" {
  // This run block can be executed in parallel with the `primary_db` run block, because it does not reference its
  // output and has a different state key.
  state_key = "secondary"
  module {
    source = "./setup"
  }

  variables {
    input = "foo"
  }

  assert {
    condition = output.value == var.foo
    error_message = "bad"
  }
}

run "site_one" {
  // This run block can only be executed after the `primary_db` run block is completed, because it references the output of the `primary_db` run block.
  state_key = "unique_2"
  variables {
    input = run.primary_db.value
  }

  assert {
    condition = output.value == var.foo
    error_message = "double bad"
  }
}

run "site_two" {
  // This run block can only be executed after the `primary_db` run block is completed, because it references the output of the `primary_db` run block.
  // After that, it can be executed in parallel with the `site_one` run block.
  state_key = "unique_3"
  variables {
    input = run.primary_db.value
  }

  assert {
    condition = output.value == var.foo
    error_message = "double bad"
  }
}

run "using_external_db" {
  // This run block does not reference the output of any other run block, and it has a different state key from its
  // preceding runs, so it can be executed in parallel with runs `primary_db` and `secondary_db`.
  state_key = "unique_4"
  variables {
    input = "externally_created_db"
  }

  assert {
    condition = output.value == var.foo
    error_message = "double bad"
  }
}

run "site_four" {
  // This run block has set `parallel = false`.
  // Therefore, it will wait for all preceding runs to complete before it can be executed.
  state_key = "unique_5"

  // This overrides the global parallel flag.
  parallel = false
  variables {
    input = run.secondary_db.value
  }

  assert {
    condition = output.value == var.foo
    error_message = "double bad"
  }
}

run "site_five" {
  // This run block will wait for the run `site_four` to complete, because run `site_four` has the `parallel` attribute set to `false`.
  state_key = "unique_6"
  variables {
    input = run.secondary_db.value
  }

  assert {
    condition = output.value == var.foo
    error_message = "double bad"
  }
}

run "site_six" {
  // This run block can only be executed after the run `site_five` is completed, because it references the output of the run `site_five`.
  state_key = "unique_7"
  variables {
    input = run.site_five.value
  }

  assert {
    condition = output.value == var.foo
    error_message = "double bad"
  }
}

run "same_state" {
  // This run block uses an existing state key "unique_7", so it will wait for the run `site_six` to complete.
  state_key = "unique_7"
  variables {
    input = "another_external_db"
  }

  assert {
    condition = output.value == var.foo
    error_message = "double bad"
  }
}

run "site_eight" {
  // This run block is entirely unrelated to the other run blocks, however, because `site_four`,
  // which is one of its preceding runs has the `parallel` attribute set to `false`,
  // it cannot run until site_four is completed. It can be executed in parallel with `site_five` and `site_six`.
  state_key = "unique_7"
  variables {
    input = "yet_another_external_db"
  }

  assert {
    condition = output.value == var.foo
    error_message = "double bad"
  }
}

참고: 테스트 파일에서 parallel=true인 일련의 run을 구성하고 parallel=false인 단일 run을 포함하면 작업 흐름을 두 그룹으로 나누는 동기화 지점을 만들게 돼요. parallel=false 앞의 모든 run이 먼저 완료되어야 해요. 그런 다음 parallel=false인 run이 완료된 후 이후 run들이 시작돼요.

Run A (parallel: true)
Run B (parallel: true)
Run C (parallel: false)
Run D (parallel: true)
Run E (parallel: true)

Run A와 B는 동시에 실행돼요. Run C는 A와 B가 모두 끝날 때까지 기다렸다가 시작해요. 마지막으로 run D와 E는 병렬로 실행되지만, C가 완료된 후에만 실행돼요.

더 알아보기 (Learn more)