HCL 표현식 레퍼런스

HCL 표현식 레퍼런스 (HCL expressions reference)

이 글에서는 Packer용 HCL 템플릿에서 사용할 수 있는 표현식에 대한 레퍼런스 정보를 다뤄요.

출처: Packer 공식 문서

본문

소개 (Introduction)

표현식(Expressions) 은 구성 안에서 값을 참조하거나 계산하는 데 사용돼요. 가장 단순한 표현식은 "hello"나 5 같은 리터럴 값이지만, HCL은 source가 내보낸 데이터에 대한 참조, 산술, 조건 평가, 여러 내장 함수 같은 더 복잡한 표현식도 허용해요.

표현식은 HCL의 여러 곳에서 사용할 수 있지만, 일부 컨텍스트는 특정 타입의 리터럴 값을 요구하거나 금지하는 식으로 어떤 표현식 구문이 허용되는지 제한해요. 각 언어 기능의 문서는 표현식에 적용되는 제한을 설명해요.

이 페이지의 나머지에서는 Packer 표현식 문법의 모든 기능을 설명해요.

타입과 값 (Types and Values)

표현식의 결과는 값(value) 이에요. 모든 값은 타입(type) 을 가지며, 이 타입이 그 값을 어디에 쓸 수 있고 어떤 변환이 적용될 수 있는지를 정해요.

HCL은 값에 다음 타입을 사용해요:

  • string: "hello" 같은 어떤 텍스트를 나타내는 유니코드 문자들의 시퀀스.
  • number: 숫자 값. number 타입은 15 같은 정수와 6.283185 같은 분수 값을 모두 나타낼 수 있어요.
  • bool: true 또는 false. bool 값은 조건 로직에 쓸 수 있어요.
  • list (또는 tuple): ["us-west-1a", "us-west-1c"] 같은 값들의 시퀀스. 리스트나 튜플의 요소는 0부터 시작하는 연속된 정수로 식별돼요.
  • map (또는 object): {name = "Mabel", age = 52} 같은 이름 있는 라벨로 식별되는 값들의 그룹.

문자열, 숫자, bool은 때로 원시 타입(primitive types) 이라고 불려요. 리스트/튜플과 맵/객체는 때로 복합 타입(complex types), 구조적 타입(structural types), 또는 컬렉션 타입(collection types) 이라고 불려요.

마지막으로 타입이 없는 특별한 값이 하나 있어요:

  • null: 부재(absence) 또는 생략(omission) 을 나타내는 값. source나 module의 인자를 null로 설정하면 Packer는 여러분이 완전히 생략한 것처럼 동작해요 — 기본값이 있으면 그 기본값을 쓰고, 인자가 필수라면 오류를 내요. null은 조건 표현식에서 가장 유용해요. 조건이 충족되지 않을 때 인자를 동적으로 생략할 수 있죠.

고급 타입 세부 정보 (Advanced Type Details)

대부분의 상황에서 리스트와 튜플은 동일하게 동작하고, 맵과 객체도 마찬가지예요. 그 구분이 중요하지 않을 때마다 Packer 문서는 각 쌍의 용어를 혼용해요("list"와 "map"을 역사적으로 선호하면서요).

다만 플러그인 작성자는 이 비슷한 타입들(그리고 관련된 set 타입)의 차이를 이해해야 해요. 입력 변수와 source 인자의 허용 값을 제한하는 방식이 서로 다르기 때문이에요.

타입 변환 (Type Conversion)

표현식은 보통 인자에 값을 설정하는 데 사용돼요. 이런 경우 인자에는 예상 타입이 있고, 주어진 표현식은 그 타입의 값을 만들어야 해요.

가능한 경우 Packer는 예상 타입을 만들기 위해 값을 한 타입에서 다른 타입으로 자동 변환해요. 불가능하면 Packer는 타입 불일치 오류를 내고, 더 적합한 표현식으로 구성해야 해요.

Packer는 필요할 때 number와 bool 값을 문자열로 자동 변환해요. 또한 문자열이 숫자나 bool 값의 유효한 표현을 담고 있는 한 문자열을 숫자나 bool로도 변환해요.

  • true는 "true"로 변환되고 그 반대도 마찬가지예요.
  • false는 "false"로 변환되고 그 반대도 마찬가지예요.
  • 15는 "15"로 변환되고 그 반대도 마찬가지예요.

리터럴 표현식 (Literal Expressions)

