타입 제약

타입 제약 (Type Constraints)

Terraform 모듈 작성자와 프로바이더 개발자는 상세한 타입 제약을 사용해 입력 변수와 리소스 인자에 사용자가 제공하는 값을 검증할 수 있어요. 여기에는 Terraform의 타입 시스템에 대한 추가 지식이 필요하지만, 모듈과 리소스에 더 견고한 사용자 인터페이스를 만들 수 있게 해줘요.

출처: 문서

본문

타입 키워드와 생성자 (Type Keywords and Constructors)

타입 제약은 타입 키워드타입 생성자라는 함수 같은 구조의 혼합으로 표현돼요.

  • 타입 키워드는 정적 타입을 나타내는 따옴표 없는 기호예요.
  • 타입 생성자는 따옴표 없는 기호 뒤에 괄호가 오는 구조로, 괄호 안의 인자가 타입에 대한 더 많은 정보를 지정해요. 인자가 없으면 타입 생성자는 완전한 타입을 나타내지 않고, 비슷한 타입들의 종류를 나타내요.

타입 제약은 다른 종류의 Terraform 표현식처럼 보이지만 특별한 문법이에요. Terraform 언어에서 이것은 입력 변수type 인자, 모듈 출력, 또는 convert 함수 호출에서 유효해요.

원시 타입 (Primitive Types)

원시 타입은 다른 타입들로 만들어지지 않은 단순한 타입이에요. Terraform의 모든 원시 타입은 타입 키워드로 표현돼요. 사용 가능한 원시 타입은:

  • string: "hello" 같은 일부 텍스트를 나타내는 유니코드 문자 나열
  • number: 숫자 값. 15 같은 정수와 6.283185 같은 분수 값을 모두 표현 가능
  • bool: true 또는 false. 조건부 로직에 사용 가능

원시 타입 변환

Terraform 언어는 필요할 때 numberbool 값을 string 값으로 자동 변환하고, 문자열이 숫자나 불리언 값의 유효한 표현을 포함한다면 그 반대도 자동 변환해요.

  • true"true"로 변환, 그 반대도 동일
  • false"false"로 변환, 그 반대도 동일
  • 15"15"로 변환, 그 반대도 동일

복합 타입 (Complex Types)

복합 타입은 여러 값을 단일 값으로 묶는 타입이에요. 복합 타입은 타입 생성자로 표현되지만, 몇몇은 축약 키워드 버전도 있어요.

복합 타입에는 두 가지 범주가 있어요: 컬렉션 타입(유사한 값 묶기)과 구조적 타입(잠재적으로 서로 다른 값 묶기).

컬렉션 타입 (Collection Types)

컬렉션 타입은 하나의 다른 타입의 여러 값을 단일 값으로 묶을 수 있게 해줘요. 컬렉션 안의 값 타입을 요소 타입이라고 해요. 모든 컬렉션 타입은 요소 타입을 가져야 하며, 생성자의 인자로 제공돼요.

예를 들어 list(string) 타입은 "문자열 리스트"를 뜻하며, list(number)(숫자 리스트)와는 다른 타입이에요. 컬렉션의 모든 요소는 항상 같은 타입이어야 해요.

