Terraform

Terraform Functions, Expressions, Loops (Examples)

terraform functions

The native language of Terraform, called HCL, can be an extremely convenient and efficient tool, if you know it well. Although it is not a programming language (even though operators coding in HCL are often jokingly called “HCL Developers”), it does implement some structures and logic that bear resemblance to traditional programming. 

In this article, we will discuss the HCL’s core – built-in functions, expressions and loops. What are those, and what are they for? Let’s dive in.

Terraform Functions

What are Terraform Functions? Terraform functions are a means of manipulating data, and provide you with the possibilities to combine, transform or operate in another way on provided values. For example, let’s look at the “join” function:

join(separator, list)

Its purpose is to create a string out of the elements from a given list, with the given separator as a delimiter. So, if you declare it like so,

join("|", ["This", "Is", "A", "Test"])

the resulting output will be

"This|Is|A|Test"

As you can see, the base principle is pretty easy. Things can go in, different things can come out. 

Functions are used for many, many things – from simple type conversions (e.g. string to number), value combinations (such as getting all list elements to a single string as above), selecting (e.g. select the biggest number from a list of numbers), correcting (trimming suffixes, prefixes, capitalizing the letters of each list element), getting date and time, hashing values, advanced technical tasks such as calculating IP addresses… and much, much more.

The list of functions you can use is quite extensive – but it is also finite. Terraform does not allow you to create your own functions, so you’re bound to using what is provided by default.

Terraform Expressions

Expressions are the core of HCL itself – the logic muscle of the entire language. Terraform expressions allow you to get a value from somewhere, calculate or evaluate it. You can use them to refer to the value of something, or extend the logic of a component – for example, make one copy of the resource for each value contained within a variable, using it as an argument.

They are used pretty much everywhere – the most simple type of expression would be a literal value – so, there is a great chance that you have already used them before.

As it is an extremely extensive topic, I couldn’t possibly fit everything into this single article. Instead, I can talk about what in my humble opinion are the most notable/interesting expressions: Operators, Conditional expressions, Splat Expressions, Constraints, and Loops.

Operators

Dedicated to logical comparison and arithmetic operations, operators are mostly used for doing math and basic Bool’s algebra. If you need to know if number A equals number B, add them together, or determine if both boolean A and boolean B are “true”, Terraform offers the following operators:

Types of Terraform Operators

  • Arithmetic operators – the basic ones for typical math operations (+, -, *, /) and two special ones: “X % Y” would return the remainder of dividing X by Y, and “-X”, which would return X multiplied by -1. Those can only be used with numeric values.
  • Equality operators – “X == Y” is true, if X and Y have both the same value and type, “X != Y” would return false in this case. This one will work with any type of value.
  • Comparison operators – “<, >, <=, >=” – exclusive to numbers, returns true or false depending on the condition. 
  • Logical operators – the Bool’s algebra part of the pack, work only with the boolean values of true and false.
    – “X || Y” returns true if either X or Y is true, false if any of them is false.
    – “X && Y” returns true only, if both X and Y are true, false if any of them is false.
                – “!X” is true, if X is false, false if X is true.

Conditionals

Sometimes, you might run into a scenario where you’d want the argument value to be different, depending on another value. The conditional syntax is as such:

condition ? true_val : false_val

The condition part is constructed using previously described operators. In this example, the bucket_name value is based on the “test” variable—if it’s set to true, the bucket will be named “dev” and if it’s false, the bucket will be named “prod”:

bucket_name = var.test == true ? "dev" : "prod"

Splat Expressions

Splat expressions are used to extract certain values from complicated collections – like grabbing a list of attributes from a list of objects containing those attributes. Usually you would need an “for” expression to do this, but humans are lazy creatures who like to make complicated things simpler.

For example, if you had a list of objects such as these:

test_variable = [
 {
   name  = "Arthur",
   test  = "true"
 },
 {
   name  = "Martha"
   test  = "true"
 }
]

Instead of using the entire “for” expression:

[for o in var.test_variable : o.name]

you could go for the splat expression form:

var.test_variable[*].name

And in both cases, get the same result:

["Arthur", "Martha"]

Do note, that this behavior applies only if splat was used on a list, set, or tuple. Anything else (Except null) will be transformed into a tuple, with a single element inside, null will simply stay as is. This may be good or bad, depending on your use case. I would suggest you read the official documentation on the topic.

Constraints

