website: Elaborate the "expressions" sub-pages in the language section

When we did the earlier documentation rework for Terraform v0.12 we still
had one big "Expressions" page talking about the various operators and
constructs, and so we had to be a bit economical with the details about
some more complicated constructs in order to avoid the page becoming even
more overwhelming.

However, we've recently reorganized the language documentation again so
that the expressions section is split across several separate pages, and
that gives some freedom to go into some more detail about and show longer
examples for certain features.

My changes here are not intended to be an exhaustive rewrite but I did
try to focus on some areas I've commonly seen questions about when helping
in the community forum and elsewhere, and also to create a little more
connectivity between the different content so readers can hopefully find
what they are looking for more easily when they're not yet sure what
terminology to look for.
This commit is contained in:
Martin Atkins 2020-12-04 16:15:54 -08:00
parent 627a2121b8
commit ab1dd87540
7 changed files with 467 additions and 68 deletions

View File

@ -41,3 +41,28 @@ The two result values may be of any type, but they must both
be of the _same_ type so that Terraform can determine what type the whole
conditional expression will return without knowing the condition value.
If the two result expressions don't produce the same type then Terraform will
attempt to find a type that they can both convert to, and make those
conversions automatically if so.
For example, the following expression is valid and will always return a string,
because in Terraform all numbers can convert automatically to a string using
decimal digits:
```hcl
var.example ? 12 : "hello"
```
Relying on this automatic conversion behavior can be confusing for those who
are not familiar with Terraform's conversion rules though, so we recommend
being explicit using type conversion functions in any situation where there may
be some uncertainty about the expected result type.
The following example is contrived because it would be easier to write the
constant `"12"` instead of the type conversion in this case, but shows how to
use [`tostring`](../functions/tostring.html) to explicitly convert a number to
a string.
```hcl
var.example ? tostring(12) : "hello"
```

View File