Terraform 언어의 세 가지 컬렉션 타입:

  • list(...): 0부터 시작하는 연속 정수로 식별되는 값들의 시퀀스

    list 키워드는 list(any)의 축약형이며, 모든 요소가 같은 타입인 한 어떤 요소 타입도 받아요. 이전 구성과의 호환성을 위한 것이므로, 새 코드에서는 전체 형식을 사용하는 것을 권장해요.

  • map(...): 각 값이 문자열 라벨로 식별되는 값들의 컬렉션

    map 키워드는 map(any)의 축약형이며, 모든 요소가 같은 타입인 한 어떤 요소 타입도 받아요. 이전 구성과의 호환성을 위한 것이므로, 새 코드에서는 전체 형식을 사용하는 것을 권장해요.

    맵을 정의할 때 다음 문자를 사용할 수 있어요:

    - `{}`
    - `:`
    - `=`
    

    예를 들어 { "foo": "bar", "bar": "baz" }{ foo = "bar", bar = "baz" }는 같은 맵을 정의해요. 키가 숫자로 시작하거나, 공백을 포함하거나, 특수 문자를 포함하면 키를 따옴표로 감싸야 해요. 그렇지 않으면 생략할 수 있어요. 한 줄 맵에서는 키-값 쌍 사이에 쉼표를 넣어야 해요. 여러 줄 맵에서는 키-값 쌍을 새 줄에 넣을 수 있어요.

    참고: 콜론은 키와 값 사이의 유효한 구분 기호지만 terraform fmt는 콜론을 무시해요. 반면 terraform fmt는 등호를 세로로 정렬하려고 해요.

  • set(...): 중복되지 않는 유일한 값들의 컬렉션으로, 별도의 식별자나 순서가 없음

구조적 타입 (Structural Types)

구조적 타입은 여러 개별 타입의 여러 값을 단일 값으로 묶을 수 있게 해줘요. 구조적 타입은 어느 요소에 어떤 타입을 허용할지 지정하기 위해 인자로 스키마를 요구해요.

Terraform 언어의 두 가지 구조적 타입:

  • object(...): 각각 자신의 타입을 가진 명명된 속성들의 컬렉션

    object 타입의 스키마는 { <KEY> = <TYPE>, <KEY> = <TYPE>, ... } — 즉 쉼표로 구분된 <KEY> = <TYPE> 쌍들을 중괄호로 감싼 형태예요. object 타입과 일치하는 값은 지정된 모든 키를 포함해야 하며, 각 키의 값은 지정된 타입과 일치해야 해요. (추가 키가 있는 값도 object 타입과 일치할 수 있지만, 타입 변환 중에 추가 속성은 버려져요.)

  • tuple(...): 각 요소가 자신의 타입을 가지는, 0부터 시작하는 연속 정수로 식별되는 요소들의 시퀀스

    tuple 타입의 스키마는 [<TYPE>, <TYPE>, ...] — 즉 쉼표로 구분된 타입들을 대괄호로 감싼 형태예요. tuple 타입과 일치하는 값은 정확히 같은 수의 요소를 가져야 하며(더 많거나 적으면 안 됨), 각 위치의 값은 해당 위치의 지정 타입과 일치해야 해요.

예를 들어 object({ name=string, age=number })라는 object 타입은 다음과 같은 값과 일치해요:

{
  name = "John"
  age  = 52
}

또한 object({ id=string, cidr_block=string })라는 object 타입은 aws_vpc.example_vpc처럼 aws_vpc 리소스 참조로 생성된 객체와 일치해요. 리소스에 추가 속성이 있더라도 타입 변환 중에 버려져요.

마지막으로 tuple([string, number, bool])이라는 tuple 타입은 다음과 같은 값과 일치해요:

["a", 15, true]

복합 타입 리터럴

Terraform 언어에는 tuple과 object 값을 만들기 위한 리터럴 표현식이 있으며, 이는 표현식: 리터럴 표현식에서 각각 "list/tuple" 리터럴과 "map/object" 리터럴로 설명돼요.

Terraform은 list, map, set을 직접 나타내는 방법을 제공하지 않아요. 하지만 자동 복합 타입 변환(아래 설명) 덕분에 유사한 복합 타입 간의 차이는 일반 사용자에게 거의 중요하지 않으며, 대부분의 Terraform 문서는 list를 tuple과, map을 object와 혼용해요. 그 구분은 모듈이나 리소스의 입력 값을 제한할 때만 유용해요.

복합 타입 변환

