In today’s blog post, we will learn working with lists in terraform. In particular, defining lists and tuples, accessing list elements, terraform list functions to get the length of a list, merge two lists into one, remove duplicates, sort and reverse, split a list, compare and find the difference between two lists, checking if an item exists in a list, getting specific item from a list based on condition, and best practices. Hence without any further delay, let us get started.
Defining Lists and Tuples
In Terraform, both lists and tuples represent ordered sequences of elements. The primary difference lies in their type constraint strictness:
- List (or
list
type): A collection where all elements are expected to be of the same type, though Terraform is often flexible and can implicitly convert. It is typically used for variable-length collections where the number of elements might change. - Tuple (or
tuple
type): A fixed-length, fixed-type collection where each element can have a distinct type. Tuples are less common for general resource definitions but are powerful for representing structured records.
For most common use cases, you will primarily interact with the list
type.
Defining a List
You can define a list by enclosing its elements in square brackets []
, separated by commas.
# main.tf
# Define a list of strings
variable "instance_names" {
description = "A list of names for EC2 instances."
type = list(string)
default = ["web-server-01", "db-server-01", "app-server-01"]
}
# Define a list of numbers
variable "ports" {
description = "A list of port numbers."
type = list(number)
default = [80, 443, 22]
}
# Define a list of mixed types (though less common for type-constrained variables)
output "mixed_list_example" {
value = ["hello", 123, true, null]
# Outcome: ["hello", 123, true, null] (type becomes list(any))
}
Explanation of the code:
- The
instance_names
variable is a list of strings, providing names that can be used to create multiple instances. - The
ports
variable is a list of numbers, useful for defining security group rules. - The
mixed_list_example
output shows a list with elements of different types. When a variable’s type constraint islist(any)
, it allows elements of any type.
Defining a Tuple
While less frequently explicitly defined in variables, tuples are common for fixed, ordered sets of heterogeneous data.
# main.tf
# Define a tuple type for a network interface configuration
variable "network_config" {
description = "A tuple containing IP and port for a service."
type = tuple([string, number]) # Tuple of string and number
default = ["192.168.1.100", 8080]
}
# Accessing tuple elements
output "config_ip" {
value = var.network_config[0]
# Outcome: "192.168.1.100"
}
output "config_port" {
value = var.network_config[1]
# Outcome: 8080
}
Explanation of the code:
- The
network_config
variable is explicitly defined as a tuple with a string and a number. This ensures a fixed structure for the configuration. - Individual elements of the tuple can be accessed using zero-based indexing.
Accessing List Elements
You can retrieve specific elements from a list or tuple using their zero-based index or the element
function.
# main.tf
# Accessing a list element by index
variable "my_list" {
type = list(string)
default = ["apple", "banana", "cherry"]
}
output "first_fruit" {
description = "The first element of the list."
value = var.my_list[0]
# Outcome: "apple"
}
output "second_fruit" {
description = "The second element of the list."
value = var.my_list[1]
# Outcome: "banana"
}
# Using the element function (useful with count or for robustness)
output "fruit_by_element_function" {
description = "Accessing an element using the element function."
value = element(var.my_list, 2)
# Outcome: "cherry"
}
Explanation of the code:
- Direct indexing (
var.my_list[0]
) is the most common way to access elements. - The
element(list, index)
function serves a similar purpose but can be more robust in certain dynamic scenarios, especially when combined withcount
.
Common List Functions
Terraform provides several built-in functions to manipulate lists effectively.
Getting the Length of a List
The length
function returns the number of elements in a list or tuple.
# main.tf
variable "regions" {
type = list(string)
default = ["us-east-1", "eu-west-1", "ap-southeast-2"]
}
output "number_of_regions" {
description = "The number of regions in the list."
value = length(var.regions)
# Outcome: 3
}
Explanation of the code:
- The
length
function is useful for determining how many resources or configurations need to be created when usingcount
with a list.
Merging Lists
The concat
function joins two or more lists into a single list.
# main.tf
variable "dev_ips" {
type = list(string)
default = ["10.0.1.10", "10.0.1.11"]
}
variable "prod_ips" {
type = list(string)
default = ["10.0.2.10", "10.0.2.11"]
}
output "all_ips" {
description = "Combined list of development and production IPs."
value = concat(var.dev_ips, var.prod_ips, ["10.0.3.1"]) # Can merge more than two
# Outcome: ["10.0.1.10", "10.0.1.11", "10.0.2.10", "10.0.2.11", "10.0.3.1"]
}
Explanation of the code:
concat
is valuable for combining different sets of configurations or merging default values with environment-specific overrides.
Flattening Nested Lists
The flatten
function takes a list of lists and flattens it into a single list.
# main.tf
variable "nested_subnets" {
type = list(list(string))
default = [
["subnet-a", "subnet-b"],
["subnet-c", "subnet-d"]
]
}
output "flat_subnets" {
description = "A flattened list of subnet IDs."
value = flatten(var.nested_subnets)
# Outcome: ["subnet-a", "subnet-b", "subnet-c", "subnet-d"]
}
Explanation of the code:
flatten
is particularly useful when dealing with outputs from modules that might return nested lists, and you need a single, linear list for further processing.
Removing Duplicates
The distinct
function returns a new list containing only the unique elements from the input list, preserving the original order of the first appearance.
# main.tf
variable "roles" {
type = list(string)
default = ["admin", "viewer", "admin", "editor", "viewer"]
}
output "unique_roles" {
description = "List of distinct roles."
value = distinct(var.roles)
# Outcome: ["admin", "viewer", "editor"]
}
Explanation of the code:
distinct
ensures that you are working with only unique values, preventing redundant resource creation or configuration issues.
Sorting and Reversing Lists
The sort
function sorts elements alphabetically or numerically, and the reverse
function reverses the order of elements in a list.
# main.tf
variable "unsorted_names" {
type = list(string)
default = ["zebra", "apple", "banana"]
}
variable "unsorted_numbers" {
type = list(number)
default = [5, 1, 9, 2]
}
output "sorted_names" {
description = "Alphabetically sorted list of names."
value = sort(var.unsorted_names)
# Outcome: ["apple", "banana", "zebra"]
}
output "sorted_numbers" {
description = "Numerically sorted list of numbers."
value = sort(var.unsorted_numbers)
# Outcome: [1, 2, 5, 9]
}
output "reversed_names" {
description = "List of names in reverse order."
value = reverse(var.unsorted_names)
# Outcome: ["banana", "apple", "zebra"] (original order was "zebra", "apple", "banana", reverse applies to current order)
}
Explanation of the code:
sort
is useful for standardizing lists or when you need elements processed in a specific order.reverse
provides a way to simply invert the order of elements.
Slicing Lists
The slice
function extracts a sub-list from a given list, similar to slicing in other programming languages.
# main.tf
variable "full_list" {
type = list(string)
default = ["a", "b", "c", "d", "e"]
}
output "slice_from_start" {
description = "Elements from index 0 up to (but not including) index 2."
value = slice(var.full_list, 0, 2)
# Outcome: ["a", "b"]
}
output "slice_from_middle" {
description = "Elements from index 1 up to (but not including) index 4."
value = slice(var.full_list, 1, 4)
# Outcome: ["b", "c", "d"]
}
output "slice_to_end" {
description = "Elements from index 2 to the end."
value = slice(var.full_list, 2, length(var.full_list))
# Outcome: ["c", "d", "e"]
}
Explanation of the code:
slice
allows you to extract specific portions of a list, defined by a starting index and an ending index (exclusive).
Set Operations
For lists that are treated as mathematical sets (where order and duplicate elements do not matter), Terraform provides functions for common set operations. These functions normalize the input lists into sets internally before performing the operation.
Union of Lists
The setunion
function returns a list containing all unique elements that are present in any of the input lists.
# main.tf
variable "group_a_users" {
type = list(string)
default = ["alice", "bob", "charlie"]
}
variable "group_b_users" {
type = list(string)
default = ["bob", "diana", "eve"]
}
output "all_unique_users" {
description = "Union of users from both groups."
value = setunion(var.group_a_users, var.group_b_users)
# Outcome: ["alice", "bob", "charlie", "diana", "eve"] (order might vary)
}
Explanation of the code:
setunion
is useful for combining distinct elements from multiple sources, like combining all users across different access groups.
Intersection of Lists
The setintersection
function returns a list containing only the unique elements that are common to all input lists.
# main.tf
output "common_users" {
description = "Users present in both groups."
value = setintersection(var.group_a_users, var.group_b_users)
# Outcome: ["bob"] (order might vary)
}
Explanation of the code:
setintersection
helps identify common elements between different lists, such as users with access to multiple systems.
Difference Between Lists
The setdifference
function returns a list containing unique elements from the first list that are not present in any of the subsequent lists.
# main.tf
output "users_only_in_group_a" {
description = "Users present in group A but not in group B."
value = setdifference(var.group_a_users, var.group_b_users)
# Outcome: ["alice", "charlie"] (order might vary)
}
Explanation of the code:
setdifference
is useful for finding elements unique to one list when compared to others, for example, identifying users who lost access or resources that are no longer needed.
Checking for Membership
The contains
function checks if a list or set contains a specific element.
# main.tf
variable "allowed_zones" {
type = list(string)
default = ["us-east-1a", "us-east-1b"]
}
output "is_zone_allowed_a" {
description = "Checks if 'us-east-1a' is in allowed zones."
value = contains(var.allowed_zones, "us-east-1a")
# Outcome: true
}
output "is_zone_allowed_c" {
description = "Checks if 'us-east-1c' is in allowed zones."
value = contains(var.allowed_zones, "us-east-1c")
# Outcome: false
}
Explanation of the code:
contains
is a simple boolean check, very useful in conditional expressions or for validating inputs against a predefined list of allowed values.
Transforming and Filtering with For Expressions
The for
expression can be used for iterating over lists, transforming elements, and filtering results. It allows you to create new lists or maps based on existing collections.
Basic Transformation
# main.tf
variable "server_names" {
type = list(string)
default = ["web", "app", "db"]
}
output "formatted_server_ids" {
description = "List of formatted server IDs."
value = [for name in var.server_names : "${name}-server-id"]
# Outcome: ["web-server-id", "app-server-id", "db-server-id"]
}
Explanation of the code:
- This
for
expression iterates over eachname
invar.server_names
and creates a new string for each item, resulting in a new list of formatted IDs.
Filtering Elements
# main.tf
variable "all_ports" {
type = list(number)
default = [80, 443, 22, 8080, 3306]
}
output "http_https_ports" {
description = "List containing only HTTP and HTTPS ports."
value = [for port in var.all_ports : port if contains([80, 443], port)]
# Outcome: [80, 443]
}
Explanation of the code:
- This
for
expression iterates overall_ports
but includesport
in the new list only if it is present in the[80, 443]
list, effectively filtering the original list.
Practical Use Cases
- Creating Multiple Resources: Use
count
withlength(var.list)
andelement(var.list, count.index)
to create identical resources with names or other properties derived from list elements. Even better, usefor_each
with a set of strings or a map. - Dynamic Security Group Rules: Define ingress/egress rules based on lists of CIDR blocks or port ranges, using
for_each
onaws_security_group_rule
for each list element. - Module Inputs/Outputs: Pass lists of IDs or names between modules to manage dependencies and organize infrastructure.
Best Practices for List Manipulation
- Type Consistency: While Terraform is flexible, explicitly defining the type of your lists (e.g.,
list(string)
) in variables improves readability and helps catch errors early. - Readability: For complex transformations, break down
for
expressions or chained function calls into local values for better clarity. for_each
vs.count
: For creating multiple resources based on a list,for_each
is often preferred overcount
when the order of elements might change or you need more robust state management (e.g., when removing an item from the middle of the list,for_each
manages resource lifecycle better). However,count
is still useful for simple, numerically indexed repetitions.- Null Values: Be mindful of
null
values within lists, as some functions might behave unexpectedly or require explicit handling. - Comments: Add clear comments for complex list manipulation logic, explaining the intent and expected outcome.
Conclusion
Mastering list and tuple manipulation in Terraform is an important skill for building dynamic and scalable infrastructure. By effectively using built-in functions like length
, concat
, distinct
, and sort
, along with the powerful for
expressions for transformation and filtering, you can write more concise, reusable, and adaptable Terraform configurations.
Author

Experienced Cloud & DevOps Engineer with hands-on experience in AWS, GCP, Terraform, Ansible, ELK, Docker, Git, GitLab, Python, PowerShell, Shell, and theoretical knowledge on Azure, Kubernetes & Jenkins.
In my free time, I write blogs on ckdbtech.com