Terraform Collection Functions
Learn Terraform list, map, set, and type-conversion functions, plus for expressions and transformation patterns for dynamic infrastructure code.
Why Collection Functions Matter
As soon as your Terraform configuration moves beyond a few hardcoded resources, you start working with collections of values rather than isolated strings.
Typical examples include:
- a list of subnet CIDR blocks
- a map of environment settings
- a set of allowed regions
- a list of bucket definitions
- merged tags from multiple sources
Terraform collection functions help you shape that data into the exact form your resources, modules, and outputs need. They are especially important because cloud infrastructure is often repetitive but not perfectly uniform. You may need to combine lists, remove duplicates, merge maps, convert input types, or build a new collection dynamically from an old one.
Once you understand these functions, Terraform starts to feel far more expressive. Instead of repeating logic manually, you can transform data declaratively and keep your configuration compact.
A useful mental model is that collection functions sit between raw input and final resource arguments. Variables, module inputs, and data sources often provide values in a shape that is convenient for humans but not ideal for direct use in resources. Collection functions help you clean, combine, filter, and restructure that data so the rest of the configuration stays easier to read.
List Functions
Lists are ordered collections. Terraform includes several functions that help you combine, filter, inspect, and reorganize them.
concat()
concat() combines multiple lists into one list.
concat(["subnet-a"], ["subnet-b", "subnet-c"])
Result:
["subnet-a", "subnet-b", "subnet-c"]
Use cases:
- combine default and custom lists
- merge subnet groups
- build a final list from several sources
Example:
locals {
all_cidrs = concat(var.default_cidrs, var.extra_cidrs)
}
flatten()
flatten() converts a nested list into a single-level list.
flatten([["a", "b"], ["c"], ["d", "e"]])
Result:
["a", "b", "c", "d", "e"]
This is very useful when different expressions each produce a list and you want one final list.
distinct()
distinct() removes duplicate elements while preserving the first occurrence order.
distinct(["dev", "prod", "dev", "staging"])
Result:
["dev", "prod", "staging"]
This is helpful when inputs may overlap and you want clean results.
slice()
slice(list, start, end) returns a portion of a list.
slice(["a", "b", "c", "d"], 1, 3)
Result:
["b", "c"]
Common use cases:
- choose the first two availability zones
- trim an input list
- test subsets of values
reverse()
reverse() returns the elements in the opposite order.
reverse(["a", "b", "c"])
Result:
["c", "b", "a"]
This is less common than concat() or flatten(), but it can be useful in selection logic.
sort()
sort() returns a sorted version of a list of strings.
sort(["prod", "dev", "staging"])
Result:
["dev", "prod", "staging"]
This helps when you want predictable ordering, especially before turning values into outputs or iterating for human-readable results.
length()
length() works on strings and collections.
length(["a", "b", "c"])
Result:
3
This is essential for:
- driving
count - validations
- conditional logic
In practice, list functions are often used together. For example, you might concat() two subnet lists, distinct() the result to remove duplicates, and then sort() it to produce predictable ordering for outputs or iteration.
Map Functions
Maps store key-value pairs. They are ideal for named configuration.
merge()
merge() combines maps into one map.
merge(
{ Environment = "dev" },
{ Project = "shop" },
{ ManagedBy = "Terraform" }
)
Result:
{
Environment = "dev"
Project = "shop"
ManagedBy = "Terraform"
}
This is very common for tags.
Example:
locals {
common_tags = merge(var.default_tags, {
Environment = var.environment
})
}
If duplicate keys exist, later maps win.
lookup()
lookup(map, key, default) fetches a value from a map and optionally returns a fallback.
lookup(var.instance_types, var.environment, "t3.micro")
This is useful when not every key is guaranteed to exist.
keys()
keys() returns a list of map keys.
keys({ dev = "t3.micro", prod = "t3.medium" })
Result:
["dev", "prod"]
Useful when you want to iterate over the names in a map or inspect which items exist.
values()
values() returns the map values.
values({ dev = "t3.micro", prod = "t3.medium" })
Result:
["t3.micro", "t3.medium"]
This is helpful when only the values matter for a downstream expression.
zipmap()
zipmap(keys, values) constructs a map by pairing elements from two lists.
zipmap(["dev", "prod"], ["t3.micro", "t3.medium"])
Result:
{
dev = "t3.micro"
prod = "t3.medium"
}
This is useful when data arrives in parallel lists and you want a more meaningful map structure.
Set Functions
Sets are unordered collections of unique values. They are useful with for_each, membership logic, and deduplication.
toset(), Set Conversion
toset() converts another collection into a set.
toset(["dev", "dev", "prod"])
Result:
["dev", "prod"]
This is commonly used with for_each:
for_each = toset(var.environments)
setintersection()
Returns values present in both sets.
setintersection(toset(["dev", "prod"]), toset(["prod", "stage"]))
Result:
["prod"]
Useful for identifying overlap between allowed values and requested values.
setunion()
Returns all unique values across both sets.
setunion(toset(["a", "b"]), toset(["b", "c"]))
Result:
["a", "b", "c"]
setsubtract()
Returns values from the first set that are not in the second.
setsubtract(toset(["dev", "stage", "prod"]), toset(["stage"]))
Result:
["dev", "prod"]
Useful for exclusions and policy checks.
Type Conversion Functions
Terraform often needs values in a specific type. Type-conversion functions help you normalize data for resources and modules.
tolist()
Converts a value into a list where possible.
tolist(toset(["a", "b"]))
Use this when a resource or expression expects list behavior.
tomap()
Converts a value into a map where possible.
This is helpful when you want consistent map semantics for merging or lookups.
toset(), Type Conversion
Converts a collection to a set.
This is common for deduplication and for_each iteration.
tostring()
Converts a value into a string.
tostring(42)
Result:
"42"
Useful for tags, names, or outputs where a string is required.
tonumber()
Converts a value into a number.
tonumber("8080")
Result:
8080
Useful when input arrives as text but arithmetic or numeric provider fields require a number.
for Expressions
One of the most powerful collection features in Terraform is the for expression.
A basic list transformation looks like this:
[for x in var.list : x if condition]
This means:
- iterate over each item in
var.list - emit
xfor matching items - optionally filter with
if
Example: filtering environments
[for env in var.environments : env if env != "dev"]
This returns all environments except dev.
Example: building a new list
[for cidr in var.subnet_cidrs : "subnet-${cidr}"]
Example: building a map
{ for env in var.environments : env => upper(env) }
That returns a map where each environment is keyed to its uppercase version.
Dynamic Expressions for Complex Transformations
Real Terraform code often combines multiple functions and for expressions.
Example:
locals {
normalized_envs = distinct(sort([for env in var.environments : lower(trimspace(env))]))
}
This one expression:
- trims whitespace
- converts values to lowercase
- sorts them
- removes duplicates
That is a good example of Terraform data transformation in practice.
Practical tag-merging example
locals {
common_tags = merge(
var.default_tags,
{
Environment = var.environment
Project = var.project_name
}
)
}
Practical object transformation example
locals {
bucket_map = {
for bucket in var.buckets :
bucket.name => {
acl = bucket.acl
versioning = bucket.versioning
}
}
}
This is the kind of structure that can then feed for_each cleanly.
A Realistic Example: Networking Inputs
Suppose you receive subnet CIDRs from different sources and want a clean, consistent output.
variable "default_subnet_cidrs" {
type = list(string)
}
variable "extra_subnet_cidrs" {
type = list(string)
}
locals {
combined_subnets = concat(var.default_subnet_cidrs, var.extra_subnet_cidrs)
distinct_subnets = distinct(local.combined_subnets)
sorted_subnets = sort(local.distinct_subnets)
selected_subnets = slice(local.sorted_subnets, 0, 2)
subnet_name_map = zipmap(local.selected_subnets, ["app", "data"])
}
This example shows how collection functions help turn raw input into well-structured, intentional values.
Best Practices for Collection Transformations
1. Prefer clarity over compression
A very long one-line expression may be technically correct but difficult to maintain. Break complex logic into multiple local values when needed.
2. Use maps for named configuration
Maps usually model real infrastructure identity better than parallel lists.
3. Normalize collections before iteration
Deduplicating, sorting, or trimming inputs early makes later resource logic simpler.
4. Be careful with set ordering assumptions
Sets are unordered. If order matters, convert intentionally and do not depend on set position.
5. Use for expressions to express intent
for expressions are great when the goal is transformation, filtering, or reshaping data for later use.
Common Beginner Mistakes
Mistake 1: Keeping everything in parallel lists
Parallel lists are fragile. If named configuration matters, maps are often a better structure.
Mistake 2: Forgetting type expectations
Some functions and meta-arguments expect specific types. Conversions like toset() and tomap() help, but you must be intentional.
Mistake 3: Assuming sets preserve order
They do not. If order matters for output or selection, use lists deliberately.
Mistake 4: Writing transformations inline everywhere
If the same logic is reused, move it into locals.
Final Thoughts
Terraform collection functions are the tools that let you treat infrastructure inputs as structured data instead of static text. Once you can combine, merge, deduplicate, filter, and reshape collections, your configurations become far more reusable and far less repetitive.
A strong beginner workflow is:
- collect inputs with variables or data sources
- normalize them with functions and locals
- transform them with
forexpressions - feed the clean result into resources, modules, and outputs
That pattern appears again and again in well-designed Terraform code.
Knowledge Check
Question 1: concat()
What does Terraform's concat() function do?
Question 2: merge()
Which Terraform function is commonly used to combine multiple tag maps into one?
Question 3: for Expressions
What does the expression [for env in var.environments : env if env != "dev"] return?