리터럴 표현식(literal expression) 은 특정 상수 값을 직접 나타내는 표현식이에요. Packer는 위에서 설명한 각 값 타입에 대해 리터럴 표현식 문법을 가져요:

  • 문자열은 보통 큰따옴표로 묶인 유니코드 문자 시퀀스("like this")로 나타내요. 더 복잡한 문자열을 위한 "heredoc" 문법도 있어요. 문자열 리터럴은 Packer에서 가장 복잡한 리터럴 표현식이고, 이 페이지에 추가 문서가 있어요:

    • 이스케이프 시퀀스와 heredoc 문법에 대한 정보는 아래 문자열 리터럴을 참고해 주세요.
    • 보간과 템플릿 지시문에 대한 정보는 아래 문자열 템플릿을 참고해 주세요.
  • 숫자는 소수점이 있거나 없는 따옴표 없는 숫자 시퀀스(15나 6.283185)로 나타내요.

  • bool은 따옴표 없는 기호 true와 false로 나타내요.

  • null 값은 따옴표 없는 기호 null로 나타내요.

  • 리스트/튜플은 쉼표로 구분된 값 시퀀스를 담은 한 쌍의 대괄호(["a", 15, true])로 나타내요.

리스트 리터럴은 가독성을 위해 여러 줄로 나눌 수 있지만, 값 사이에는 항상 쉼표가 필요해요. 마지막 값 뒤의 쉼표는 허용되지만 필수는 아니에요. 리스트의 값은 임의의 표현식일 수 있어요.

  • 맵/객체는 <KEY> = <VALUE> 쌍들의 시퀀스를 담은 한 쌍의 중괄호로 나타내요:
{
  name = "John"
  age  = 52
}

키/값 쌍은 쉼표나 줄바꿈으로 구분할 수 있어요. 값은 임의의 표현식일 수 있어요. 키는 문자열이고, 유효한 식별자라면 따옴표 없이 둘 수 있지만, 그 외에는 반드시 따옴표를 붙여야 해요. 리터럴이 아닌 표현식을 키로 쓰려면 (var.business_unit_tag_name) = "SRE"처럼 괄호로 감싸면 돼요.

이름 있는 값 참조 (References to Named Values)

다음의 이름 있는 값이 사용 가능해요:

사용 가능한 함수 (Available Functions)

사용 가능한 함수의 전체 목록은 함수 레퍼런스를 참고해 주세요.

조건 표현식 (Conditional Expressions)

조건 표현식은 불리언 표현식의 값을 사용해 두 값 중 하나를 선택해요. 이것은 표현식 안에서 if-then-else 로직을 표현하는 간결한 방법이에요.

조건 표현식의 문법은:

condition ? true_val : false_val

condition이 true이면 표현식은 true_val을 반환하고, false이면 false_val을 반환해요. true_val과 false_val 인자는 호환 가능한 타입이어야 해요.

예시 (Examples)

조건 표현식 사용 예시 몇 가지를 보여줄게요.

  1. 기본값 설정 (Setting a Default Value)

조건을 사용해 사용자 정의할 수 있는 변수에 기본값을 제공할 수 있어요.

locals {
  # Set the region to the value of var.region if it is not empty.
  # Otherwise, use a default value.
  region = var.region != "" ? var.region : "us-east-1"
}
  1. 인자 동적 생략 (Dynamically Omitting an Argument)

null 값을 사용해 source에서 인자를 동적으로 생략할 수 있어요. 이는 인자가 설정되는 것을 효과적으로 "끄거나" 방지하는 흔한 패턴이에요.

variable "use_http" {
  type        = bool
  description = "Whether to use HTTP for file transfer."
  default     = true
}

source "example" "foo" {
  # The http_directory argument is only set if var.use_http is true.
  # If false, http_directory will be unset.
  http_directory = var.use_http ? "/path/to/files" : null
}

이것은 상호 배타적인 두 인자 중 하나를 설정하는 데도 쓸 수 있어요:

source "builder" "example" {
  # If var.use_http is true, http_content is set and cd_content is null.
  # If var.use_http is false, http_content is null and cd_content is set.
  http_content = var.use_http ? local.http_files : null
  cd_content   = !var.use_http ? local.cd_files : null
}

가독성과 권장 사례 (Readability and Recommended Practices)