유사한 종류의 복합 타입(list/tuple/set과 map/object)은 Terraform 언어 안에서 보통 서로 바꿔 사용할 수 있으며, 대부분의 Terraform 문서는 복합 타입 종류 간의 차이를 애매하게 넘어가요. 이는 두 가지 변환 동작 때문이에요:

  • 가능할 때마다 Terraform은 제공된 값이 요청된 정확한 타입이 아니면 유사한 종류의 복합 타입 간에 값을 변환해요. "유사한 종류"는 다음과 같이 정의돼요:
    • Objects와 maps는 유사해요.
      • 맵(또는 더 큰 객체)은 object 스키마가 요구하는 키를 최소한 가지고 있다면 object로 변환될 수 있어요. 변환 중에 추가 속성은 버려지므로 map → object → map 변환은 손실이 있을 수 있어요.
    • Tuples와 lists는 유사해요.
      • list는 정확히 요구된 수의 요소를 가질 때만 tuple로 변환될 수 있어요.
    • Sets는 tuple과 list 둘 다에 거의 유사해요:
      • list나 tuple이 set으로 변환될 때 중복 값은 버려지고 요소의 순서는 사라져요.
      • set이 list나 tuple로 변환될 때 요소는 임의의 순서로 배치돼요. set의 요소가 문자열이었다면 사전순으로 정렬되고, 다른 요소 타입의 set은 특정 순서를 보장하지 않아요.
  • 가능할 때마다 Terraform은 복합 타입 안의 요소 값을 변환하는데, 복합 타입 요소를 재귀적으로 변환하거나 위의 원시 타입 변환에 설명된 대로 변환해요.

예를 들어 모듈 인자가 list(string) 타입의 값을 요구하고 사용자가 tuple ["a", 15, true]를 제공한다면, Terraform은 내부적으로 요소를 요구된 string 요소 타입으로 변환해 ["a", "15", "true"]로 바꿔요. 나중에 모듈이 그 요소들을 각각 string, number, bool을 요구하는 다른 리소스 인자를 설정하는 데 사용하면, Terraform은 두 번째와 세 번째 문자열이 number와 bool의 유효한 표현을 포함하므로 그 시점에 요구된 타입으로 자동 변환해요.

반면 제공된 값(요소 값 포함)이 요구된 타입과 호환되지 않으면 자동 변환은 실패해요. 인자가 map(string) 타입을 요구하는데 사용자가 object {name = ["Kristy", "Claudia", "Mary Anne", "Stacey"], age = 12}를 제공하면, tuple은 string으로 변환될 수 없으므로 Terraform은 타입 불일치 오류를 발생시켜요.

동적 타입: "any" 제약

경고: any는 올바른 타입 제약이 되는 경우가 매우 드물어요. 단지 타입 제약을 명시하지 않기 위해 any를 사용하지 마세요. 진짜로 동적 데이터를 다루지 않는 한 항상 정확한 타입 제약을 쓰세요.

any 키워드는 아직 결정되지 않은 타입의 자리표시자 역할을 하는 특별한 구조예요. any는 그 자체로 타입이 아니며, any를 포함한 타입 제약에 대해 값을 해석할 때 Terraform은 any 키워드를 대체해 유효한 결과를 만들 수 있는 단일 실제 타입을 찾으려고 해요.

any를 사용하는 것이 적절한 유일한 상황은 주어진 값을 내용을 직접 접근하지 않고 다른 시스템에 그대로 전달할 때예요. 예를 들어 jsonencode와 함께만 사용해 전체 값을 리소스에 직접 전달한다면 any 타입 변수를 사용해도 괜찮아요:

variable "settings" {
  type = any
}

resource "aws_s3_object" "example" {
  # ...

  # This is a reasonable use of "any" because this module
  # just writes any given data to S3 as JSON, without
  # inspecting it further or applying any constraints
  # to its type or value.
  content = jsonencode(var.settings)
}

모듈의 어떤 부분이 값의 요소나 속성에 접근하거나, 값이 string이나 number일 것을 기대하거나, 다른 불투명하지 않은 처리를 한다면 any를 사용하는 것은 잘못된 일이에요. 대신 모듈이 기대하는 정확한 타입을 쓰세요.

any와 컬렉션 타입

