Skip to content

Expressions and Types


Terraform configurations become expressive when values can be calculated rather than simply copied into arguments. A condition can select one value or another, a list can be transformed, a map can provide named settings, and a resource attribute can become the input to another resource. To use these features confidently, you need a solid understanding of Terraform’s types and expressions.

Values and Types

Every Terraform expression produces a value, and every value has a type.

The basic primitive types are:

string
number
bool

For example:

"hello"
42
true

Terraform also has collection and structural types such as:

list
set
map
object
tuple

A type matters because it determines what operations make sense for a value.

For example, a number can participate in arithmetic:

5 + 3

while a string is text:

"hello"

Terraform can sometimes convert compatible values between types, but you should not rely on implicit conversion when a clear type makes the configuration easier to understand.

Strings, Numbers, and Booleans

Strings represent text:

project = "web"

Numbers represent numeric values:

instance_count = 3

Booleans represent true or false:

enabled = true

These values can be assigned to variables:

variable "project" {
  type = string
}

variable "instance_count" {
  type = number
}

variable "enabled" {
  type = bool
}

The type constraints communicate what the configuration expects.

Lists

A list is an ordered sequence of values.

For example:

regions = ["region-a", "region-b", "region-c"]

You can access an element by its zero-based index:

var.regions[0]

The first element is therefore:

region-a

and the second is:

region-b

Lists are useful when order matters or when you naturally think of the values as a sequence.

Sets

A set is a collection of unique values without a meaningful ordering.

For example:

toset(["web", "api", "web"])

produces a set containing the unique values.

Conceptually:

list
    = ordered sequence

set
    = unique collection without meaningful ordering

This distinction matters when you need predictable positions. A set is not a replacement for a list when order is important.

Maps

A map associates keys with values.

For example:

tags = {
  environment = "dev"
  owner       = "platform"
}

You can access a value by key:

var.tags["environment"]

Maps are useful when you have named values of the same general type.

A variable can constrain a map:

variable "tags" {
  type = map(string)
}

This says that the value is a map whose values are strings.

Objects

An object is a structured value with named attributes that can have different types.

For example:

application = {
  name    = "web"
  replicas = 3
  enabled = true
}

A corresponding type constraint is:

type = object({
  name     = string
  replicas = number
  enabled  = bool
})

This is more precise than a generic map because the object has a defined shape.

The distinction is useful:

map(string)
    = arbitrary string values indexed by keys

object(...)
    = a defined structure with named attributes and types

Tuples

A tuple is an ordered sequence whose elements can have different types.

For example:

["web", 3, true]

can be represented by:

tuple([string, number, bool])

The exact type describes each position.

Tuples are less common in everyday beginner Terraform configurations than lists, but understanding them helps when Terraform reports a complex type.

References

Terraform expressions can refer to other values.

For example:

var.environment

references an input variable.

A local value is referenced as:

local.application_name

A resource attribute is referenced using the resource address:

random_pet.application.id

These references are how Terraform configurations connect values.

For example:

resource "random_pet" "application" {
  length = 2
}

output "name" {
  value = random_pet.application.id
}

The output depends on the resource attribute because its expression references that attribute.

Arithmetic Expressions

Terraform supports arithmetic expressions.

For example:

total_instances = var.web_instances + var.api_instances

You can also use arithmetic directly:

value = 3 * 2

Expressions become particularly useful when a value depends on other configuration values.

Instead of hard-coding:

disk_count = 6

you might calculate it:

disk_count = var.server_count * 2

Now changing server_count changes the calculated value automatically.

Conditional Expressions

A conditional expression selects one of two values:

condition ? true_value : false_value

For example:

locals {
  instance_type = var.environment == "prod" ? "large" : "small"
}

The expression means:

If environment is prod:
    use "large"

Otherwise:
    use "small"

This is useful when a configuration needs small variations based on an input.

Do not turn every decision into a conditional, though. Excessive conditional logic can make a Terraform configuration harder to understand.

String Interpolation

Terraform supports interpolation inside strings.

For example:

name = "${var.environment}-web"

If:

var.environment = "dev"

the resulting string is:

dev-web

Terraform also supports direct expression syntax in many contexts, so you do not need interpolation when the entire value is already an expression.

For example:

length = var.name_length

is preferable to:

length = "${var.name_length}"

because the resource expects a number rather than text.

A useful rule is:

Use interpolation when you are constructing a string; use a direct expression when you are supplying a value.

Heredocs

Heredocs let you write multiline strings.

A basic heredoc looks like:

message = <<EOF
Hello from Terraform.
This is a multiline string.
EOF

The opening marker and closing marker define the string boundary.

Heredocs are useful for larger blocks of text such as:

  • scripts
  • configuration files
  • policy documents
  • multiline messages

Use them when a multiline value genuinely makes the configuration clearer. For a short string, a normal quoted string is easier to read.

for Expressions

A for expression transforms or filters collections.

For example:

[for name in ["web", "api", "worker"] : upper(name)]

produces:

["WEB", "API", "WORKER"]

The basic structure is:

[for item in collection : expression]