조건 표현식은 강력하지만, 조심해서 쓰지 않으면 복잡하거나 읽기 어려운 구성을 만들 수 있어요.

  • 명확성 우선 (Prioritize Clarity): 조건 표현식이 매우 길어지거나 여러 중첩 조건을 포함하게 되면, 그것을 쪼개는 것을 고려해 보세요.
  • 복잡성에는 locals 활용 (Leverage locals for Complexity): 더 복잡한 조건 로직에서는 결과를 local 변수에 정의해 주세요. 그러면 local이 소비되는 메인 source나 provisioner 블록에서 가독성이 좋아져요.

예시:

locals {
  # Determine the kickstart file based on the build environment.
  kickstart_config_path = var.environment == "production" ?
                          "config/prod-ks.cfg" :
                          (var.environment == "staging" ?
                           "config/stage-ks.cfg" :
                           "config/dev-ks.cfg")
}

source "builder" "example" {
  # ...
  # Use the pre-determined kickstart path.
  http_content = file(local.kickstart_config_path)
  # ...
}
  • 인자 생략에 null 사용 (Use null to Omit Arguments): 예시에서 보여줬듯이 null을 사용하는 것이 인자가 설정되는 것을 막거나 조건에 따라 설정을 효과적으로 "끄는" 관용적인 방법이에요.

for 표현식 (for Expressions)

for 표현식 은 다른 복합 타입 값을 변환해 복합 타입 값을 만들어요. 입력 값의 각 요소는 결과에서 한 개 또는 0개의 값에 대응할 수 있고, 각 입력 요소를 출력 요소로 변환하는 데 임의의 표현식을 쓸 수 있어요.

예를 들어 var.list가 문자열 리스트라면 다음 표현식은 모두 대문자인 문자열 리스트를 만들어요:

[for s in var.list : upper(s)]

이 for 표현식은 var.list의 각 요소를 순회하고, s를 각 요소로 설정해 upper(s) 표현식을 평가해요. 그런 다음 그 표현식을 실행한 모든 결과로 같은 순서의 새 튜플 값을 만들어요.

for 표현식 주위의 괄호 타입이 어떤 타입의 결과를 만드는지 정해요. 위 예시는 [와 ]를 사용해 튜플을 만들어요. {와 }를 대신 쓰면 결과는 객체이고, => 기호로 구분된 두 개의 결과 표현식을 제공해야 해요:

{for s in var.list : s => upper(s)}

이 표현식은 속성이 var.list의 원래 요소들이고 대응하는 값이 대문자 버전인 객체를 만들어요.

for 표현식은 선택적인 if 절을 포함해 소스 컬렉션에서 요소를 필터링할 수도 있어요. 그러면 소스보다 요소가 적은 값을 만들 수 있어요:

[for s in var.list : upper(s) if s != ""]

소스 값은 객체나 맵 값일 수도 있는데, 이 경우 키와 값을 각각 접근하기 위해 두 개의 임시 변수 이름을 제공할 수 있어요:

[for k, v in var.map : length(k) + length(v)]

마지막으로 결과 타입이 객체(구분자 {와 })라면 값 결과 표현식 뒤에 ... 기호를 붙여 공통 키를 가진 결과를 함께 그룹화할 수 있어요:

{for s in var.list : substr(s, 0, 1) => s... if s != ""}

스플랫 표현식 (Splat Expressions)

스플랫 표현식(splat expression) 은 그렇지 않으면 for 표현식으로 수행할 수 있는 흔한 연산을 더 간결하게 표현하는 방법이에요.

var.list가 모두 id 속성을 가진 객체 리스트라면, ID 리스트를 다음 for 표현식으로 만들 수 있어요:

[for o in var.list : o.id]

이것은 다음 스플랫 표현식 과 동등해요:

var.list[*].id

특수한 [*] 기호는 왼쪽에 주어진 리스트의 모든 요소를 순회하고, 각 요소에서 오른쪽에 주어진 속성 이름을 접근해요. 스플랫 표현식은 기호 오른쪽으로 연산 시퀀스를 확장해 복합 타입 리스트의 속성과 인덱스에도 접근할 수 있어요:

var.list[*].interfaces[0].name

위 표현식은 다음 for 표현식과 동등해요:

[for o in var.list : o.interfaces[0].name]

스플랫 표현식은 리스트 전용이에요(그래서 맵으로 표현되는 for_each로 만든 리소스 참조에는 쓸 수 없어요). 다만 스플랫 표현식을 리스트나 튜플이 아닌 값에 적용하면, 처리 전에 그 값이 자동으로 단일 요소 리스트로 감싸져요.

