length, the size of a collection
Works on list, set, map, and string:
length(["a", "b", "c"]) # 3
length(toset(["a", "b", "b"])) # 2 (duplicate removed)
length({a = 1, b = 2}) # 2length("hello") # 5Useful for conditions and validation:
validation {condition = length(var.azs) >= 2
error_message = "At least 2 availability zones required."
}
lookup, a value by key with a default
lookup({a = 1, b = 2}, "a") # 1lookup({a = 1, b = 2}, "missing") # error: key not foundlookup({a = 1, b = 2}, "missing", 0) # 0 (third arg = default)The third argument is the fallback used when the key is not found. Without it, lookup fails. In practice you almost always pass a default.
The modern shortcut is map["key"]:
var.tags["Environment"] # like lookup but without a default; fails if the key is absent
try(var.tags["Environment"], "") # with a default via try
merge, combining maps
Several maps into one. Later maps override earlier ones:
merge(
{ Project = "demo", Owner = "team" }, { Owner = "platform", Env = "prod" })
# {# Project = "demo"
# Owner = "platform" # overridden
# Env = "prod"
# }
The main case is common_tags plus specific tags:
locals { common_tags = { Project = var.project, ManagedBy = "terraform" }}
resource "aws_s3_bucket" "logs" {bucket = "logs-bucket"
tags = merge(local.common_tags, {Purpose = "logs"
})
}
concat, joining lists
concat(["a", "b"], ["c", "d"]) # ["a", "b", "c", "d"]
concat(var.base_cidrs, var.extra_cidrs) # join two CIDR lists
Order is preserved. Duplicates are not removed (wrap the result in distinct if you need that).
flatten, unnest nested lists
Often the result of a for expression or several concat calls is an array of arrays. flatten turns it into a flat list:
flatten([["a", "b"], ["c"], [], ["d", "e"]])
# ["a", "b", "c", "d", "e"]
A typical case: collect all IPs of all instances across all groups:
locals {all_private_ips = flatten([
for asg in aws_autoscaling_group.tiers : asg.target_group_arns
])
}
It unnests only one level. If there is deeper nesting inside, use flatten(flatten(...)).
keys, values, the parts of a map
keys({a = 1, b = 2, c = 3}) # ["a", "b", "c"] (sorted by key)values({a = 1, b = 2, c = 3}) # [1, 2, 3] (same order as keys)keys always returns a sorted list, which is handy for stable results in plan.
contains, checking for membership
contains(["dev", "staging", "prod"], var.env)
# true / false
Its most common role is in validation:
validation {condition = contains(["dev", "staging", "prod"], var.env)
error_message = "env must be dev, staging, or prod."
}
There is no contains for a map. Use contains(keys(map), "k") or try(map["k"], null) != null.
distinct, remove duplicates
distinct(["a", "b", "a", "c", "b"]) # ["a", "b", "c"]
Order is preserved (first occurrence). The alternative is toset(...), but toset loses the order.
element and slice
element(["a", "b", "c"], 1) # "b"
element(["a", "b", "c"], 5) # "c", wraps around: 5 % 3 = 2
slice(["a", "b", "c", "d"], 1, 3) # ["b", "c"], [start, end)
element wraps around with the modulo operator, which is its quirk. If you want a hard error when the index goes out of bounds, use list[index].
zipmap, build a map from two lists
zipmap(["a", "b", "c"], [1, 2, 3])
# {a = 1, b = 2, c = 3}The lengths must match, otherwise you get an error.
for expressions, more powerful than any function
# list → list
[for s in ["dev", "prod"] : upper(s)]
# ["DEV", "PROD"]
# list → map
{ for s in ["dev", "prod"] : s => "bucket-${s}" }# {dev = "bucket-dev", prod = "bucket-prod"}# with a filter
[for s in var.envs : s if s != "test"]
A for expression covers most transformations. Many of the built-in functions (keys, values, sometimes merge) can be rewritten with for.
Gotchas
-
mergeis flat. If the values are themselves maps, they get overwritten, not merged. For a deep merge, use severalmergecalls or do it by hand with a for expression. -
concatdoes not deduplicate. If you need to combine and drop duplicates, usedistinct(concat(a, b)). -
lookupwithout a default blows up. Always pass the third argument or usetry(). -
flattengoes one level deep. For double nesting, useflatten(flatten(arr)). -
keyssorts, which is good for stability. If you need the original order of a map's keys, that is a problem in itself, because an HCL map is unordered by nature. Use a list of objects instead. -
elementwraps around, butlist[index]does not.element(arr, 999)gives a result by modulo.arr[999]fails. For a safe lookup, usetry(arr[index], default). -
containsis strictly typed.contains([1, 2, 3], "1")fails, because"1"is a string and1is a number. Cast the types explicitly. -
forwith a map requires unique keys. In{ for s in arr : key_func(s) => ... }, if key_func produces the same key for two elements, it fails. Fix it withfor ... if condor by changing the key.