[November 20 Webinar] Detecting & Correcting Infrastructure Drift

➡️ Register Now

Terraform

Terraform Element Function – How It Works & Use Cases

terraform element

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:

  1. Overview of Terraform functions
  2. What is the element function?
  3. How does the element function work?
  4. Terraform element function use case example

Overview of Terraform functions

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.

What is the Terraform 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.

How does the element function work?

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.

terraform list

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)

There is also a set collection type. A set is similar to a list, but each value has to be unique. There is no guaranteed order for the elements of a set.

As with lists, it is not possible to create a literal set in HCL. Instead, you have to use the toset function.

$ 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"

Terraform element function use case example

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:

architecture for terraform element function example

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 named aws_region that takes the name of an AWS region to create the VPC and subnets in
  • A number variable named number_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.

Deploying Terraform resources with Spacelift

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.

Key points

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 powerful feature of the element function is that if the index argument exceeds the length of the collection, it automatically wraps the index 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.

Learn more

The Practitioner’s Guide to Scaling Infrastructure as Code

Transform your IaC management to scale

securely, efficiently, and productively

into the future.

ebook global banner
Share your data and download the guide