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 to dev
  • The locals block contains an object called ec2, nested in this is an object called dev, 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 within local.ec2 for the value of var.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.

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 called vpc, nested in this is an object called dev, 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 use each.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 return eu-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 return arryw-web.
  • The subnets set is a collection of unique values, and you reference the values within it by their index, local.subnets[0] would return subnet-aaaaaaa123example.