WintelGuy.com

Terraform Associate Exam Cram - Part 4

Terraform Configuration

This is the Part 4 of the Terraform Associate 004 Exam Cram. It covers the following Terraform Associate Certification exam objectives:

4a. Use and Differentiate resource and data blocks

Resources (resource)

Terraform creates and manages infrastructure with the help of resource blocks. All resources declared by resource blocks are known as managed resources.

resource "aws_instance" "web" { ami = "ami-12345" instance_type = "t2.micro" # ... }

A resource block declares a resource of a given type (aws_instance) with a given local name (web). The resource type and name together serve as an identifier for a given resource and must be unique within a module (aws_instance.web).

Within the block body (between { and }) are the configuration arguments for the resource itself. Most arguments in this section depend on the specific resource type.

There are also some meta-arguments that are defined by Terraform itself and apply across all resource types:

  • depends_on - specifies explicit dependencies, must be a list of references to other resources or child modules in the same calling module.
  • count - creates multiple resource instances based on a count.
  • for_each - creates multiple instances from a map or set of strings.
  • provider - selects a non-default provider configuration.
  • lifecycle - customizes resource lifecycle behavior. The arguments available within a lifecycle block are:
    • create_before_destroy - Terraform creates a replacement resource before destroying the current resource.
    • prevent_destroy - Terraform rejects operations to destroy the resource and returns an error.
    • ignore_changes - Specifies a list of resource attributes that Terraform ignores changes to. Otherwise, Terraform attempts to update the actual resource to match the configuration.
    • replace_triggered_by - Terraform replaces the resource when any of the referenced resources or specified attributes change.
    • precondition - Specifies a condition that Terraform evaluates before creating the resource.
    • postcondition - Specifies a condition that Terraform evaluates after creating the resource.
  • provisioner - performs extra actions on the local machine or on a remote machine after resource creation or before destruction. Built-in provisioners:
    • file - copies files or directories from the local machine to the newly created resource.
    • local-exec - invokes a local executable.
    • remote-exec - invokes a script on a remote resource.

count

The count meta-argument accepts a whole number and creates that many instances of a resource or module. When count is used, the block's address refers to a list of objects, with each object representing a distinct instance of a resource or module.

Terraform also provides a special attribute available within the block, count.index, which indicates the index number (starting from 0) of the instance currently being processed in the count loop. count.index can be used to modify the configuration of each instance, for example, by incorporating the index value into names, tags, etc.

resource "aws_instance" "web" { count = 2 # create two similar EC2 instances ami = "ami-a1234567" instance_type = "t2.micro" tags = { name = "web-${count.index + 1}" } }

In this example, the resource address aws_instance.web refers to a list containing two objects, which can be individually accessed as aws_instance.web[0] and aws_instance.web[1].

If count = 0, Terraform will not create any instances of the resource or module. By combining count with a conditional expression, you can control whether the resource is created. For example, count = var.create_bucket ? 1 : 0 instructs Terraform to create the bucket only when the create_bucket variable is true.

for_each

The for_each meta-argument accepts a map or a set of strings and creates an instance for each item in that map or set. When for_each is used, the block's address refers to a map of objects, with each object representing a distinct instance of a resource or module. Terraform links each map key (or set element) from the value provided to for_each with the specific instance processed in the for_each loop.

Terraform also provides two special attributes available within the block: each.key and each.value.

  • If for_each value is a map, each.key represents the current map key, and each.value represents the corresponding value.
  • If a set is used, each.key and each.value are identical, both representing the current set element.

The each.key and each.value attributes can be used to modify the configuration of each instance, for example, by incorporating the values into names, tags, or other attributes.

locals { web_servers = { primary = "t2.small" secondary = "t2.micro" } } resource "aws_instance" "web" { for_each = local.web_servers ami = "ami-a1234567" instance_type = each.value tags = { name = "web-${each.key}" } }

In this example, the resource address aws_instance.web refers to a map containing two objects, which can be individually accessed as aws_instance.web["primary"] and aws_instance.web["secondary"].

Note: A given resource or module block cannot use both count and for_each.

Provisioners (provisioner)

The file provisioner copies files or directories from the machine running Terraform to the newly created resource. It supports both ssh and winrm type connections. The file provisioner accepts the following arguments:

  • source - the source file or directory. Cannot be combined with content.
  • content - the direct content to copy to the destination. If the destination file does not exist, the content will be written to a file named tf-file-content created inside the target directory. Cannot be combined with source.
  • destination - the destination path to write to on the remote system.

The local-exec provisioner invokes a local executable (on the machine running Terraform) after a resource is created. It supports the following arguments:

  • command - the command to execute.
  • working_dir - specifies the working directory where command will be executed.
  • interpreter - a list of interpreter arguments used to execute the command. The first argument is the interpreter itself. The remaining arguments are appended prior to the command. If unspecified, sensible defaults will be chosen based on the system OS.
  • environment - a block of key value pairs representing the environment of the executed command.

The remote-exec provisioner invokes a script on a remote resource after it is created. It requires a connection and supports both ssh and winrm. The remote-exec provisioner accepts the following arguments (only one can be used per block):

  • inline - list of command strings. They are executed in the order they are provided.
  • script - a path to a local script that will be copied to the remote resource and then executed.
  • scripts - a list of paths to local scripts that will be copied to the remote resource and then executed. They are executed in the order they are provided.

If multiple provisioners are specified within a resource, they are executed in the order they're defined in the configuration file.

All provisioners support the when and on_failure meta-arguments.

Creation-time provisioners (the default behavior) run after the resource they are defined within is created and not during update or any other lifecycle. If a creation-time provisioner fails, the resource is marked as tainted. A tainted resource will be planned for destruction and recreation upon the next terraform apply.

Destroy-time provisioners (with when = destroy) run before the resource they are defined within is destroyed. If they fail, Terraform will error and rerun the provisioners again on the next terraform apply.

By default, provisioners that fail will also cause the Terraform apply itself to fail. The on_failure argument can change this behavior. The allowed values are:

  • continue - Ignore the error and continue with creation or destruction.
  • fail - Raise an error and stop applying (the default behavior). If this is a creation-time provisioner, mark the resource as tainted.

Expressions in provisioner blocks cannot refer to their parent resource by name. Instead, they can use the special self object which represents the provisioner's parent resource, and has all of that resource's attributes.

Most provisioners require access to the remote resource via SSH or WinRM and expect a connection block with details about how to connect. Connection blocks don't take a block label and can be nested within either a resource or a provisioner.

  • A connection block nested directly within a resource affects all of that resource's provisioners.
  • A connection block nested in a provisioner block only affects that provisioner and overrides any resource-level connection settings.

It is possible (but not recommended) to use third-party provisioners as plugins, by placing them in %APPDATA%\terraform.d\plugins, ~/.terraform.d/plugins, or the same directory where the Terraform binary is installed.

To run provisioners that are not directly associated with a specific resource, use a null_resource. Utilize its triggers argument and any meta-arguments to create necessary resource dependencies. triggers accepts a map of arbitrary strings that, when changed, will force the null_resource to be replaced, re-running any associated provisioners.

Data sources (data)

Data sources allow Terraform to use information defined outside of Terraform, defined by another separate Terraform configuration, or modified by functions. All data sources are essentially a read only subset of resources.

A data source is accessed via a special kind of resource known as a data resource, declared using a data block:

data "aws_ami" "ubuntu" { most_recent = true owners = ["099720109477"] # Canonical # ... }

A data block requests that Terraform read from a given data source (aws_ami) and export the result under the given local name (ubuntu).

The data source and name together serve as an identifier for a given resource and must be unique within a module (data.aws_ami.ubuntu). Exported attributes can be accessed using the following form: data.<TYPE>.<NAME>.<ATTRIBUTE>.

Within the block body (between { and }) is configuration for the data instance. Most arguments in this section depend on the specific data source.

data blocks accept the same meta-arguments as resource blocks with the exception of the lifecycle configuration block.

Back to Top

4b. Refer to Resource Attributes and Create Cross-Resource References

A resource address is a string that identifies zero or more resource instances in overall Terraform configuration.

Input Variables

var.<NAME> is the value of the input variable of the given name.

Local Values

local.<NAME> is the value of the local value of the given name.

Resources

<RESOURCE_TYPE>.<NAME> represents a managed resource of the given type and name. Its value is:

  • an object representing a single instance, if the resource does not use count or for_each
  • a list of objects, if the resource has the count meta-argument; individual objects can be accessed using the square-bracket index notation, e.g., aws_instance.web[1]
  • a map of objects, if the resource has the for_each meta-argument; individual objects can be accessed using the square-bracket index notation, for example: aws_instance.web["main"]

The resource's attributes are elements of the object and can be accessed using dot or square bracket notation. For example:

  • aws_instance.web.id - accessing id of a single-instance resource
  • aws_instance.web[1].id - resource using count, accessing id of the second instance (index starts at 0)
  • aws_instance.web["main"].id - resource using for_each, accessing id of the instance with key "main"

Data Sources

data.<DATA_TYPE>.<NAME> is an object representing a data resource of the given data source type and name. Its value is:

  • an object representing a single instance, if the date resource does not use count or for_each
  • a list of objects, if the date resource has the count meta-argument; individual objects can be accessed using the square-bracket index notation
  • a map of objects, if the date resource has the for_each meta-argument; individual objects can be accessed using the square-bracket index notation

The data resource's attributes are elements of the object and can be accessed using dot-separated attribute notation, e.g., data.<DATA_TYPE>.<NAME>.<ATTRIBUTE_NAME>.

Child Module Outputs

module.<MODULE_NAME> is a value representing the results of a module block. Its value is:

  • an object representing a single module instance, if the corresponding module block does not have neither count nor for_each set
  • a list of objects, each representing one module instance, if the resource has the count meta-argument; individual objects can be accessed using the square-bracket index notation
  • a map of objects, each representing one module instance, if the resource has the for_each meta-argument; individual objects can be accessed using the square-bracket index notation

The module's outputs are elements of the object (one attribute for each output value defined in the module) and can be accessed using dot-separated attribute notation, e.g., module.<MODULE_NAME>.<OUTPUT_NAME>.

Connecting Resources

Constructs like resources and module blocks often use references to other resources and variables. Terraform analyzes these expressions to automatically builds dependencies between objects. An expression in a resource argument that refers to another managed resource creates an implicit dependency between the two resources. In following example, aws_eip.example depends on aws_instance.web:

resource "aws_eip" "example" { instance = aws_instance.web.id # ... }

If needed, explicit dependencies can be created using the depends_on argument which can be added to any resource or module block and accepts a list of resources to create explicit dependencies for: depends_on = [aws_s3_bucket.example, aws_instance.example].

Back to Top

4c. Use Variables and Outputs

Variables (variable)

Input variables let you customize aspects of Terraform modules without altering the module's own source code. This allows you to share modules across different Terraform configurations, making your module composable and reusable.

Each input variable accepted by a module must be declared using a variable block:

variable "region" { type = string default = "us-east-1" }

The label after the variable keyword is a name for the variable, which must be unique among all variables in the same module. This name is used to assign a value to the variable from outside and to reference the variable's value from within the module.

The name of a variable can be any valid identifier except the following: source, version, providers, count, for_each, lifecycle, depends_on, locals.

Optional arguments accepted by variable blocks:

  • default - A default value which, if provided, makes the variable optional.
  • type - This argument specifies what value types are accepted for the variable (type constraint). If no type constraint is set then a value of any type is accepted.
  • description - This specifies the input variable's description.
  • validation - A block to define validation rules, usually in addition to type constraints.
  • sensitive - Masks variable's value in Terraform output.
  • nullable - Specify if the variable can be null.

Variable Types

Type constraints are created from a mixture of type keywords and type constructors. The supported type keywords are:

  • string
  • number
  • bool

The type constructors allow you to specify complex types such as collection and structural types:

  • list(<TYPE>)
  • set(<TYPE>)
  • map(<TYPE>)
  • object({<ATTR NAME> = <TYPE>, ... })
  • tuple([<TYPE>, ...])

Using Input Variable Values

Within the module that declared a variable, its value can be accessed as var.<NAME>, where <NAME> matches the label given in the declaration block, for example: var.region.

The value assigned to a variable can only be accessed in expressions within the module where it was declared.

Assigning Values to Root Module Variables

When variables are declared in the root module of your configuration, they can be set in a number of ways:

  • In a HCP Terraform workspace.
  • Individually, with the -var command line option.
  • In variable definitions (.tfvars or.tfvars.json) files, specified with the -var-file command line option.
  • In automatically loaded variable definitions files (terraform.tfvars or terraform.tfvars.json or any files with names ending in .auto.tfvars or .auto.tfvars.json).
  • As environment variables (TF_VAR_<NAME>).

Variable Definition Precedence

Terraform loads variables in the following order, with later sources taking precedence over (overwriting) earlier ones:

  • Environment variables
  • The terraform.tfvars file, if present.
  • The terraform.tfvars.json file, if present.
  • Any *.auto.tfvars or *.auto.tfvars.json files, processed in lexical order of their filenames.
  • Any -var and -var-file options on the command line, in the order they are provided.

Assigning Values to Child Module Variables

Input variables values for a child module are assigned within the module block in the configuration of the parent module.

For example:

# vpc module variable "vpc_name" { type = string } variable "cidr_block" { type = string } # ...
# root module module "vpc" { source = "./modules/vpc" vpc_name = “example_vpc” cidr_block = "10.0.0.0/16" } # ...

Outputs (output)

Output values have several uses:

  • A child module can use outputs to expose a subset of its resource attributes to a parent module. Useful for chaining modules.
  • A root module can use outputs to print certain values in the CLI output after running terraform apply. Outputs are only rendered when Terraform applies your plan. Running terraform plan will not render outputs.
  • When using remote state, root module outputs can be accessed by other configurations via a terraform_remote_state data source.

Output values exported by a module must be declared using output blocks:

output "instance_ip" { value = aws_instance.web.public_ip }

The output block supports the following arguments:

  • value - The value Terraform returns for this output (Required).
  • description - A description of the output's purpose and how to use it.
  • sensitive - Specifies if Terraform hides this value in CLI output.
  • ephemeral - Specifies whether to prevent storing this value in state (Terraform v1.10 and later).
  • depends_on - A list of explicit dependencies for this output.
  • precondition - A condition to validate before computing the output or storing it in state. Additional attributes:
    • condition - Expression that must return true for Terraform to proceed with an operation.
    • error_message - Message to display if the condition evaluates to false.

Terraform stores output values in the configuration's state file.

In a parent module, outputs of child modules are available in expressions as module.<MODULE_NAME>.<OUTPUT_NAME>.

Use the terraform output [options] [OUTPUT_NAME] command to print the outputs. If OUTPUT_NAME is not specified, all outputs are printed.

Options:

  • -state=path - Path to the state file to read. Defaults to terraform.tfstate. Ignored when remote state is used.
  • -no-color - If specified, output won't contain any color.
  • -json - If specified, machine-readable output will be printed in JSON format.
  • -raw - For value types that can be automatically converted to a string, will print the raw string directly, rather than a human-oriented representation of the value.

Sensitive outputs

Terraform will redact outputs marked with sensitive = true when planning, applying, or destroying a configuration, or when querying all outputs with terraform output. Terraform will not redact sensitive outputs in other cases, such as when querying a specific output by name (terraform output <OUTPUT_NAME>), querying all outputs in JSON format (terraform output -json), or when using outputs from a child module in the root module.

Terraform stores all output values, including those marked as sensitive, as plain text in the state file.

Local Values (locals)

A local value assigns a name to a value or an expression and helps avoiding repetition of the same expression multiple times in a module.

Example:

locals { environment = "dev" vpc_name = "${local.environment}-vpc" } resource "google_compute_network" "vpc_network" { name = local.vpc_name auto_create_subnetworks = false }

Local values can be referred in expressions as local.<NAME>, e.g., local.environment, local.vpc_name.

Interpolation

A ${ ... } sequence is an interpolation, which evaluates the expression given between the brackets, converts the result to a string if necessary, and then inserts it into the final string, for example: name = "server-${var.id}"

Back to Top

4d. Understand and Use Complex Types

Primitive Types: string, number, bool.

Complex Types

A complex type is a type that groups multiple values into a single value. There are two categories of complex types: collection types (for grouping similar values), and structural types (for grouping potentially dissimilar values).

Collection Types

A collection type allows multiple values of one other type to be grouped together as a single value. The type of value within a collection is called its element type. All collection types must have an element type, which is provided as the argument to their constructor. All elements of a collection must always be of the same type.

  • list(<TYPE>):
    • an ordered sequence of values identified by consecutive whole numbers starting with zero.
    • can be represented by a pair of square brackets containing a comma-separated sequence of values: ["a", "15", "true"].
    • values can be accessed using the square-bracket index notation: var.list[3].
  • map(<TYPE>):
    • a collection of values where each is identified by a string label (key/value pairs).
    • can be represented by a pair of curly braces containing a series of <KEY> = <VALUE> pairs, separated by either a comma or a line break: { "foo": "bar", "bar": "baz" } or { foo = "bar", bar = "baz" }.
    • values can be accessed using the square-bracket index notation: var.map["key"], or the dot-separated attribute notation: var.map.key.
  • set(<TYPE>):
    • a collection of unique values that do not have any secondary identifiers or ordering.

Structural Types

A structural type allows multiple values of several distinct types to be grouped together as a single value. Structural types require a schema as an argument, to specify which types are allowed for which elements.

  • object({<ATTR NAME> = <TYPE>, ... }):
    • a collection of named attributes that each have their own type.
    • can be represented by a pair of curly braces containing a series of <KEY> = <VALUE> pairs, separated by either a comma or a line break.
    • values can be accessed using the square-bracket index notation: var.object["key"], or the dot-separated attribute notation: var.object.key.
  • tuple([<TYPE>, ... ]):
    • a sequence of elements identified by consecutive whole numbers starting with zero, where each element has its own type.
    • can be represented by a pair of square brackets containing a comma-separated sequence of values: ["a", 15, true].
    • values can be accessed using the square-bracket index notation: var.tuple[3].

Whenever possible, Terraform automatically converts between similar complex types. It also provides a number of type conversion functions:

  • tobool - converts its argument to a boolean value.
  • tolist - converts its argument to a list value.
  • tomap - converts its argument to a map value.
  • tonumber - converts its argument to a number value.
  • toset - converts its argument to a set value.
  • tostring - converts its argument to a string value.
  • type - returns the type of a given value.

Back to Top

4e. Write Dynamic Configuration Using Expressions and Functions

Terraform's configuration language is based on a more general language called HashiCorp Configuration Language (HCL). The main purpose of the Terraform language is to create declarative configurations that represent infrastructure objects.

The syntax of the Terraform language consists of only a few basic elements:

  • Blocks are containers for other content and usually represent the configuration of some kind of object, like a resource. Blocks have a block type, can have zero or more labels, and have a body that contains any number of arguments and nested blocks. Most of Terraform's features are controlled by top-level blocks in a configuration file.
  • Arguments assign a value to a name. They appear within blocks.
  • Expressions represent a value, either literally or by referencing and combining other values. They appear as values for arguments, or within other expressions.

While the configuration language is not a programming language, you can use several built-in functions to perform operations dynamically.

A Terraform configuration is a complete document that tells Terraform how to manage a given collection of infrastructure. A configuration can consist of multiple files and directories.

The ordering of blocks and the files they are organized into are generally not significant. Terraform only considers implicit and explicit relationships (dependencies) between resources when determining an order of operations.

Terraform expects native syntax for files named with a .tf suffix, and JSON syntax for files named with a .tf.json suffix.

Arguments

An argument assigns a value to a particular name: image_id = "abc123". The identifier before the equals sign is the argument name, and the expression after the equals sign is the argument's value.

Blocks

A block is a container for other content:

resource "aws_instance" "example" { ami = "abc123" network_interface { # ... } # ... }

A block has:

  • a type (resource in this example)
  • labels (aws_instance and example) - The number of labels is determined by the block type. The resource block type expects two labels: the resource type (aws_instance) and an arbitrary name (example).
  • a body (delimited by the { and } characters) - Within the block body, further arguments and blocks may be nested, creating a hierarchy of blocks and their associated arguments.

Identifiers

Argument names, block type names, and the names of most Terraform-specific constructs like resources, input variables, etc. are all identifiers. Identifiers can contain letters, digits, underscores (_), and hyphens (-). The first character of an identifier must not be a digit, to avoid ambiguity with literal numbers.

Built-in Values

  • path.module - the filesystem path of the module.
  • path.root - the filesystem path of the root module.
  • path.cwd - the filesystem path of the original working directory from where you ran Terraform before applying any -chdir argument.
  • terraform.workspace - the name of the currently selected workspace.

Expressions

Expressions refer to or compute values within a configuration, including references to variables or to data exported by resources, arithmetic operations, conditional evaluations, function calls, etc.

Please refer to the official Terraform documentation for more details about Terraform's expression syntax:

  • Types and Values - describes Terraform data types and value syntax.
  • Strings and Templates - explains string syntax & templates:
    • heredoc - <<EOT ... EOT and <<-EOT ... EOT
    • interpolation - ${ ... }
    • directives - %{if <BOOL>} ... %{else} ... %{endif} and %{for <NAME> in <COLLECTION>} ... %{endfor}
  • References to Values - shows how to refer to variables and resource attributes.
  • Operators - describes available arithmetic, comparison, and logical operators: !, - (multiplication by -1), *, /, %, +, - (subtraction), >, >=, <, <=, ==, !=, &&, ||.
  • Function Calls - explains syntax for calling built-in functions.
  • Conditional Expressions - describes expressions that return values based on a condition (<CONDITION> ? <TRUE_VAL> : <FALSE_VAL>).
  • For Expressions - explains how to transform complex data types using iteration.
  • Splat Expressions - shows how to extract simpler collections from more complicated expressions.
  • Dynamic Blocks - describes creating multiple repeatable nested blocks within a resource or other construct.
  • Validate your configuration - explains how to verify variable conditions, check blocks, and resource preconditions and postconditions.
  • Type Constraints - defines syntax for specifying accepted variable types.
  • Version Constraints - explains syntax for restricting allowed software versions.

for Expressions

A for expression performs transformation between complex types. It accepts any collection or structural type (a list, a set, a tuple, a map, or an object) as an input and uses an arbitrary user-defined expression to generate either a tuple (with [ ]) or an object (with { }) as an output.

The following examples show various for expression options, determined by the input and output types:

[for v in var.input_var : "Value: ${v}"] - This for expression perform the following:

  • iterates over each element of var.input_var
  • assigns the value of each respective element to the temporary symbol v
  • evaluates the expression "Value: ${v}" for each v
  • returns a tuple value with evaluation results as elements

[for k, v in var.input_var : "Key: ${k}, Value: ${v}"] - This example illustrates how to use for with two temporary symbols iterating over a map or an object. The temporary symbols k and v are set to the key/attribute and the value of each element of var.input_var, respectively. The for expression then evaluates "Key: ${k}, Value: ${v}" for each k and v pair and returns a tuple value with evaluation results as elements.

[for i, v in var.input_var : "Index: ${i}, Value: ${v}"] - This for expression is similar to the one above but it iterates over a list or a tuple. It sets i to the index and v to the value of each respective element. It then evaluates the expression "Index: ${i}, Value: ${v}" and includes the results into the output tuple.

{for i, v in var.input_var : "key-${i}" => "Value: ${v}"} - This for expression returns an object. It uses two temporary symbols i and v and evaluates two expressions separated by =>. "key-${i}" defines the attributes of the output object, and "Value: ${v}" - the corresponding values.

[for v in var.regions : v if can(regex("^us", v))] - A for expression can include an optional if clause to filter elements from the source collection. In this example, Terraform iterates over the list of regions (var.regions) and returns only those whose names start with "us" as determined by the if can(regex("^us", v)) clause.

{for k, v in var.input_var : v => k...} - This for expression operates in grouping mode activated by adding ... after the value expression. In this example, for returns an object where each attribute contains a tuple value, with one or more elements each. Note: Grouping cannot be used when building a tuple.

Splat Expressions

A splat expression is identified by the special symbol [*] and can be used only with lists, sets, and tuples. It iterates over all of the element of the object given to left of [*] and returns a list or a tuple containing the respective values of the attribute given on the right of [*]. For example:

variable "list" { type = list(object({ id : string })) default = [{ id = "a" }, { id = "b" }] } locals { list_splat = var.list[*].id }
$ terraform console > var.list tolist([ { "id" = "a" }, { "id" = "b" }, ]) > local.list_splat tolist([ "a", "b", ]) >

The splat expression does not work with maps or objects, use for expressions in such cases.

Functions

The Terraform language has a number of built-in functions that can be used in expressions to transform and combine values:

  • Numeric Functions - min(), max(), ceil(), parseint() ...
  • String Functions - format(), join(), trim(), substr(), regex() ...
  • Collections Functions - length(), contains(), sort(), merge(), values() ...
  • Encoding Functions - base64encode(), base64decode(), jsondecode() ...
  • Filesystem Functions - file(), fileexists() ...
  • Date and Time Functions - timestamp(), timeadd() ...
  • Hash and Crypto Functions - md5(), bcrypt(), sha256(), uuid() ...
  • IP Network Functions - cidrhost(), cidrsubnet(), cidrnetmask() ...
  • Type Conversion Functions - can(), tostring(), try(), tolist() ...

Please refer to the official Terraform documentation for more details about Terraform's functions.

Note: The Terraform language does not support user-defined functions, and so only the functions built in to the language are available for use.

Dynamic Blocks

A dynamic block lets you programmatically generate multiple repeatable nested blocks of the same type, using input data structured as a set or map. Dynamic blocks can be added to resource, data, provider, and provisioner blocks.

resource "resource_type" "resource_name" { # ... dynamic "block_name" { for_each = <collection> iterator = <iterator_name> # optional, defaults to block_name content { # defines the body of each generated block # block content using iterator_name or block_name } } }

The iterator argument (optional) sets the name of a temporary variable that represents the current element of the value assigned to for_each. If omitted, the name of the variable defaults to the label (block_name) of the dynamic block.

Terraform also provides two special attributes available within the block: <iterator_name>.key and <iterator_name>.value.

  • If for_each value is a map, <iterator_name>.key represents the current map key, and <iterator_name>.value represents the corresponding value.
  • If a set is used, <iterator_name>.key and <iterator_name>.value are identical, both representing the current set element.

The <iterator_name>.key and <iterator_name>.value attributes can be used to modify the configuration of each nested block.

Back to Top

4f. Define Resource Dependencies in Configuration

Terraform automatically builds a resource dependency graph from the configuration. It uses this graph to determine the correct order for creating resources, rather than relying on the order in which they appear in the configuration files. Terraform creates resources in parallel when no dependency exists.

Explicit dependency between resources can be added with the depends_on meta-argument accepted by any resource or module block. depends_on must be assigned a list of references to other resources or child modules in the same calling module, for example: depends_on = [aws_s3_bucket.example, aws_instance.example].

In addition, the lifecycle settings can customize how Terraform constructs and traverses the dependency graph. Support for each lifecycle rule varies across Terraform configuration blocks. Depending on the block you are configuring, you may be able to use one or more of the following rules.

resource "<TYPE>" "<LABEL>" { # ... depends_on = [ <RESOURCE.ADDRESS.EXPRESSION> ] lifecycle { create_before_destroy = <true || false> prevent_destroy = <true || false> ignore_changes = [ <ATTRIBUTE> ] replace_triggered_by = [ <RESOURCE.ADDRESS.EXPRESSION> ] precondition { # ... } postcondition { # ... } } }
  • create_before_destroy: Terraform creates a replacement resource before destroying the current resource. Terraform propagates and applies create_before_destroy behavior to all resource dependencies.
  • prevent_destroy: Terraform rejects operations to destroy the resource and returns an error. Use this rule as protection against accidental deletions.
  • ignore_changes: Lists resource attributes that Terraform ignores changes to. Otherwise, Terraform attempts to update the actual resource to match the configuration. Use the ignore_changes when you want to retain configuration changes made by a process outside of Terraform.
  • replace_triggered_by: Terraform replaces the resource when any of the referenced resources or specified attributes change.

The terraform graph [options] command produces a representation of the dependency graph between different objects in the current configuration or execution plan. The output is in the DOT format, which can be used by GraphViz to generate charts.

Options:

  • -plan=tfplan - Render graph using the specified plan file instead of the configuration in the current directory.
  • -draw-cycles - Highlight any cycles in the graph with colored edges. This helps when diagnosing cycle errors.
  • -type=plan - Type of graph to output. Can be: plan, plan-refresh-only, plan-destroy, or apply. By default, Terraform chooses plan, or apply based on the -plan=... option.
  • -module-depth=n - (deprecated) In prior versions of Terraform, specified the depth of modules to show in the output.

Back to Top

4g. Validate Configuration Using Custom Conditions

Terraform offers several ways of validating configuration:

  • Input variable validations verify your configuration's parameters when Terraform creates a plan.
  • Preconditions ensure individual resources, data sources, and outputs meet your requirements before Terraform tries to create them.
  • Postconditions verifies that Terraform produced your resources and data sources with the expected and desired settings.
  • Checks let you validate that your resources work as expected without blocking Terraform operations based on the check's result.

Input variable validations

Include a validation sub-block into a variable definition block to ensure that a variable value meets your specific requirements.

variable "var_name" { validation { condition = <conditional_expression> error_message = "<message>" } }
  • condition - Expression that must evaluate to true for Terraform to proceed with an operation.
  • error_message - Message to display if the condition evaluates to false.

Terraform evaluates variable validations while it creates a plan, and if a validation block's condition expression evaluates to false, then Terraform displays the error_message, and stops the current operation.

Preconditions

Use precondition blocks when you want to verify your configuration's assumptions for resources, data sources, and outputs before Terraform creates them.

resource "<TYPE>" "<LABEL>" { # ... lifecycle { # ... precondition { condition = <conditional_expression> error_message = "<message>" } postcondition { condition = <conditional_expression> error_message = "<message>" } } }
  • condition - Expression that must evaluate to true for Terraform to proceed with an operation.
  • error_message - Message to display if the condition evaluates to false.

Terraform evaluates the precondition while it builds its plan, and if the precondition fails Terraform displays the error message and stops the current operation.

You can use precondition in the lifecycle settings of the following Terraform configuration blocks:

  • data
  • ephemeral
  • resource

An output block can also include a precondition to verify a module's output.

Postconditions

Use postcondition blocks to validate the requirements your resources and data sources must meet for your configuration to run. Terraform evaluates postcondition blocks after planning and applying changes to a resource, or after reading from a data source. If the postcondition fails, Terraform displays an error provided in the error_message argument and stops the current operation.

You can use postcondition in the lifecycle settings of the following Terraform configuration blocks:

  • data
  • ephemeral
  • resource

Checks

Use the check block to validate your infrastructure outside of the typical resource lifecycle. The check block executes as the last step of plan or apply operation, after Terraform has planned or provisioned your infrastructure. When a check block's assertion fails, Terraform reports a warning and continues executing the current operation.

check "<LABEL>" { data "<TYPE>" "<LABEL>" { # ... } assert { condition = <conditional_expression> error_message = "<message>" } }
  • data - Specifies a data source to use for validation. You can only reference this data source within its parent check block. Terraform fetches the information for nested data sources as the final step of a plan or apply operation.
  • assert - The validation condition to evaluate. A check block contains one or more assertions.
  • condition - Expression that Terraform evaluates. If the expression evaluates to true, then the assert block passes.
  • error_message - Message to display if the condition evaluates to false.

Order of validation

  1. Input variable validations - before generating a plan.
  2. Preconditions - after generating a plan but before creating the resource, data source, or output.
  3. Postconditions - after planning and applying changes.
  4. Checks - at the end of plan and apply operations and every time health assessments run on a workspace in HCP Terraform.

Back to Top

4h. Understand Best Practices for Managing Sensitive Data, Including Secrets Management with Vault

Best Practices

Do not hard-code secrets in .tf files. Avoid placing secrets in Terraform state file wherever possible, and if placed there, take steps to secure the sate and reduce the risk by using remote backend, encryption at rest, and access control.

Preferred secret injection methods:

  • Environment variables (e.g., TF_VAR_db_password).
  • Secrets manager (HashiCorp Vault, AWS Secrets Manager, etc.).
  • Variable files, sach as terraform.tfvars (keep out of version control systems).
  • Pass via CLI (-var "db_password=...").

Secrets in state files

Terraform stores the state as plain text, including variable values, even if you have flagged them as sensitive.

Recommendations for handling sensitive data in state:

  • Use remote backends with encryption at rest.
  • Use access controls (IAM roles, ACLs) to limit who has access to the state files or backend.
  • Use audit logs to track state access.
  • Do not commit state files to a version control system.

The sensitive Attribute

The sensitive attribute as a built-in mechanism to mark variables and outputs as sensitive and suppress the display of such values in command-line output of terraform plan and terraform apply.

variable "password" { type = string sensitive = true } output "password" { value = var.password sensitive = true }

When a value is marked as sensitive = true, Terraform:

  • propagates the sensitivity to any dependent outputs or expressions,
  • masks the value in CLI outputs,
  • requires explicit terraform output <output> commands to reveal it.

It does not:

  • prevent use of the value in interpolation or expressions,
  • encrypt or mask the value in the state file.

The ephemeral Attribute

Terraform's ephemeral attribute allows you to mark certain values as temporary or non-persistent. When applied, it tells Terraform not to store the marked value in the state file, making it useful for sensitive or short-lived data that should only exist during runtime.

When a value is marked with ephemeral = true:

  • It will not be written to the state file
  • It will still be available during the plan and apply operations for internal computation
  • It behaves similarly to sensitive = true, but with the added benefit of avoiding storage in a state file.

Ephemeral Variables

You can mark an input variable as ephemeral by setting the ephemeral argument to true.

Ephemeral variables can be referenced only in specific contexts. The following are valid contexts for ephemeral variables:

  • write-only arguments
  • local values
  • ephemeral resources
  • ephemeral outputs
  • provider blocks
  • provisioner and connection blocks

Note: Any expression that references an ephemeral variable will also be treated as ephemeral.

Ephemeral local Values

A local value is implicitly treated as ephemeral if it is assigned a value that originates from an ephemeral source.

Ephemeral Outputs

To mark an output in a child module as ephemeral, set the ephemeral attribute to true. This output will not be written to the state file.

Ephemeral outputs can be referenced only in specific contexts. The following are valid contexts for ephemeral outputs:

  • write-only arguments
  • local values
  • ephemeral resources

Note that you cannot set an output value as ephemeral in the root module.

Ephemeral Blocks and Resource

An ephemeral block is a special construct, similar to a resource block, that describes one or more temporary ephemeral resources only available during the Terraform run phase. Terraform does not store ephemeral resources in its state.

Structure of an Ephemeral Block:

ephemeral "<resource_type>" "<resource_name>" { <attributes> <meta-arguments> }

The arguments within the body of an ephemeral block vary by resource type and provider. An ephemeral resource type's documentation usually lists available arguments and provides details on how to use them.

Vault Provider

HashiCorp Vault is a centralized tool for management of tokens, passwords, certificates, encryption keys, and other sensitive data.

The Vault provider allows Terraform to integrate with HashiCorp Vault to perform the following:

  • Generate short-lived dynamic credentials for various cloud providers including AWS, GCP, and Azure.
  • Read and write Vault secrets.
  • Utilize Terraform Cloud secrets engine to generate, manage and revoke credentials for HCP Terraform and Terraform Enterprise (Generating dynamic secrets for Terraform runs using Vault).

Interacting with Vault from Terraform causes any secrets to be persisted in both Terraform's state file and in any generated plan files. For any Terraform module that reads or writes Vault secrets, these files should be treated as sensitive and protected accordingly.

Back to Top

Practice Questions

Which file extension is commonly used for Terraform configuration files?
How can you provide values for variables in Terraform?
What is the purpose of the output block in a Terraform configuration?
Which block is used to specify the configuration of a resource in Terraform?
In Terraform what is the effect of setting the lifecycle meta-argument prevent_destroy to true in a resource block?
Which syntax allows you to define a map variable in Terraform?
What is the purpose of the depends_on meta-argument in a resource block?
Which is the correct syntax for defining a count on a resource in Terraform?
How can you ignore changes to a specific resource attribute?
What is the primary purpose of the terraform output command?
In Terraform, which function is used to concatenate strings?
How can you provide different values for variables in multiple environments (e.g., Dev, Prod)?
What is the purpose of the locals block in Terraform?
What is the correct way to write a conditional expression in Terraform?
What is the correct way to define an output that is sensitive in Terraform?
Does HashiCorp Configuration Language (HCL) support user-defined functions?
Which Terraform block is specifically designed to dynamically construct a collection of nested configuration blocks?
Which block in Terraform is used to define authentication details for provisioners that connect to remote resources?
How does Terraform determine dependencies between resources?
How can you specify that a resource should only be created if a condition is true?
How do you define a list variable in Terraform?
Which block type is used to define output values in Terraform?
In Terraform how can you use a count argument to create multiple instances of a resource?
How do you define a boolean variable in Terraform?
Which block type is used to define a Terraform data source?
What does setting create_before_destroy to true do in a resource block?
Which block in a terraform configuration allows specifying conditions for resource creation or destruction?
What is the purpose of the terraform graph command?
Which Terraform features and capabilities can be used to manage different environments?
How can you reference an attribute from a different resource in Terraform?
Which provisioner is used to execute commands on the machine running Terraform?
How can you enforce a specific order of resource creation in Terraform?
Which attribute can be used to specify that a resource should be replaced when a certain condition is met?
Which command shows the outputs of your Terraform configuration?
How do you define an output value in a Terraform configuration?
What is the purpose of the terraform console command?
What does the terraform graph command output by default?
How do you specify that a particular resource should only be created if a condition is met?
Your Terraform configuration contains a resource named null_resource.script with a local-exec provisioner. What command do you need to use first in order to re-run the script?
Which provisioner invokes a process on the resource created by Terraform?
Do Terraform variable and output descriptions get stored in the state file?
Should you store sensitive or secret data (such as passwords, API keys, or private keys) in the same version control repository as your Terraform configuration files?

Back to Top