컬렉션의 모든 요소는 같은 타입이어야 하므로, 컬렉션의 요소 타입 자리표시자로 any를 사용하면 Terraform은 결과 컬렉션에 사용할 단일 정확한 요소 타입을 찾으려고 해요.

예를 들어 list(any)라는 타입 제약이 주어지면 Terraform은 주어진 값을 검사하고, 결과를 유효하게 만들 any를 대체할 값을 선택하려고 해요.

주어진 값이 ["a", "b", "c"] — 물리적 타입이 tuple([string, string, string]) — 라면 Terraform은 다음과 같이 분석해요:

  • Tuple 타입과 list 타입은 이전 섹션에 따라 유사하므로 tuple-to-list 변환 규칙이 적용돼요.
  • tuple의 모든 요소가 string이므로 string 타입 제약이 모든 list 요소에 유효해요.
  • 따라서 이 경우 any 인자는 string으로 대체되고, 최종 구체적 값 타입은 list(string)이 돼요.

주어진 tuple의 요소가 모두 같은 타입이 아니면 Terraform은 모두 변환될 수 있는 단일 타입을 찾으려고 해요. Terraform은 앞 섹션에서 설명한 다양한 변환 규칙을 고려해요.

  • 주어진 값이 ["a", 1, "b"]라면 Terraform은 여전히 원시 타입 변환 규칙 때문에 list(string)을 선택하고, 그 타입 제약이 함축하는 문자열 변환으로 인해 결과 값은 ["a", "1", "b"]가 돼요.
  • 주어진 값이 ["a", [], "b"]라면 값은 타입 제약을 따를 수 없어요. 문자열과 빈 tuple이 모두 변환될 수 있는 단일 타입이 없기 때문이에요. Terraform은 이 값을 거부하고 모든 요소가 같은 타입이어야 한다고 불평해요.

위 예제는 list(any)를 사용하지만 map(any)set(any)에도 비슷한 원리가 적용돼요.

선택적 객체 타입 속성 (Optional Object Type Attributes)

Terraform은 일반적으로 지정된 객체 속성에 대한 값을 받지 못하면 오류를 반환해요. 속성을 선택적으로 표시하면 Terraform은 대신 누락된 속성에 기본값을 삽입해요. 이렇게 하면 받는 모듈이 적절한 대체 동작을 설명할 수 있어요.

속성을 선택적으로 표시하려면 객체 타입 제약에서 optional 수정자를 사용해요. 다음 예제는 선택적 속성 b와 기본값이 있는 선택적 속성 c를 생성해요:

variable "with_optional_attribute" {
  type = object({
    a = string                # a required attribute
    b = optional(string)      # an optional attribute
    c = optional(number, 127) # an optional attribute with default value
  })
}

optional 수정자는 하나 또는 두 개의 인자를 받아요.

  • Type: (필수) 첫 번째 인자는 속성의 타입을 지정해요.
  • Default: (선택) 두 번째 인자는 속성이 없을 때 Terraform이 사용할 기본값을 정의해요. 속성 타입과 호환되어야 해요. 지정하지 않으면 Terraform은 적절한 타입의 null 값을 기본값으로 사용해요.

null이 아닌 기본값을 가진 선택적 속성은 받는 모듈 안에서 값이 절대 null이 되지 않도록 보장해요. Terraform은 호출자가 속성을 완전히 생략하거나 명시적으로 null로 설정할 때 모두 기본값을 대체하므로, 가능한 null 값을 처리하기 위한 추가 검사가 필요 없어요.

Terraform은 중첩 변수 타입에서 객체 속성 기본값을 위에서 아래로 적용해요. 즉 optional 수정자에 지정한 기본값을 먼저 적용한 다음, 중첩된 기본값을 해당 속성에 적용해요.

예제: 선택적 속성과 기본값이 있는 중첩 구조

다음 예제는 웹사이트를 호스팅하는 스토리지 버킷용 변수를 정의해요. 이 변수 타입은 여러 선택적 속성을 사용하는데, website는 그 자체로 선택적 속성과 기본값을 가진 선택적 object 타입이에요:

