Learn OpenTofu - Advanced Expressions, Built-in Functions & Loops
Episode 8 of 21

Learn OpenTofu - Advanced Expressions, Built-in Functions & Loops

Explore OpenTofu's built-in functions for strings, collections, and filesystem operations, then master the count, for_each, depends_on, and lifecycle meta-arguments, along with for expressions and ternary conditionals for writing concise, dynamic, and expressive HCL.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Introduction

In the previous episode 7 we covered dynamic provider iteration — loading many provider instances at once with for_each for multi-region and multi-account setups. We already saw how one small map can replace dozens of HCL lines. Today we level up again: unpacking OpenTofu's expression language that makes HCL feel like a real programming language.

Engineers often get stuck writing configuration statically and repetitively — hardcoding subnet lists, copying resource blocks one by one, or writing if-then conditions rigidly. Yet OpenTofu provides hundreds of built-in functions, iteration meta-arguments, and conditional expressions that can drastically cut the amount of code.

In this episode we cover four pillars of OpenTofu expressions: built-in functions for strings, collections, and the filesystem; the count, for_each, and depends_on meta-arguments; lifecycle rules; and for expressions and ternary conditionals.

Main Discussion

Built-in Functions: String, Collection, Filesystem

Built-in functions in OpenTofu work like in programming languages — they process values and return results. Broadly, there are three families you'll use most:

FamilyExample FunctionsUse Cases
Stringformat, join, split, replace, upperText manipulation and resource name construction
Collectionlength, lookup, merge, flatten, distinct, setunionCombining, filtering, and tidying data
Filesystemfile, templatefile, yamldecode, jsondecodeReading configuration from external files

Here's an example of usage in a single locals block:

locals: built-in functions
locals {
  env        = "staging"
  bucket     = join("-", ["app", local.env, "assets"])
  subnets    = split(",", var.subnet_list)
  merged     = merge(var.default_tags, { env = local.env })
  regions    = distinct(var.regions)
  db_user    = lookup(var.db_config, "user", "admin")
  yaml_conf  = yamldecode(file("config.yaml"))
  safe_value = try(coalesce(var.name, local.env), "default")
}
  • join and split build and break apart strings with a separator.
  • merge combines multiple maps — very useful for keeping tags consistent.
  • distinct removes duplicates from a list.
  • lookup fetches a value from a map with a default when the key is missing.
  • file and yamldecode read configuration files from disk and parse them.
  • try and coalesce handle empty values safely without errors.

Tip

Explore the full function list anytime by typing tofu console and trying expressions like join("-", ["a", "b"]) right in the prompt. It's the fastest playground for testing function behavior before writing it into code.

The count and for_each Meta-Arguments

These two meta-arguments are the backbone of iteration in OpenTofu. count creates resources based on a number and accesses them via count.index; for_each creates resources based on map or set items and accesses them via each.key and each.value.

count: three identical instances
resource "aws_instance" "web" {
  count         = 3
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"
  tags = {
    Name = "web-${count.index}"
  }
}
for_each: one user per name
resource "aws_iam_user" "developers" {
  for_each = toset(["budi", "sari", "dewi"])
  name     = each.key
  tags = {
    Role = each.key == "budi" ? "admin" : "dev"
  }
}

Notice the Role line above: it uses a ternary — if each.key equals budi, the value is admin, otherwise dev. That's the classic pattern: iteration to spread out, ternary to customize.

Warning

Use for_each for collections with unique identities (user names, map keys), and count only for truly identical collections. Removing one item from the middle of a count list can shift indices and trigger unwanted resource recreates — for_each is far safer because keys are explicit.

The depends_on Meta-Argument

OpenTofu builds the dependency graph automatically from references between resources. depends_on is only needed when execution order can't be inferred from references — for example, a configuration that needs another resource to finish first without a direct reference:

explicit depends_on
resource "aws_instance" "app" {
  depends_on = [aws_security_group.main]
  ami        = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"
}

Note

depends_on should be a last resort. Too many depends_on makes the graph rigid and slow, and hides dependencies that could be expressed through ordinary value references. Let OpenTofu do what it can already infer on its own.

Lifecycle Rules

The lifecycle meta-argument gives you control over when and how OpenTofu recreates resources. The three most-used rules:

lifecycle rules
resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"
 
  lifecycle {
    create_before_destroy = true
    prevent_destroy       = false
    ignore_changes        = [tags]
  }
}
  • create_before_destroy creates the new resource before deleting the old one — a must for minimizing downtime.
  • prevent_destroy blocks tofu destroy — a guard for critical resources like databases.
  • ignore_changes ignores attributes that frequently change outside IaC, such as tags modified in the console.

Warning

prevent_destroy isn't absolute anti-vandalism: it only refuses a resource being removed from the code, but doesn't prevent destroy caused by definition changes. For the most critical resources, combine it with state encryption and backups. To delete a protected resource you must use tofu plan -destroy after removing the prevent_destroy line from the code.

For Expressions & Ternary Conditionals

A for expression produces a new collection from another collection — a sort of map or filter from programming languages. Splat [*] is a shortcut to grab one attribute from all instances. Ternary condition ? if_true : if_false picks a value based on a condition:

for, splat, and ternary
locals {
  sequence   = [for i in range(3) : "web-${i}"]
  filtered   = [for name in var.names : upper(name) if name != "skip"]
  tag_map    = { for k, v in var.tags : k => upper(v) }
  all_ips    = aws_instance.web[*].private_ip
  tier       = var.environment == "production" ? "m5.large" : "t3.micro"
}
  • sequence builds a list from a range.
  • filtered transforms and filters at once with an if clause.
  • tag_map builds a map with a k => v expression.
  • all_ips uses splat to collect private_ip from all instances.
  • tier picks the instance type based on environment — a ternary here replaces an entire conditional block.

Combine all of it with built-in functions, and you can express almost any configuration logic without leaving HCL.

Conclusion

In episode 8 we mastered OpenTofu's expression language:

  • Built-in functions for strings, collections, and filesystem — from join, merge, lookup to file and yamldecode.
  • count and for_each for resource iteration, with count.index and each.key / each.value.
  • depends_on for explicit dependencies OpenTofu can't infer.
  • lifecycle rulescreate_before_destroy, prevent_destroy, and ignore_changes.
  • For expressions, splat, and ternary for concise collection transforms and conditional logic.

With these expressions under your belt, your HCL will be far more dynamic. In the next episode, episode 9, we'll cover OpenTofu State Manipulation & Import Workflows — importing existing infrastructure via the import block and tofu import, then refactoring without destroy using the moved block and tofu state mv. See you there!

Learn OpenTofu - Advanced Expressions, Built-in Functions & Loops | Learn OpenTofu