Expressions are used to refer to or compute values within a configuration. The simplest expressions are just literal values, like "hello" or 5, but HCL also allows more complex expressions such as references to data exported by sources, arithmetic, conditional evaluation, and a number of built-in functions.

Expressions can be used in a number of places in HCL, but some contexts limit which expression constructs are allowed, such as requiring a literal value of a particular type or forbidding. Each language feature's documentation describes any restrictions it places on expressions.

The rest of this page describes all of the features of Packer's expression syntax.

»Types and Values

The result of an expression is a value. All values have a type, which dictates where that value can be used and what transformations can be applied to it.

HCL uses the following types for its values:

  • string: a sequence of Unicode characters representing some text, like "hello".
  • number: a numeric value. The number type can represent both whole numbers like 15 and fractional values like 6.283185.
  • bool: either true or false. bool values can be used in conditional logic.
  • list (or tuple): a sequence of values, like ["us-west-1a", "us-west-1c"]. Elements in a list or tuple are identified by consecutive whole numbers, starting with zero.
  • map (or object): a group of values identified by named labels, like {name = "Mabel", age = 52}.

Strings, numbers, and bools are sometimes called primitive types. Lists/tuples and maps/objects are sometimes called complex types, structural types, or collection types.

Finally, there is one special value that has no type:

  • null: a value that represents absence or omission. If you set an argument of a source or module to null, Packer behaves as though you had completely omitted it — it will use the argument's default value if it has one, or raise an error if the argument is mandatory. null is most useful in conditional expressions, so you can dynamically omit an argument if a condition isn't met.

»Advanced Type Details

In most situations, lists and tuples behave identically, as do maps and objects. Whenever the distinction isn't relevant, the Packer documentation uses each pair of terms interchangeably (with a historical preference for "list" and "map").

However, plugin authors should understand the differences between these similar types (and the related set type), since they offer different ways to restrict the allowed values for input variables and source arguments.

»Type Conversion

Expressions are most often used to set values for arguments. In these cases, the argument has an expected type and the given expression must produce a value of that type.

Where possible, Packer automatically converts values from one type to another in order to produce the expected type. If this isn't possible, Packer will produce a type mismatch error and you must update the configuration with a more suitable expression.

Packer automatically converts number and bool values to strings when needed. It also converts strings to numbers or bools, as long as the string contains a valid representation of a number or bool value.

  • true converts to "true", and vice-versa
  • false converts to "false", and vice-versa
  • 15 converts to "15", and vice-versa

»Literal Expressions

A literal expression is an expression that directly represents a particular constant value. Packer has a literal expression syntax for each of the value types described above:

  • Strings are usually represented by a double-quoted sequence of Unicode characters, "like this". There is also a "heredoc" syntax for more complex strings. String literals are the most complex kind of literal expression in Packer, and have additional documentation on this page:

    • See String Literals below for information about escape sequences and the heredoc syntax.
    • See String Templates below for information about interpolation and template directives.
  • Numbers are represented by unquoted sequences of digits with or without a decimal point, like 15 or 6.283185.

  • Bools are represented by the unquoted symbols true and false.

  • The null value is represented by the unquoted symbol null.

  • Lists/tuples are represented by a pair of square brackets containing a comma-separated sequence of values, like ["a", 15, true].

    List literals can be split into multiple lines for readability, but always require a comma between values. A comma after the final value is allowed, but not required. Values in a list can be arbitrary expressions.

  • Maps/objects are represented by a pair of curly braces containing a series of <KEY> = <VALUE> pairs:

      name = "John"
      age  = 52
    {  name = "John"  age  = 52}

    Key/value pairs can be separated by either a comma or a line break. Values can be arbitrary expressions. Keys are strings; they can be left unquoted if they are a valid identifier, but must be quoted otherwise. You can use a non-literal expression as a key by wrapping it in parentheses, like (var.business_unit_tag_name) = "SRE".

»References to Named Values

Packer makes one named values available.

The following named values are available:

»Available Functions

For a full list of available functions, see the function reference.

»for Expressions

A for expression creates a complex type value by transforming another complex type value. Each element in the input value can correspond to either one or zero values in the result, and an arbitrary expression can be used to transform each input element into an output element.

For example, if var.list is a list of strings, then the following expression produces a list of strings with all-uppercase letters:

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

This for expression iterates over each element of var.list, and then evaluates the expression upper(s) with s set to each respective element. It then builds a new tuple value with all of the results of executing that expression in the same order.

The type of brackets around the for expression decide what type of result it produces. The above example uses [ and ], which produces a tuple. If { and } are used instead, the result is an object, and two result expressions must be provided separated by the => symbol:

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

This expression produces an object whose attributes are the original elements from var.list and their corresponding values are the uppercase versions.

A for expression can also include an optional if clause to filter elements from the source collection, which can produce a value with fewer elements than the source:

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

The source value can also be an object or map value, in which case two temporary variable names can be provided to access the keys and values respectively:

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

Finally, if the result type is an object (using { and } delimiters) then the value result expression can be followed by the ... symbol to group together results that have a common key:

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

»Splat Expressions

