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
boolFor example:
"hello"
42
trueTerraform also has collection and structural types such as:
list
set
map
object
tupleA type matters because it determines what operations make sense for a value.
For example, a number can participate in arithmetic:
5 + 3while 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 = 3Booleans represent true or false:
enabled = trueThese 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-aand the second is:
region-bLists 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 orderingThis 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 typesTuples
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.environmentreferences an input variable.
A local value is referenced as:
local.application_nameA resource attribute is referenced using the resource address:
random_pet.application.idThese 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_instancesYou can also use arithmetic directly:
value = 3 * 2Expressions become particularly useful when a value depends on other configuration values.
Instead of hard-coding:
disk_count = 6you might calculate it:
disk_count = var.server_count * 2Now changing server_count changes the calculated value automatically.
Conditional Expressions
A conditional expression selects one of two values:
condition ? true_value : false_valueFor 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-webTerraform 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_lengthis 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.
EOFThe 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-01You 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
validationWhen 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.tfCreate 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 initValidate:
terraform validatePlan:
terraform planApply:
terraform applyConfirm with:
yesInspect the outputs:
terraform outputNow change:
environment = "prod"in variables.tf.
Run:
terraform planThe conditional expression should now select:
largeinstead of:
smallYou can also change the list:
default = [
"small",
"medium",
"xlarge"
]and observe how the for expression transforms the values.
When finished:
terraform destroyHow 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 resultFor example:
local.application_name = "${upper(var.environment)}-${random_pet.application.id}"Terraform needs values from two places:
var.environment
random_pet.application.idIt 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_nameThe 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
└── functionsThese 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.