Terraform Functions, Expressions, Loops

terraform functions

The native language of Terraform, called HCL, can be an extremely comfortable 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 regular programming. In this article, we will discuss the core of HCL – built-in functions, expressions and loops. What are those, and what are they for? Let’s talk about that.

Terraform Functions

What are Terraform Functions? Functions are a mean of manipulating the data, and provide you with possibilities to combine, transform or operate in other way, on provided values. For example, a “join” function looks like this:

join(separator, list)

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

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

The resulting output will be:


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), corrections (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 pretty extensive – but it is also finite. Terraform does not allow you to create your own functions, so you’re bound to use what is provided by default.

Terraform Expressions

Expressions are the core of HCL itself – the logic muscle of the entire language. They 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, let me tell you about the most notable/interesting expressions in my humble opinion: Operators, Conditional expressions, Splat Expressions, Constraints, and Loops.


Dedicated to logical comparison and arithmetic operations, operators are mostly used for doing math and basic Bool’s algebra. The way to go, if you need to know, if number A does equal number B, add them together, or know if both boolean A and boolean B are “true”. The following operators are available in Terraform:

  • 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” that 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. 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. They 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.


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

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 beings, we like to do complicated things easier.

For example, if you had a list of objects like this:

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:


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 just stay as it is. This may be good or bad, depending on your use case. I would suggest you read the official documentation on the topic.


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. We have already explained those in the article about Terraform Modules – make sure to check it out! 

Terraform Loops

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

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 is the most primitive one – it allows you to specify a whole number, and produces as many instances of something, as this number tells it to. For example, this 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 this way, you must refer to it by its index value, e.g. if you’d wish to see the ID of the fifth created S3 bucket, you would need to call it as such:


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


As mentioned earlier, sometimes you might want to create resources with distinct values associated with each one – such as names or parameters (For example, memory or disk size). For_each will let you do just that, just 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 pretty 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 awesome. 


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 to have. Like this:

word_list = [

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, list went in, list goes out – but, this is not a must. The type of input, and brackets wrapping the for expression around, decide about the type of output it does produce. If you’d wrap it with curly brackets, and provide a map as an input, the output would be a map. Quite interesting. But there’s one thing that’s even more interesting – 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 if the word is “teapot”:

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

For can do all that, and much, much 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 ahead.


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 most usually has what you need. HCL is also simple to learn, but 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 little 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.

Share this post

twitter logo