A splat expression provides a more concise way to express a common operation that could otherwise be performed with a for expression.

If var.list is a list of objects that all have an attribute id, then a list of the ids could be produced with the following for expression:

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

This is equivalent to the following splat expression:


The special [*] symbol iterates over all of the elements of the list given to its left and accesses from each one the attribute name given on its right. A splat expression can also be used to access attributes and indexes from lists of complex types by extending the sequence of operations to the right of the symbol:


The above expression is equivalent to the following for expression:

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

Splat expressions are for lists only (and thus cannot be used to reference resources created with for_each, which are represented as maps). However, if a splat expression is applied to a value that is not a list or tuple then the value is automatically wrapped in a single-element list before processing.

For example, var.single_object[*].id is equivalent to [var.single_object][*].id, or effectively [var.single_object.id]. This behavior is not interesting in most cases, but it is particularly useful when referring to resources that may or may not have count set, and thus may or may not produce a tuple value:


The above will produce a list of ids whether aws_instance.example has count set or not, avoiding the need to revise various other expressions in the configuration when a particular resource switches to and from having count set.

»dynamic blocks

Within top-level block constructs like sources, expressions can usually be used only when assigning a value to an argument using the name = expression or key = expression form. This covers many uses, but some source types include repeatable nested blocks in their arguments, which do not accept expressions:

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

  tag {
    # but the "tag" block is always a literal block
source "amazon-ebs" "example" {  name = "pkr-test-name" # can use expressions here
  tag {    # but the "tag" block is always a literal block  }}

You can dynamically construct repeatable nested blocks like tag using a special dynamic block type, which is supported anywhere, example:

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

A dynamic block acts much like a for expression, but produces nested blocks instead of a complex typed value. It iterates over a given complex value, and generates a nested block for each element of that complex value.

  • The label of the dynamic block ("tag" in the example above) specifies what kind of nested block to generate.
  • The for_each argument provides the complex value to iterate over.
  • The iterator argument (optional) sets the name of a temporary variable that represents the current element of the complex value. If omitted, the name of the variable defaults to the label of the dynamic block ("tag" in the example above).
  • The labels argument (optional) is a list of strings that specifies the block labels, in order, to use for each generated block. You can use the temporary iterator variable in this value.
  • The nested content block defines the body of each generated block. You can use the temporary iterator variable inside this block.

Since the for_each argument accepts any collection or structural value, you can use a for expression or splat expression to transform an existing collection.

The iterator object (tag in the example above) has two attributes:

  • key is the map key or list element index for the current element. If the for_each expression produces a set value then key is identical to value and should not be used.
  • value is the value of the current element.

A dynamic block can only generate arguments that belong to the source type, data source or provisioner being configured.

The for_each value must be a map or set with one element per desired nested block. If you need to declare resource instances based on a nested data structure or combinations of elements from multiple data structures you can use expressions and functions to derive a suitable value. For some common examples of such situations, see the flatten and setproduct functions.

»Best Practices for dynamic Blocks

Overuse of dynamic blocks can make configuration hard to read and maintain, so we recommend using them only when you need to hide details in order to build a clean user interface for a re-usable code. Always write nested blocks out literally where possible.

»String Literals

HCL has two different syntaxes for string literals. The most common is to delimit the string with quote characters ("), like "hello". In quoted strings, the backslash character serves as an escape sequence, with the following characters selecting the escape behavior:

\rCarriage Return
\"Literal quote (without terminating the string)
\\Literal backslash
\uNNNNUnicode character from the basic multilingual plane (NNNN is four hex digits)
\UNNNNNNNNUnicode character from supplementary planes (NNNNNNNN is eight hex digits)

The alternative syntax for string literals is the so-called Here Documents or "heredoc" style, inspired by Unix shell languages. This style allows multi-line strings to be expressed more clearly by using a custom delimiter word on a line of its own to close the string:


The << marker followed by any identifier at the end of a line introduces the sequence. Packer then processes the following lines until it finds one that consists entirely of the identifier given in the introducer. In the above example, EOF is the identifier selected. Any identifier is allowed, but conventionally this identifier is in all-uppercase and begins with EO, meaning "end of". EOF in this case stands for "end of text".

The "heredoc" form shown above requires that the lines following be flush with the left margin, which can be awkward when an expression is inside an indented block:

block {
  value = <<EOF
block {  value = <<EOFhelloworldEOF}

To improve on this, Packer also accepts an indented heredoc string variant that is introduced by the <<- sequence:

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

In this case, Packer analyses the lines in the sequence to find the one with the smallest number of leading spaces, and then trims that many spaces from the beginning of all of the lines, leading to the following result:

hello  world

Backslash sequences are not interpreted in a heredoc string expression. Instead, the backslash character is interpreted literally.

In both quoted and heredoc string expressions, Packer supports template sequences that begin with ${ and %{. These are described in more detail in the following section. To include these sequences literally without beginning a template sequence, double the leading character: $${ or %%{.

»String Templates

Within quoted and heredoc string expressions, the sequences ${ and %{ begin template sequences. Templates let you directly embed expressions into a string literal, to dynamically construct strings from other values.