variable "buckets" {
  type = list(object({
    name    = string
    enabled = optional(bool, true)
    website = optional(object({
      index_document = optional(string, "index.html")
      error_document = optional(string, "error.html")
      routing_rules  = optional(string)
    }), {})
  }))
}

다음 terraform.tfvars 파일 예제는 var.buckets에 대한 세 가지 버킷 구성을 지정해요.

  • production은 리디렉션을 추가하는 라우팅 규칙을 설정해요
  • archived는 기본 구성을 사용하지만 비활성화돼요
  • docs는 인덱스와 오류 문서를 텍스트 파일로 재정의해요

production 버킷은 인덱스와 오류 문서를 지정하지 않고, archived 버킷은 website 구성을 완전히 생략해요. Terraform은 bucket 타입 제약에 지정된 기본값을 사용해요:

buckets = [
  {
    name = "production"
    website = {
      routing_rules = <<-EOT
      [
        {
          "Condition" = { "KeyPrefixEquals": "img/" },
          "Redirect"  = { "ReplaceKeyPrefixWith": "images/" }
        }
      ]
      EOT
    }
  },
  {
    name = "archived"
    enabled = false
  },
  {
    name = "docs"
    website = {
      index_document = "index.txt"
      error_document = "error.txt"
    }
  },
]

이 구성은 다음 변수 값을 생성해요.

  • productiondocs 버킷의 경우 Terraform은 enabledtrue로 설정해요. 또한 website에 기본값을 공급하고, docs에 지정된 값이 그 기본값을 재정의해요.
  • archiveddocs 버킷의 경우 Terraform은 routing_rulesnull 값으로 설정해요. Terraform이 선택적 속성을 받지 못하고 지정된 기본값이 없으면 해당 속성을 null 값으로 채워요.
  • archived 버킷의 경우 Terraform은 buckets 타입 제약에 지정된 기본값으로 website 속성을 채워요.
tolist([
  {
    "enabled" = true
    "name" = "production"
    "website" = {
      "error_document" = "error.html"
      "index_document" = "index.html"
      "routing_rules" = <<-EOT
      [
        {
          "Condition" = { "KeyPrefixEquals": "img/" },
          "Redirect"  = { "ReplaceKeyPrefixWith": "images/" }
        }
      ]

      EOT
    }
  },
  {
    "enabled" = false
    "name" = "archived"
    "website" = {
      "error_document" = "error.html"
      "index_document" = "index.html"
      "routing_rules" = tostring(null)
    }
  },
  {
    "enabled" = true
    "name" = "docs"
    "website" = {
      "error_document" = "error.txt"
      "index_document" = "index.txt"
      "routing_rules" = tostring(null)
    }
  },
])

예제: 선택적 속성 조건부 설정

선택적 인자에 값을 설정할지 여부를 다른 데이터를 기반으로 동적으로 결정해야 하는 경우가 있어요. 그럴 때 호출하는 module 블록은 조건부 표현식을 사용하고, 인자 중 하나의 결과 분기로 null을 사용해 인자를 설정하지 않은 채로 동적으로 남길 수 있어요.

이전 섹션에 보여준 variable "buckets" 선언과 함께, 다음 예제는 새 변수 var.legacy_filenames를 기반으로 website 객체의 index_documenterror_document 설정을 조건부로 재정의해요:

variable "legacy_filenames" {
  type     = bool
  default  = false
  nullable = false
}

module "buckets" {
  source = "./modules/buckets"

  buckets = [
    {
      name = "maybe_legacy"
      website = {
        error_document = var.legacy_filenames ? "ERROR.HTM" : null
        index_document = var.legacy_filenames ? "INDEX.HTM" : null
      }
    },
  ]
}

var.legacy_filenamestrue로 설정되면 호출이 문서 파일 이름을 재정의해요. false이면 호출은 두 파일 이름을 지정하지 않은 채로 두어 모듈이 지정된 기본값을 사용할 수 있게 해요.

더 알아보기 (Learn more)