@ -9,7 +9,8 @@ page_title: "Dynamic Blocks - Configuration Language"
Within top-level block constructs like resources, expressions can usually be
used only when assigning a value to an argument using the `name = expression`
form. This covers many uses, but some resource types include repeatable _nested
blocks_ in their arguments, which do not accept expressions:
blocks_ in their arguments, which typically represent separate objects that
are related to (or embedded within) the containing object:
```hcl
resource "aws_elastic_beanstalk_environment" "tfenvtest" {
@ -42,9 +43,10 @@ resource "aws_elastic_beanstalk_environment" "tfenvtest" {
}
```
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.
A `dynamic` block acts much like a [`for` expression](for.html), 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 (`"setting"` in the example above) specifies
what kind of nested block to generate.
@ -86,6 +88,56 @@ and
[`setproduct`](/docs/configuration/functions/setproduct.html)
functions.
## Multi-level Nested Block Structures
Some providers define resource types that include multiple levels of blocks
nested inside one another. You can generate these nested structures dynamically
when necessary by nesting `dynamic` blocks in the `content` portion of other
`dynamic` blocks.
For example, a module might accept a complex data structure like the following:
```hcl
variable "load_balancer_origin_groups" {
type = map(object({
origins = set(object({
hostname = string
}))
}))
}
```
If you were defining a resource whose type expects a block for each origin
group and then nested blocks for each origin within a group, you could ask
Terraform to generate that dynamically using the following nested `dynamic`
blocks:
```hcl
dynamic "origin_group" {
for_each = var.load_balancer_origin_groups
content {
name = origin_group.key
dynamic "origin" {
for_each = origin_group.value.origins
content {
hostname = origin.value.hostname
}
}
}
}
```
When using nested `dynamic` blocks it's particularly important to pay attention
to the iterator symbol for each block. In the above example,
`origin_group.value` refers to the current element of the outer block, while
`origin.value` refers to the current element of the inner block.
If a particular resource type defines nested blocks that have the same type
name as one of their parents, you can use the `iterator` argument in each of
`dynamic` blocks to choose a different iterator symbol that makes the two
easier to distinguish.
## Best Practices for `dynamic` Blocks
Overuse of `dynamic` blocks can make configuration hard to read and maintain, so
@ -93,3 +145,10 @@ we recommend using them only when you need to hide details in order to build a
clean user interface for a re-usable module. Always write nested blocks out
literally where possible.
If you find yourself defining most or all of a `resource` block's arguments and
nested blocks using directly-corresponding attributes from an input variable
then that might suggest that your module is not creating a useful abstraction.
It may be better for the calling module to define the resource itself then
pass information about it into your module. For more information on this design
tradeoff, see [When to Write a Module](/docs/modules/#when-to-write-a-module)
and [Module Composition](/docs/modules/composition.html).

View File

@ -10,8 +10,8 @@ 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 example, if `var.list` were a list of strings, then the following expression
would produce a tuple of strings with all-uppercase letters:
```hcl
[for s in var.list : upper(s)]
@ -22,10 +22,41 @@ 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.
## Input Types
A `for` expression's input (given after the `in` keyword) can be a list,
a set, a tuple, a map, or an object.
The above example showed a `for` expression with only a single temporary
symbol `s`, but a `for` expression can optionally declare a pair of temporary
symbols in order to use the key or index of each item too:
```hcl
[for k, v in var.map : length(k) + length(v)]
```
For a map or object type, like above, the `k` symbol refers to the key or
attribute name of the current element. You can also use the two-symbol form
with lists and tuples, in which case the additional symbol is the index
of each element starting from zero, which conventionally has the symbol name
`i` or `idx` unless it's helpful to choose a more specific name:
```hcl
[for i, v in var.list : "${i} is ${v}"]
```
The index or key symbol is always optional. If you specify only a single
symbol after the `for` keyword then that symbol will always represent the
_value_ of each element of the input collection.
## Result Types
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:
it produces.
The above example uses `[` and `]`, which produces a tuple. If you use `{` and
`}` instead, the result is an object and you must provide two result
expressions that are separated by the `=>` symbol:
```hcl
{for s in var.list : s => upper(s)}
@ -33,39 +64,146 @@ expressions must be provided separated by the `=>` symbol:
This expression produces an object whose attributes are the original elements
from `var.list` and their corresponding values are the uppercase versions.
For example, the resulting value might be as follows:
```hcl
{
foo = "FOO"
bar = "BAR"
baz = "BAZ"
}
```
A `for` expression alone can only produce either an object value or a tuple
value, but Terraform's automatic type conversion rules mean that you can
typically use the results in locations where lists, maps, and sets are expected.
## Filtering Elements
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:
from the source collection, producing a value with fewer elements than
the source value:
```
[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:
One common reason for filtering collections in `for` expressions is to split
a single source collection into two separate collections based on some
criteria. For example, if the input `var.users` is a map of objects where the
objects each have an attribute `is_admin` then you may wish to produce separate
maps with admin vs non-admin objects:
```
[for k, v in var.map : length(k) + length(v)]
```hcl
variable "users" {
type = map(object({
is_admin = boolean
}))
}
locals {
admin_users = {
for name, user in var.users : name => user
if user.is_admin
}
regular_users = {
for name, user in var.users : name => user
if !user.is_admin
}
}
```
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:
## Element Ordering
```
{for s in var.list : substr(s, 0, 1) => s... if s != ""}
Because `for` expressions can convert from unordered types (maps, objects, sets)
to unordered types (lists, tuples), Terraform must choose an implied ordering
for the elements of an unordered collection.
For maps and objects, Terraform sorts the elements by key or attribute name,
using lexical sorting.
For sets of strings, Terraform sorts the elements by their value, using
lexical sorting.
For sets of other types, Terraform uses an arbitrary ordering that may change
in future versions of Terraform. For that reason, we recommend converting the
result of such an expression to itself be a set so that it's clear elsewhere
in the configuration that the result is unordered. You can use
[the `toset` function](../functions/toset.html)
to concisely convert a `for` expression result to be of a set type.
```hcl
toset([for e in var.set : e.example])
```
For expressions are particularly useful when combined with other language
features to combine collections together in various ways. For example,
the following two patterns are commonly used when constructing map values
to use with
[the `for_each` meta-argument](/docs/configuration/meta-arguments/for_each.html):
## Grouping Results
* Transform a multi-level nested structure into a flat list by
[using nested `for` expressions with the `flatten` function](/docs/configuration/functions/flatten.html#flattening-nested-structures-for-for_each).
* Produce an exhaustive list of combinations of elements from two or more
collections by
[using the `setproduct` function inside a `for` expression](/docs/configuration/functions/setproduct.html#finding-combinations-for-for_each).
If the result type is an object (using `{` and `}` delimiters) then normally
the given key expression must be unique across all elements in the result,
or Terraform will return an error.
Sometimes the resulting keys are _not_ unique, and so to support that situation
Terraform supports a special _grouping mode_ which changes the result to support
multiple elements per key.
To activate grouping mode, add the symbol `...` after the value expression.
For example:
```hcl
variable "users" {
type = map(object({
role = string
}))
}
locals {
users_by_role = {
for name, user in var.users : user.role => name...
}
}
```
The above represents a situation where a module expects a map describing
various users who each have a single "role", where the map keys are usernames.
The usernames are guaranteed unique because they are map keys in the input,
but many users may all share a single role name.
The `local.users_by_role` expression inverts the input map so that the keys
are the role names and the values are usernames, but the expression is in
grouping mode (due to the `...` after `name`) and so the result will be a
map of lists of strings, such as the following:
```hcl
{
"admin": [
"ps",
],
"maintainer": [
"am",
"jb",
"kl",
"ma",
],
"viewer": [
"st",
"zq",
],
}
```
Due to [the element ordering rules](#element-ordering), Terraform will sort
the users lexically by username as part of evaluating the `for` expression,
and so the usernames associated with each role will be lexically sorted
after grouping.
## Repeated Configuration Blocks
The `for` expressions mechanism is for constructing collection values from
other collection values within expressions, which you can then assign to
individual resource arguments that expect complex values.
Some resource types also define _nested block types_, which typically represent
separate objects that belong to the containing resource in some way. You can't
dynamically generated nested blocks using `for` expressions, but you _can_
generate nested blocks for a resource dynamically using
[`dynamic` blocks](dynamic-blocks.html).

View File

@ -30,6 +30,11 @@ min(55, 3453, 2)
A function call expression evaluates to the function's return value.
## Available Functions
For a full list of available functions, see
[the function reference](/docs/configuration/functions.html).
## Expanding Function Arguments
If the arguments to pass to a function are available in a list or tuple value,
@ -43,8 +48,45 @@ min([55, 2453, 2]...)
The expansion symbol is three periods (`...`), not a Unicode ellipsis character
(`…`). Expansion is a special syntax that is only available in function calls.
## Available Functions
## When Terraform Calls Functions
For a full list of available functions, see
[the function reference](/docs/configuration/functions.html).
Most of Terraform's built-in functions are, in programming language terms,
[pure functions](https://en.wikipedia.org/wiki/Pure_function). This means that
their result is based only on their arguments and so it doesn't make any
practical difference when Terraform would call them.
However, a small subset of functions interact with outside state and so for
those it can be helpful to know when Terraform will call them in relation to
other events that occur in a Terraform run.
The small set of special functions includes
[`file`](../functions/file.html),
[`templatefile`](../functions/templatefile.html),
[`timestamp`](../functions/timestamp.html),
and [`uuid`](../functions/uuid.html).
If you are not working with these functions then you don't need
to read this section, although the information here may still be interesting
background information.
The `file` and `templatefile` functions are intended for reading files that
are included as a static part of the configuration and so Terraform will
execute these functions as part of initial configuration validation, before
taking any other actions with the configuration. That means you cannot use
either function to read files that your configuration might generate
dynamically on disk as part of the plan or apply steps.
The `timestamp` function returns a representation of the current system time
at the point when Terraform calls it, and the `uuid` function returns a random
result which differs on each call. Without any special behavior these would
would both cause the final configuration during the apply step not to match the
actions shown in the plan, which violates the Terraform execution model.
For that reason, Terraform arranges for both of those functions to produce
[unknown value](references.html#values-not-yet-known) results during the
plan step, with the real result being decided only during the apply step.
For `timestamp` in particular, this means that the recorded time will be
the instant when Terraform began applying the change, rather than when
Terraform _planned_ the change.
For more details on the behavior of these functions, refer to their own
documentation pages.

View File

@ -30,15 +30,15 @@ in the following order of operations:
1. `&&`
1. `||`
Parentheses can be used to override the default order of operations. Without
parentheses, higher levels are evaluated first, so `1 + 2 * 3` is interpreted
as `1 + (2 * 3)` and _not_ as `(1 + 2) * 3`.
Use parentheses to override the default order of operations. Without
parentheses, higher levels will be evaluated first, so Terraform will interpret
`1 + 2 * 3` as `1 + (2 * 3)` and _not_ as `(1 + 2) * 3`.
The different operators can be gathered into a few different groups with
similar behavior, as described below. Each group of operators expects its
given values to be of a particular type. Terraform will attempt to convert
values to the required type automatically, or will produce an error message
if this automatic conversion is not possible.
if automatic conversion is impossible.
## Arithmetic Operators
@ -53,6 +53,11 @@ as results:
generally useful only when used with whole numbers.
* `-a` returns the result of multiplying `a` by `-1`.
Terraform supports some other less-common numeric operations as
[functions](function-calls.html). For example, you can calculate exponents
using
[the `pow` function](../functions/pow.html).
## Equality Operators
The equality operators both take two values of any type and produce boolean
@ -62,6 +67,18 @@ values as results.
value, or `false` otherwise.
* `a != b` is the opposite of `a == b`.
Because the equality operators require both arguments to be of exactly the
same type in order to decide equality, we recommend using these operators only
with values of primitive types or using explicit type conversion functions
to indicate which type you are intending to use for comparison.
Comparisons between structural types may produce surprising results if you
are not sure about the types of each of the arguments. For example,
`var.list == []` may seem like it would return `true` if `var.list` were an
empty list, but `[]` actually builds a value of type `tuple([])` and so the
two values can never match. In this situation it's often clearer to write
`length(var.list) == 0` instead.
## Comparison Operators
The comparison operators all expect number values and produce boolean values
@ -80,3 +97,7 @@ The logical operators all expect bool values and produce bool values as results.
* `a || b` returns `true` if either `a` or `b` is `true`, or `false` if both are `false`.
* `a && b` returns `true` if both `a` and `b` are `true`, or `false` if either one is `false`.
* `!a` returns `true` if `a` is `false`, and `false` if `a` is `true`.
Terraform does not have an operator for the "exclusive OR" operation. If you
know that both operators are boolean values then exclusive OR is equivalent
to the `!=` ("not equal") operator.

View File

@ -60,24 +60,58 @@ For more information about how to use resource references, see
`var.<NAME>` is the value of the [input variable](/docs/configuration/variables.html) of the given name.
If the variable has a type constraint (`type` argument) as part of its
declaration, Terraform will automatically convert the caller's given value
to conform to the type constraint.
For that reason, you can safely assume that a reference using `var.` will
always produce a value that conforms to the type constraint, even if the caller
provided a value of a different type that was automatically converted.
In particular, note that if you define a variable as being of an object type
with particular attributes then only _those specific attributes_ will be
available in expressions elsewhere in the module, even if the caller actually
passed in a value with additional attributes. You must define in the type
constraint all of the attributes you intend to use elsewhere in your module.
### Local Values
`local.<NAME>` is the value of the [local value](/docs/configuration/locals.html) of the given name.
Local values can refer to other local values, even within the same `locals`
block, as long as you don't introduce circular dependencies.
### Child Module Outputs
* `module.<MODULE NAME>.<OUTPUT NAME>` is the value of the specified
[output value](/docs/configuration/outputs.html) from a
[child module](/docs/configuration/blocks/modules/index.html) called by the
current module.
`module.<MODULE NAME>` is an value representing the results of
[a `module` block](../blocks/modules/).
If the corresponding `module` block does not have either `count` nor `for_each`
set then the value will be an object with one attribute for each output value
defined in the child module. To access one of the module's
[output values](../outputs.html), use `module.<MODULE NAME>.<OUTPUT NAME>`.
If the corresponding `module` uses `for_each` then the value will be a map
of objects whose keys correspond with the keys in the `for_each` expression,
and whose values are each objects with one attribute for each output value
defined in the child module, each representing one module instance.
If the corresponding module uses `count` then the result is similar to for
`for_each` except that the value is a _list_ with the requested number of
elements, each one representing one module instance.
### Data Sources
* `data.<DATA TYPE>.<NAME>` is an object representing a
[data resource](/docs/configuration/data-sources.html) of the given data
source type and name. If the resource has the `count` argument set, the value
is a list of objects representing its instances. If the resource has the `for_each`
argument set, the value is a map of objects representing its instances.
`data.<DATA TYPE>.<NAME>` is an object representing a
[data resource](/docs/configuration/data-sources.html) of the given data
source type and name. If the resource has the `count` argument set, the value
is a list of objects representing its instances. If the resource has the `for_each`
argument set, the value is a map of objects representing its instances.
For more information, see
[References to Resource Attributes](#references-to-resource-attributes), which
also applies to data resources aside from the addition of the `data.` prefix
to mark the reference as for a data resource.
### Filesystem and Workspace Info
@ -91,6 +125,36 @@ For more information about how to use resource references, see
* `terraform.workspace` is the name of the currently selected
[workspace](/docs/state/workspaces.html).
Use the values in this section carefully, because they include information
about the context in which a configuration is being applied and so may
inadvertently hurt the portability or composability of a module.
For example, if you use `path.cwd` directly to populate a path into a resource
argument then later applying the same configuration from a different directory
or on a different computer with a different directory structure will cause
the provider to consider the change of path to be a change to be applied, even
if the path still refers to the same file.
Similarly, if you use any of these values as a form of namespacing in a shared
module, such as using `terraform.workspace` as a prefix for globally-unique
object names, it may not be possible to call your module more than once in
the same configuration.
Aside from `path.module`, we recommend using the values in this section only
in the root module of your configuration. If you are writing a shared module
which needs a prefix to help create unique names, define an input variable
for your module and allow the calling module to define the prefix. The
calling module can then use `terraform.workspace` to define it if appropriate,
or some other value if not:
```hcl
module "example" {
# ...
name_prefix = "app-${terraform-workspace}"
}
```
### Block-Local Values
Within the bodies of certain blocks, or in some other specific contexts,
@ -110,6 +174,12 @@ _temporary variables_ in their documentation. These are not [input
variables](/docs/configuration/variables.html); they are just arbitrary names
that temporarily represent a value.
The names in this section relate to top-level configuration blocks only.
If you use [`dynamic` blocks](dynamic-blocks.html) to dynamically generate
resource-type-specific _nested_ blocks within `resource` and `data` blocks then
you'll refer to the key and value of each element differently. See the
`dynamic` blocks documentation for details.
## Named Values and Dependencies
Constructs like resources and module calls often use references to named values

View File

@ -39,34 +39,75 @@ The above expression is equivalent to the following `for` expression:
[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`](/docs/configuration/meta-arguments/for_each.html#referring-to-instances),
which are represented as maps in Terraform). 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.
## Splat Expressions with Maps
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 splat expression patterns shown above apply only to lists, sets, and
tuples. To get a similar result with a map or object value you must use
[`for` expressions](for.html).
```hcl
aws_instance.example[*].id
Resources that use the `for_each` argument will appear in expressions as a map
of objects, so you can't use splat expressions with those resources.
For more information, see
[Referring to Resource Instances](/docs/configuration/meta-arguments/for_each.html#referring-to-instances).
## Single Values as Lists
Splat expressions have a special behavior when you apply them to a value that
isn't a list, set, or tuple.
If the value is anything other than a null value then the splat expression will
transform it into a single-element list, or more accurately a single-element
tuple value. If the value is _null_ then the splat expression will return an
empty tuple.
This special behavior can be useful for modules that accept optional input
variables whose default value is `null` to represent the absense of any value,
to adapt the variable value to work with other Terraform language features that
are designed to work with collections. For example:
```
variable "website" {
type = object({
index_document = string
error_document = string
})
default = null
}
resource "aws_s3_bucket" "example" {
# ...
dynamic "website" {
for_each = var.website[*]
content {
index_document = website.value.index_document
error_document = website.value.error_document
}
}
}
```
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.
The above example uses a [`dynamic` block](dynamic-blocks.html), which
generates zero or more nested blocks based on a collection value. The input
variable `var.website` is defined as a single object that might be null,
so the `dynamic` block's `for_each` expression uses `[*]` to ensure that
there will be one block if the module caller sets the website argument, or
zero blocks if the caller leaves it set to null.
This special behavior of splat expressions is not obvious to an unfamiliar
reader, so we recommend using it only in `for_each` arguments and similar
situations where the context implies working with a collection. Otherwise,
the meaning of the expression may be unclear to future readers.
## Legacy (Attribute-only) Splat Expressions
An older variant of the splat expression is available for compatibility with
code written in older versions of the Terraform language. This is a less useful
version of the splat expression, and should be avoided in new configurations.
Earlier versions of the Terraform language had a slightly different version
of splat expressions, which Terraform continues to support for backward
compatibility. This older variant is less useful than the modern form described
above, and so we recommend against using it in new configurations.
An "attribute-only" splat expression is indicated by the sequence `.*` (instead
of `[*]`):
The legacy "attribute-only" splat expressions use the sequence `.*`, instead of
`[*]`:
```
var.list.*.interfaces[0].name
@ -81,4 +122,7 @@ This form has a subtly different behavior, equivalent to the following
Notice that with the attribute-only splat expression the index operation
`[0]` is applied to the result of the iteration, rather than as part of
the iteration itself.
the iteration itself. Only the attribute lookups apply to each element of
the input. This limitation was confusing some people using older versions of
Terraform and so we recommend always using the new-style splat expressions,
with `[*]`, to get the more consistent behavior.