예를 들어 var.single_object[*].id는 [var.single_object][*].id, 즉 실질적으로 [var.single_object.id]와 동등해요. 이 동작은 대부분의 경우 흥미롭지 않지만, count가 설정되었을 수도 아닐 수도 있어서 튜플 값을 만들 수도 안 만들 수도 있는 리소스를 참조할 때 특히 유용해요:

aws_instance.example[*].id

위 표현식은 aws_instance.example에 count가 설정되었든 아니든 ID 리스트를 만들어요. 특정 리소스가 count를 설정했다가 뺐다가 하더라도, 구성의 다른 여러 표현식을 수정할 필요가 없어지죠.

dynamic 블록 (dynamic blocks)

source 같은 최상위 블록 구조 안에서 표현식은 보통 name = expression나 key = expression 형태로 인자에 값을 할당할 때만 쓸 수 있어요. 이것은 많은 용도를 다루지만, 일부 source 타입은 인자에 표현식을 받지 않는 반복 가능한 중첩 블록(nested blocks) 을 포함해요:

source "amazon-ebs" "example" {
  name = "pkr-test-name" # can use expressions here

  tag {
    # but the "tag" block is always a literal block
  }
}

특수한 dynamic 블록 타입을 사용해 tag 같은 반복 가능한 중첩 블록을 동적으로 만들 수 있어요. dynamic은 최상위 블록 구조에서 지원돼요. 예:

locals {
  standard_tags = {
    Component   = "user-service"
    Environment = "production"
  }
}

source "amazon-ebs" "example" {
  # ...

  tag {
    key                 = "Name"
    value               = "example-asg-name"
  }

  dynamic "tag" {
    for_each = local.standard_tags

    content {
      key                 = tag.key
      value               = tag.value
    }
  }
}

dynamic 블록은 for 표현식과 상당히 비슷하게 동작하지만, 복합 타입 값 대신 중첩 블록을 만들어요. 주어진 복합 값을 순회하고, 그 복합 값의 각 요소에 대해 중첩 블록을 생성해요.

  • dynamic 블록의 라벨(위 예시의 "tag")은 어떤 종류의 중첩 블록을 생성할지 지정해요.
  • for_each 인자는 순회할 복합 값을 제공해요.
  • iterator 인자(선택)는 복합 값의 현재 요소를 나타내는 임시 변수의 이름을 설정해요. 생략하면 변수 이름은 dynamic 블록의 라벨(위 예시의 "tag")을 기본값으로 해요.
  • labels 인자(선택)는 생성된 각 블록에 순서대로 사용할 블록 라벨을 지정하는 문자열 리스트예요. 이 값에서 임시 iterator 변수를 쓸 수 있어요.
  • 중첩된 content 블록은 생성된 각 블록의 본문을 정의해요. 이 블록 안에서 임시 iterator 변수를 쓸 수 있어요.

for_each 인자는 어떤 컬렉션 또는 구조적 값이든 받아들이므로, for 표현식이나 스플랫 표현식으로 기존 컬렉션을 변환할 수 있어요.

iterator 객체(위 예시의 tag)에는 두 가지 속성이 있어요:

  • key는 현재 요소의 맵 키 또는 리스트 요소 인덱스예요. for_each 표현식이 set 값을 만들면 key는 value와 동일하므로 사용해서는 안 돼요.
  • value는 현재 요소의 값이에요.

dynamic 블록은 구성 중인 source 타입, 데이터 소스, 또는 provisioner에 속한 중첩 블록에만 인자를 생성할 수 있어요.

for_each 값은 원하는 각 중첩 블록당 하나의 요소를 가진 맵 또는 set이어야 해요. 중첩 데이터 구조를 기반으로 리소스 인스턴스를 선언해야 하거나 여러 데이터 구조의 요소 조합을 선언해야 한다면, 표현식과 함수를 사용해 적합한 값을 도출할 수 있어요. 이런 상황의 몇 가지 흔한 예시는 flatten과 setproduct 함수를 참고해 주세요.

dynamic 블록의 모범 사례 (Best Practices for dynamic Blocks)

dynamic 블록을 과도하게 사용하면 구성이 읽기·유지보수하기 어려워질 수 있어요. 재사용 가능한 코드를 위한 깔끔한 사용자 인터페이스를 만들기 위해 세부 사항을 숨겨야 할 때만 사용하는 걸 권장해요. 가능하면 항상 중첩 블록을 리터럴로 작성해 주세요.