In simple terms, constraints regulate what can be what, and where something can, or cannot be used. There are two main types of constraints—for types and versions.

  • Type constraints regulate the values of variables and outputs. For example, a string is represented by anything enclosed in quotes, bool value is either a literal true or false, a list is always opened with square brackets [ ], a map is defined with curly brackets { }.
  • Version constraints usually apply to modules and regulate which versions should or should not be used. See this article to learn more about it: What are Terraform Modules and How to Use Them (Examples)

Terraform Loops

Ah yes, the elephant in the room. Mentioned a few times, but still unexplained—until now.

Terraform loops are used to handle collections, and to produce multiple instances of a resource or module without repeating the code. There are three loops provided by Terraform to date:

Count

Count is the most primitive—it allows you to specify a whole number, and produces as many instances of something as this number tells it to. For example, the following would order Terraform to create ten S3 buckets:

resource "aws_s3_bucket" "test" {
 count = 10
[...]
}

When count is in use, each instance of a resource or module gets a separate index, representing its place in the order of creation. To get a value from a single resource created in this way, you must refer to it by its index value, e.g. if you wished to see the ID of the fifth created S3 bucket, you would need to call it as such:

aws_s3_bucket.test[5].id

Although this is fine for identical, or nearly identical objects, as previously mentioned, count is pretty primitive. When you need to use more distinct, complex values – count yields to for_each.

For_each

As mentioned earlier, sometimes you might want to create resources with distinct values associated with each one – such as names or parameters (memory or disk size for example). For_each will let you do just that. Merely provide a variable—map, or a set of strings, and the resources can access values contained within, via each.key and each.value:

test_map = {
 test1 = "test2",
 test2 = "test4"
}
 
resource "test_resource" "thing" {
 for_each = var.test_map
 
 test_attribute_1 = each.key
 test_attribute_2 = each.value
}

As you can see, for_each is quite powerful, but you haven’t seen the best yet. By constructing a map of objects, you can leverage a resource or module to create multiple instances of itself, each with multiple declared variable values:

my_instances = {
 instance_1 = {
   ami   = "ami-00124569584abc",
   type  = "t2.micro"
 },
 instance_2 = {
   ami   = "ami-987654321xyzab",
   type  = "t2.large"
 },
}
 
resource "aws_instance" "test" {
 for_each = var.my_instances
 
 ami           = each.value["ami"]
 instance_type = each.value["type"]
}

Using this approach, you don’t have to touch anything except the .tfvars file, to provide new instances of resources you have already declared in your configuration. Absolutely brilliant.

If you want to know more about Terraform count and for_each meta-arguments, see: Terraform Count vs. For_Each Meta-Argument: When to Use Them

For

For is made for picking out, iterating over, and operating on things from complex collections. Imagine that you have a list of words (strings), but unfortunately, all of them contain newline characters at the end which you don’t want. Like this:

word_list = [
"Critical\n",
"failure\n", 
"teapot\n", 
"broken\n"
]

To fix this problem, you could do

[for word in var.word_list : chomp(word)]

which would result in:

["Critical", "failure", "teapot", "broken"]

As you can see, a list comes in, a list goes out—but, this is not a must. The type of input, and the brackets which wrap the for expression, determine the type of output it produces. If you had wrapped it with curly brackets, and provided a map as an input, the output would have been a map. Quite interesting. But there’s one thing that’s even more interesting—the for expression can also be used to filter the input as you please. By adding an if clause, you can conditionally operate or not operate on certain values, depending on your defined terms. Take this example, making every word start with a capital letter… except for the word “teapot”:

[for word in var.word_list : upper(word) if word != "teapot"]

For can do all that, and a lot more. Check out this page of the official Terraform documentation if you want to know more – it’s really powerful and can help you with many tasks that lie ahead.

Key Points

Regarding the joke I have mentioned earlier—as you can see from this piece of Terraform syntax pie, HCL is very, very extensive. It couldn’t compete with most programming languages, but when it comes to infrastructure, Terraform usually has just what you need. HCL is also simple to learn, albeit very intricate under the hood—to the point from which to consider it a tiny programming language indeed wouldn’t be much of a stretch. I hope this brief glimpse into the world of Terraform functions, expressions, and loops will help you to bring the awesome powers they provide into your configurations. Have fun, and have a productive day.

Discover better way to manage Terraform

Spacelift helps manage Terraform state, build more complex workflows, supports policy as code, programmatic configuration, context sharing, drift detection, resource visualization and many more.

Start free trial