Terraform Conditionals and for Expressions: Ternary, count, splat and Examples
Logic in HCL #
HCL is declarative, but it has expressions to choose values and transform collections. These are the ones you use every day. The full list of built-in functions is in Terraform functions.
Conditional expression (ternary) #
variable "environment" {
type = string
}
locals {
instance_type = var.environment == "pro" ? "m6i.large" : "t3.micro"
min_size = var.environment == "pro" ? 3 : 1
}The format is condition ? value_if_true : value_if_false. Both values must be the same type. Combine conditions with &&, || and !, and compare with ==, !=, <, >.
Create a resource only if... #
variable "create_bastion" {
type = bool
default = false
}
resource "aws_instance" "bastion" {
count = var.create_bastion ? 1 : 0
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
}
output "bastion_ip" {
value = one(aws_instance.bastion[*].public_ip) # null when not created
}count = condition ? 1 : 0 is the standard switch. one() returns the single element or null if the list is empty, avoiding an index error. See for_each vs count.
Optional values and defaults #
locals {
# first non-null, non-empty value
name = coalesce(var.name, "default-name")
# fall back if an expression fails or is null
port = try(var.config.port, 8080)
# value from a map with a default
size = lookup(var.sizes, var.environment, "small")
}Use optional() in object types for attributes that may be missing:
variable "rule" {
type = object({
port = number
protocol = optional(string, "tcp")
cidrs = optional(list(string), ["10.0.0.0/8"])
})
}for expressions #
Transform a list into another list:
locals {
names = ["web", "db", "cache"]
upper = [for n in local.names : upper(n)] # ["WEB", "DB", "CACHE"]
prefixed = [for n in local.names : "ditwl-${n}"]
with_idx = [for i, n in local.names : "${i}-${n}"]
filtered = [for n in local.names : n if n != "cache"] # ["web", "db"]
}Build a map (use braces and =>):
locals {
subnet_cidrs = {
a = "10.0.1.0/24"
b = "10.0.2.0/24"
}
name_to_cidr = { for k, v in local.subnet_cidrs : "private-${k}" => v }
only_a = { for k, v in local.subnet_cidrs : k => v if k == "a" }
}Using the result with for_each creates one resource per item.
Splat expressions #
output "instance_ids" {
value = aws_instance.web[*].id # same as [for i in aws_instance.web : i.id]
}With for_each resources, use values(): [for i in aws_instance.web : i.id] or values(aws_instance.web)[*].id.
Merge, flatten and setproduct #
locals {
tags = merge(var.common_tags, { Name = "web" }, var.extra_tags)
# flatten nested lists
all_subnets = flatten([for vpc in var.vpcs : vpc.subnets])
# every combination
pairs = setproduct(["web", "db"], ["a", "b"]) # [["web","a"],["web","b"],["db","a"],["db","b"]]
}A common pattern is to flatten nested structures into a map for for_each:
locals {
rules = flatten([
for sg, ports in var.sg_ports : [
for port in ports : {
key = "${sg}-${port}"
sg = sg
port = port
}
]
])
}
resource "aws_vpc_security_group_ingress_rule" "this" {
for_each = { for r in local.rules : r.key => r }
security_group_id = aws_security_group.this[each.value.sg].id
ip_protocol = "tcp"
from_port = each.value.port
to_port = each.value.port
cidr_ipv4 = "10.0.0.0/8"
}Dynamic blocks and templates #
Conditions can generate nested blocks (dynamic blocks) and templatefile supports %{ if } and %{ for } directives to build text files such as user data.
Test expressions #
$ terraform console
> [for n in ["a","b"] : upper(n)]
[
"A",
"B",
]
> var.environment == "pro" ? 3 : 1
terraform console loads your configuration and evaluates any expression, the fastest way to debug. See also common errors and the cheat sheet.