Terraform, Objects, Maps, Lists, Sets & Tuples
A quick look at the different data structures in Terraform, with some examples of how to use them
Updated April, 2024
This is part of a collection of articles covering an Introduction to Advanced Terraform, it is deliberately of a certain style, you can find out more in the first post.
Introduction
Terraform has a few different data structures that you can use to define your infrastructure. These are Objects, Maps, Lists, Sets and Tuples. This series of articles is opinionated, meaning that I'm going to recommend a way to do things so that we can keep moving quickly, you don't have to do it this way, but I think it's a good way to do things. We will mostly start out with Objects and Tuples.
Objects
Objects are a collection of key-value pairs. You can define an object in Terraform like this, we're going to put them into a locals block and build it so that more environments could be added later, but for now we'll just have a dev environment. I've included some code to show what other pieces of code are needed to make it referenceable.
variable "environment" {
description = "The environment to deploy to"
default = "dev"
}
locals {
ec2 = {
dev = {
arryw-web = {
name = "arryw-web"
instance_type = "t4g.small"
aws_region = "eu-west-1"
associate_public_ip_address = true
root_volume_size = 20
}
}
}
}
resource "aws_instance" "ec2_dub" {
for_each = {
for k, v in local.ec2[var.environment] : k => v
if v.aws_region == "eu-west-1"
}
provider = "aws.dublin"
name = each.key
ami = "ami-0c55b15example1f0"
instance_type = each.value.instance_type
key_name = "arryw"
subnet_id = "subnet-0bb1c79de3example"
associate_public_ip_address = try(each.value.associate_public_ip_address, false)
root_block_device {
volume_size = try(each.value.root_volume_size, 8)
volume_type = try(each.value.root_volume_type, "gp3")
}
}
In the above code:
- The
environment
variable is created and set todev
- The
locals
block contains an object calledec2
, nested in this is an object calleddev
, within this is our ec2 instance object,arryw-web
. While being complex, I think this is still readable and allows for easy expansion. - The resource block is an example of how the values with the
dev
object can be referenced.- The
for_each
says look withinlocal.ec2
for the value ofvar.environment
and take the object(s) within that as your data source. - A value of
each.value.something
is how you reference the values within the object. - The
try
function is used to provide a default value if the key doesn't exist, which means you don't have to specify every value for every object.
- The
Tuples
In Terraform a List is a list of values that are all the same type, I rarely use these but they do exist, a Tuple (more common) is a list of values that can be different types. Here's an example of a tuple and how you might use it:
locals {
vpc = {
dev = {
# vpc_reference = [0:aws_region, 1:cidr_block, 2:private_subnet_cidr_blocks, 3:public_subnet_cidr_blocks, 4:enable_dns_support]
arryw-dublin = ["eu-west-1", "10.0.0.0/24", ["10.0.0.0/26", "10.0.0.64/26"], ["10.0.0.128/26", "10.0.0.192/26"], true]
}
}
}
resource "aws_vpc" "dublin" {
for_each = {
for k, v in local.vpc[var.environment] : k => v
if v.0 == "eu-west-1"
}
provider = aws.dublin
cidr_block = each.value[1]
enable_dns_hostnames = true
enable_dns_support = each.value[4]
tags = {
Name = "${each.key}-vpc"
}
}
In the above code:
- The
locals
block contains an object calledvpc
, nested in this is an object calleddev
, within this is our VPC tuple,arryw-dublin
. - This is a Tuple because the first and second values are strings, the third and fourth values are lists and the fifth value is a boolean.
- The difference in how these are referenced from the previous example is that
each.value
is a list, so you reference the values within it by their index.each.value[0]
is how you reference the first value in the tuple, if you wanted to reference the first private subnet CIDR block you would useeach.value[2][0]
or value 2, index 0.
Lists, Maps and Sets
I don't directly use these very often, by that I mean, my locals definitions are not laid out that way, you will see in later articles that we do create maps by data manipulation, so these are useful to know about.
A List is a list of values that are all the same type, a Map is an ordered collection of key-value pairs, and a Set is a collection of unique values. Here's an example of each:
locals {
# List
availability_zones = ["eu-west-1a", "eu-west-1b", "eu-west-1c"]
# Map
tags = {
Name = "arryw-web"
Environment = "dev"
}
# Set
subnets = toset(["subnet-aaaaaaa123example", "subnet-bbbbbbb456example", "subnet-ccccccc789example"])
}
In the above code:
- The
availability_zones
list is a list of strings, and you reference the values within it by their index,local.availability_zones[0]
would returneu-west-1a
. - The
tags
map is a collection of key-value pairs, and you reference the values within it by their key,local.tags.Name
would returnarryw-web
. - The
subnets
set is a collection of unique values, and you reference the values within it by their index,local.subnets[0]
would returnsubnet-aaaaaaa123example
.