Terraform configurations are written with the HashiCorp Configuration Language (HCL). HCL contains many built-in functions to help you build dynamic configurations and to simplify common data manipulation tasks.
In this article, we cover:
If the framework you work with offers few built-in functions (e.g., AWS CloudFormation), you will appreciate the vast selection of functions available with Terraform.
The built-in functions are grouped into categories. A few of the most common categories are:
- String functions (
join
,split
,startswith
, regex, and more) - File system functions (
file
,fileexists
,basename
, and more) - IP network functions (
cidrhost
,cidrsubnet
, and more)
There is also a category called collection functions. Collection functions allow you to work with collection types (lists, maps, sets) as well as structural types (objects and tuples).
Read more: Terraform Functions, Expressions, Loops (Examples)
In this blog post, we will learn more about one of the collection-type functions: the element
function.
element
is a Terraform function used to select an element from a collection of values. It’s particularly useful when you want to cycle through a list or select an item based on a variable index.
This function takes two arguments:
element(<collection>, <index>)
<collection>
– The first argument is the list or tuple that you want to select an element from.-
<index>
– The second argument is the index of the element you want to select. The index has to be zero or greater (with a max value of 9223372036854775807, which you are unlikely to reach).
The element function provides an alternative way to access elements in a collection. The common way to select elements from a collection is the square-bracket index notation [index]
, e.g. my_collection[0]
to select the first element from the collection my_collection
.
Before we learn how the element
function works, it helps to understand the types of values that the element
function is compatible with.
A list in Terraform is a sequence of values of the same type. Its elements are ordered with an index starting at zero.
A HCL list is represented by wrapping its values in a pair of square brackets and placing commas between the values. Here is an example of a list:
["this", "is", "a", "list"]
Terraform also has a data type called tuples. Tuples are often interchangeable with lists. However, tuple values do not need to be of the same type. Here is an example of a tuple:
["this", 1, "is a tuple", true]
The distinctions between lists and tuples in Terraform are important when you specify type constraints for input variables. Apart from that, there is no notable difference.
It is not possible to write a literal list in HCL. To create a list, you have to use the tolist
function. Using terraform console
, we can see this by checking the type of the literal value [1,2,3]
:
$ terraform console
> type([1,2,3])
tuple([
number,
number,
number,
])
>
> type(tolist([1,2,3]))
list(number)
$ terraform console
> toset([1,2,2,3,3,4])
toset([
1,
2,
3,
4,
])
What does the element
function have to do with lists, tuples, and sets?
We can use terraform console
again to run a few examples of using the element
function for a tuple, a list, and a set:
$ terraform console
> element(["first", "second", "third"], 0)
"first"
>
> element(tolist(["first", "second", "third"]), 0)
"first"
>
> element(toset(["first", "second", "third"]), 0)
Error: Error in function call
Call to function "element" failed: cannot read elements from set of string.
This example shows that the element
function can be used for both lists and tuples, but not for sets. This is due to the unordered nature of sets.
The element
function is deterministic, meaning that for the same input, it will always produce the same output. Using the element
function on sets would not fulfill the deterministic nature of the function.
For the input values of lists and tuples, the element function selects the desired element from the collection.
$ terraform console
> element(["first", "second", "third"], 0)
"first"
> element(["first", "second", "third"], 1)
"second"
> element(["first", "second", "third"], 2)
"third"
For lists and tuples, you can also select elements using the square-bracket index notation, e.g., mylist[0]
. Here is an example:
$ terraform console
> ["first", "second", "third"][0]
"first"
> ["first", "second", "third"][1]
"second"
> ["first", "second", "third"][2]
"third"
Why do we need the element
function when it is more concise to use the square-bracket index notation to access elements of lists and tuples?
The element
function has one powerful feature: If the index
argument exceeds the length of the collection, it automatically wraps the index
using modulo arithmetic.
This means that if your list has a length of N after the index exceeds the length of the collection, for example, all these indexes will do a modulo N operation. Modulo is a function that returns the division remainder.
If you have a list with a length of N (where N is greater than 2) and your index is N (which exceeds the length of the collection; remember, we start indexes from 0), the operation that happens for this index is N % N, which will always return 0. If the index is N+1, the operation that happens for this index is (N+1) % N, which will return 1.
You can keep increasing the value of the index
argument, and you will keep looping through the values in the collection:
$ terraform console
> element(["first", "second", "third"], 0)
"first"
> element(["first", "second", "third"], 1)
"second"
> element(["first", "second", "third"], 2)
"third"
> element(["first", "second", "third"], 3)
"first"
> element(["first", "second", "third"], 4)
"second"
Let’s suppose you are working with AWS and you want to create a Virtual Private Cloud (VPC) resource with a large number of subnets.
An overview of the architecture you have in mind is this:
You have a VPC resource with subnets that should be spread across all the availability zones of the AWS region. Remember, an AWS region is a collection of data centers somewhere in the world (e.g. the eu-west-1
region is located in Ireland). The data centers are spread out at some distance from each other in distinct zones called availability zones.
The challenge is that for all AWS regions, the number of availability zones might differ.
Your Terraform configuration has two input variables:
- A
string
variable namedaws_region
that takes the name of an AWS region to create the VPC and subnets in - A
number
variable namednumber_of_subnets
that takes an integer representing how many subnets to create
The variables are defined in variables.tf
:
variable "aws_region" {
type = string
}
variable "number_of_subnets" {
type = number
}
In main.tf
you add the definition of your VPC resource:
resource "aws_vpc" "this" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "vpc-spacelift-${var.aws_region}"
}
}
You want the subnets of the VPC to be spread evenly across all the availability zones of the selected AWS region. You could define a static map where you specify how many availability zones each AWS region has.
However, you should instead use the aws_availability_zones
data source to query AWS for all availability zones in the region.
You add the following data source to main.tf
:
data "aws_availability_zones" "available" {
state = "available"
}
To evenly distribute the subnets across the availability zones, you utilize the element
function in the availability_zone
argument for the subnet resources. You then use the count
meta-argument to create the correct number of subnets.
You add the following resource to main.tf
:
resource "aws_subnet" "all" {
count = var.number_of_subnets
vpc_id = aws_vpc.this.id
cidr_block = cidrsubnet(aws_vpc.this.cidr_block, 8, count.index)
availability_zone = element(
data.aws_availability_zones.available.names,
count.index
)
tags = {
Name = "subnet-spacelift-${var.aws_region}-${count.index}"
}
}
To complete the Terraform configuration, you add a file named providers.tf
with the following content:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.72"
}
}
}
provider "aws" {
region = var.aws_region
}
Run terraform init
to initialize the configuration:
$ terraform init
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.72"...
- Installing hashicorp/aws v5.72.1...
- Installed hashicorp/aws v5.72.1 (signed by HashiCorp)
Terraform has been successfully initialized!
Create a Terraform variables file named terraform.tfvars
with the following content:
aws_region = "eu-west-1"
number_of_subnets = 10
Apply the configuration to create the VPC and subnets:
$ terraform apply -auto-approve
data.aws_availability_zones.available: Reading...
data.aws_availability_zones.available: Read complete after 1s [id=eu-west-1]
# … output truncated
Plan: 11 to add, 0 to change, 0 to destroy.
# … output truncated
aws_subnet.all[9]: Creation complete after 1s [id=subnet-0b1dd747dd8d4670d]
aws_subnet.all[7]: Creation complete after 1s [id=subnet-0992224201fe81b62]
aws_subnet.all[8]: Creation complete after 1s [id=subnet-015ff3dda00316f9d]
aws_subnet.all[1]: Creation complete after 1s [id=subnet-0215b9759d0e06d27]
aws_subnet.all[4]: Creation complete after 1s [id=subnet-024eb04f6450154bc]
aws_subnet.all[3]: Creation complete after 1s [id=subnet-085614a12f90eb5f4]
aws_subnet.all[5]: Creation complete after 1s [id=subnet-035e2a940c177f277]
aws_subnet.all[0]: Creation complete after 1s [id=subnet-01cf051cbbd51cc70]
aws_subnet.all[6]: Creation complete after 1s [id=subnet-08100f75f5aa53acd]
aws_subnet.all[2]: Creation complete after 1s [id=subnet-03ac21111fb211a6d]
Apply complete! Resources: 11 added, 0 changed, 0 destroyed.
All the subnets have been created successfully.
Go to the AWS console, select the VPC service, and find the VPC named vpc-spacelift-eu-west-1
. Open the VPC resource map to verify there are ten subnets spread evenly across the availability zones:
The eu-west-1
region has three availability zones. The first availability zone has four subnets, and each of the other two has three subnets — leaving a total of ten subnets, as we expected.
To verify that the solution works for an AWS region with a different number of subnets, first run terraform destroy
for the current configuration. Next, update terraform.tfvars
to the following:
aws_region = "us-east-1"
number_of_subnets = 10
Run another terraform apply
to create the VPC and subnets in the us-east-1
region.
Go to the AWS console and open the VPC service in the us-east-1
region. Select the vpc-spacelift-us-east-1
VPC and look at the resource map view:
The us-east-1
region has six availability zones. The first four zones contain two subnets each, while the last two availability zones contain one subnet each. Again, this leaves a total of ten subnets, as expected.
In general, the element function should be used when you want to distribute something evenly (e.g. subnets) across something else (e.g. availability zones). Examples include:
- Evenly distributing AWS EC2 instances across VPC subnets.
- Evenly distributing certain tag values for a collection of resources.
- Evenly distribute deployments across multiple Kubernetes namespaces.
- Evenly deploy modules across different AWS regions.
- If you want to assign each value from a list once, you can access elements from the list using the normal square bracket index notation.
Terraform is really powerful, but to achieve an end-to-end secure Gitops approach, you need to use a product that can run your Terraform workflows. Spacelift takes managing Terraform to the next level by giving you access to a powerful CI/CD workflow and unlocking features such as:
- Policies (based on Open Policy Agent) – You can control how many approvals you need for runs, what kind of resources you can create, and what kind of parameters these resources can have, and you can also control the behavior when a pull request is open or merged.
- Multi-IaC workflows – Combine Terraform with Kubernetes, Ansible, and other infrastructure-as-code (IaC) tools such as OpenTofu, Pulumi, and CloudFormation, create dependencies among them, and share outputs
- Build self-service infrastructure – You can use Blueprints to build self-service infrastructure; simply complete a form to provision infrastructure based on Terraform and other supported tools.
- Integrations with any third-party tools – You can integrate with your favorite third-party tools and even build policies for them. For example, see how to Integrate security tools in your workflows using Custom Inputs.
Spacelift enables you to create private workers inside your infrastructure, which helps you execute Spacelift-related workflows on your end. For more information on configuring private workers, refer to the documentation.
You can check it for free by creating a trial account or booking a demo with one of our engineers.
In this article we explained the Terraform element function. Here are the key takeaways:
- A list is a collection of ordered values of the same type.
- A tuple is a collection of ordered values of different (or the same) types.
- You can select elements from a list or tuple using the square-bracket index selector syntax (e.g.
my_list[0]
,my_tuple[1]
, etc.). - The
element
function can work with lists and tuples, and it has the same basic functionality as the square-bracket index selector. - The
element
function takes two arguments, e.g.element(<collection>, <index>
):- A
<collection>
, either a list or a tuple, to select an element from. - An
<index>
of the element to select. The index is an integer and has to be 0 or greater.
- A
- A powerful feature of the
element
function is that if theindex
argument exceeds the length of the collection, it automatically wraps theindex
using modulo arithmetic. - The generic use case for the
element
function is to evenly distribute something (e.g. subnets) across something else (e.g. availability zones).
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.