문자열 리터럴 (String Literals)

HCL에는 문자열 리터럴을 위한 두 가지 다른 문법이 있어요. 가장 흔한 것은 따옴표 문자(")로 문자열을 구분하는 "hello" 방식이에요. 따옴표 문자열에서 백슬래시 문자는 이스케이프 시퀀스로 쓰이고, 다음 문자가 이스케이프 동작을 선택해요:

Sequence Replacement
\n 새줄 (Newline)
\r 캐리지 리턴 (Carriage Return)
\t 탭 (Tab)
\" 리터럴 따옴표(문자열을 끝내지 않음)
\\ 리터럴 백슬래시
\uNNNN 기본 다국어 평면의 유니코드 문자(NNNN은 4자리 16진수)
\UNNNNNNNN 보조 평면의 유니코드 문자(NNNNNNNN은 8자리 16진수)

문자열 리터럴의 대안 문법은 Unix 셸 언어에서 영감을 받은 소위 Here Documents 또는 "heredoc" 스타일이에요. 이 스타일은 문자열을 닫는 데 자체 줄의 사용자 정의 구분 단어를 사용해 여러 줄 문자열을 더 명확하게 표현할 수 있게 해줘요:

<<EOF
hello
world
EOF

줄 끝의 << 표시 뒤에 오는 임의의 식별자가 그 시퀀스를 도입해요. 그러면 Packer는 도입부에 주어진 식별자로만 이루어진 줄을 찾을 때까지 다음 줄들을 처리해요. 위 예시에서 EOF가 선택된 식별자예요. 어떤 식별자든 허용되지만, 관례적으로 이 식별자는 모두 대문자이고 "end of"를 뜻하는 EO로 시작해요. 이 경우 EOF는 "end of text"를 의미해요.

위에 보여준 "heredoc" 형태는 뒤따르는 줄들이 왼쪽 여백과 나란해야 해요. 표현식이 들여쓰기된 블록 안에 있을 때 이는 어색할 수 있어요:

block {
  value = <<EOF
hello
world
EOF
}

이를 개선하기 위해 Packer는 <<- 시퀀스로 도입되는 들여쓰기된(indented) heredoc 문자열 변형도 허용해요:

block {
  value = <<-EOF
  hello
    world
  EOF
}

이 경우 Packer는 시퀀스의 줄들을 분석해 선행 공백이 가장 적은 줄을 찾고, 모든 줄의 시작에서 그만큼의 공백을 잘라내요. 결과는 다음과 같아요:

hello
  world

heredoc 문자열 표현식에서는 백슬래시 시퀀스가 해석되지 않아요. 대신 백슬래시 문자는 리터럴로 해석돼요.

따옴표 및 heredoc 문자열 표현식 모두에서 Packer는 ${와 %{로 시작하는 템플릿 시퀀스를 지원해요. 이것들은 다음 절에서 자세히 설명해요. 템플릿 시퀀스를 시작하지 않고 이 시퀀스들을 리터럴로 포함하려면 처음 문자를 두 번 쓰면 돼요: $${ 또는 %%{.

문자열 템플릿 (String Templates)

따옴표 및 heredoc 문자열 표현식 안에서 ${와 %{ 시퀀스는 템플릿 시퀀스 를 시작해요. 템플릿을 사용하면 문자열 리터럴에 표현식을 직접 임베드해, 다른 값들로 문자열을 동적으로 만들 수 있어요.

다음 예시는 템플릿 시퀀스를 사용해 변수 값을 스크립트 내용으로 쓸 수 있는 문자열에 임베드하는 방법을 보여줘요:

locals {
  packages = ["git", "curl", "vim"]

  install_packages = <<-EOF
    #!/bin/bash
    if [ ${length(local.packages)} -eq 0 ]; then
      echo "No packages to install."
      exit 1
    fi
    apt-get update
    %{ for package in local.packages ~}
    apt-get install -y ${package}
    %{ endfor ~}
  EOF
}

source "amazon-ebs" "example" {
  # ...
}

build {
  sources = ["source.amazon-ebs.example"]

  provisioner "shell" {
    inline = [local.install_packages]
  }
}

이것은 packer console 명령어로 테스트할 수 있어요:

$ packer source.pkr.hcl

> local.install_packages

> #!/bin/bash
if [ 3 -eq 0 ]; then
  echo "No packages to install."
  exit 1
fi
apt-get update
    apt-get install -y git
    apt-get install -y curl
    apt-get install -y vim