You can also filter values.

For example:

[for name in ["web", "api", "worker"] : name if name != "worker"]

produces:

["web", "api"]

The important idea is not the syntax itself. A for expression lets you calculate a new collection from an existing collection.

More advanced uses of for expressions become especially useful when building reusable modules and complex infrastructure interfaces. For now, focus on understanding the transformation model.

Built-In Functions

Terraform provides many built-in functions for working with values.

You will encounter functions such as:

length(var.names)

to determine a collection’s length,

upper(var.environment)

to convert text to uppercase,

and:

contains(var.environments, var.environment)

to check whether a collection contains a value.

Another useful function is:

join("-", ["dev", "web", "01"])

which produces:

dev-web-01

You do not need to memorize every Terraform function.

A better approach is to recognize the categories of problems functions solve:

strings
collections
numbers
encoding
type conversion
validation

When you need a function, consult the Terraform language documentation rather than trying to remember the entire function library.

Type Conversion

Terraform can convert between some compatible types.

For example:

tostring(42)

produces a string representation of the number.

Similarly:

tonumber("42")

converts a numeric string to a number when the string contains a valid number.

Other conversion functions include:

tolist(...)
toset(...)
tomap(...)

These are useful when you need to make the intended type explicit.

For example:

toset(["web", "api", "web"])

makes it clear that you want a unique collection rather than an ordered list.

Explicit conversion can make a configuration easier to reason about because the intended type is visible in the code.

A Complete Expression Exercise

Create:

terraform-expressions/
├── main.tf
├── variables.tf
└── outputs.tf

Create main.tf:

terraform {
  required_providers {
    random = {
      source  = "hashicorp/random"
      version = "~> 3.6"
    }
  }
}

provider "random" {}

resource "random_pet" "application" {
  length    = var.name_length
  separator = "-"
}

locals {
  application_name = "${upper(var.environment)}-${random_pet.application.id}"
  sizes            = [for size in var.sizes : upper(size)]
  instance_class   = var.environment == "prod" ? "large" : "small"
}

Create variables.tf:

variable "environment" {
  type        = string
  description = "Deployment environment."
  default     = "dev"
}

variable "name_length" {
  type        = number
  description = "Number of words in the generated name."
  default     = 2
}

variable "sizes" {
  type        = list(string)
  description = "Application size labels."

  default = [
    "small",
    "medium",
    "large"
  ]
}

Create outputs.tf:

output "application_name" {
  description = "Generated application name."
  value       = local.application_name
}

output "sizes" {
  description = "Uppercase size labels."
  value       = local.sizes
}

output "instance_class" {
  description = "Selected class for the environment."
  value       = local.instance_class
}

output "joined_sizes" {
  description = "Size labels joined into one string."
  value       = join(", ", local.sizes)
}

Initialize:

terraform init

Validate:

terraform validate

Plan:

terraform plan

Apply:

terraform apply

Confirm with:

yes

Inspect the outputs:

terraform output

Now change:

environment = "prod"

in variables.tf.

Run:

terraform plan

The conditional expression should now select:

large

instead of:

small

You can also change the list:

default = [
  "small",
  "medium",
  "xlarge"
]

and observe how the for expression transforms the values.

When finished:

terraform destroy

How Terraform Evaluates Expressions

It is tempting to think of expressions as little pieces of text that Terraform substitutes mechanically.

A better model is:

Expression
    │
    ▼
Terraform evaluates references
    │
    ▼
Values become available
    │
    ▼
Terraform produces a result

For example:

local.application_name = "${upper(var.environment)}-${random_pet.application.id}"

Terraform needs values from two places:

var.environment
random_pet.application.id

It evaluates the expression using those values and produces the resulting string.

The result can then be used by another part of the configuration.

This is why expressions are the connective tissue of Terraform configurations.

Keep Expressions Readable

Terraform expressions can become extremely powerful, but power can also make configuration difficult to read.

Compare:

name = "${var.environment}-web"

with a deeply nested expression containing several conditionals, conversions, comprehensions, and functions.

The second may be technically correct while still being a maintenance problem.

A useful principle is:

Prefer an expression that a future reader (including yourself) can understand quickly over an expression that is merely compact.

If a calculation is complicated or reused, a local value can give it a meaningful name:

locals {
  application_name = "${var.environment}-web"
}

Then the rest of the configuration can use:

local.application_name

The name explains the purpose without forcing every reader to reconstruct the expression.

The Mental Model for Terraform Values

At this point, you can think about Terraform values as a small language of their own:

Primitive values
├── string
├── number
└── bool

Collections
├── list
├── set
└── map

Structural values
├── object
└── tuple

Expressions
├── references
├── arithmetic
├── conditionals
├── interpolation
├── for expressions
└── functions

These pieces combine to turn static configuration into a programmable description of infrastructure.

You do not need to learn every expression feature before moving forward. You need to be comfortable recognizing values, types, references, and the common ways Terraform calculates one value from another.

That foundation will become increasingly important when configurations grow into reusable modules and more advanced infrastructure patterns later in the course.

Last updated on