# Terraform Conditionals and for Expressions: Ternary, count, splat and Examples

> How to write conditions and loops in Terraform and OpenTofu: the ternary operator, count and for_each switches, for expressions, splat, try, lookup, merge and flatten.

- Source: https://www.itwonderlab.com/terraform-conditionals-for-expressions/
- Published: 2026-05-30
- Updated: 2026-05-30
- Author: Javier Ruiz Jiménez (https://www.javierruizjimenez.com/)
- Site: IT Wonder Lab (https://www.itwonderlab.com/)

---

## Logic in HCL

[HCL](https://www.itwonderlab.com/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](https://www.itwonderlab.com/terraform-functions/).

### Conditional expression (ternary)

```hcl title="conditional.tf"
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...

```hcl title="optional.tf"
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](https://www.itwonderlab.com/terraform-for-each-vs-count/).

### Optional values and defaults

```hcl title="defaults.tf"
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:

```hcl title="variables.tf"
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:

```hcl title="for.tf"
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 `=>`):

```hcl title="for-map.tf"
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`](https://www.itwonderlab.com/terraform-for-each-vs-count/) creates one resource per item.

### Splat expressions

```hcl title="splat.tf"
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

```hcl title="collections.tf"
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`:

```hcl title="flatten.tf"
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](https://www.itwonderlab.com/terraform-dynamic-blocks/)) and `templatefile` supports `%{ if }` and `%{ for }` directives to build text files such as [user data](https://www.itwonderlab.com/terraform-provisioners-user-data/).

### Test expressions

```shell
$ 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](https://www.itwonderlab.com/terraform-common-errors/) and the [cheat sheet](https://www.itwonderlab.com/terraform-cheat-sheet/).
