Chapter 7: Complex Terraform Data Types
Complex data types
Primitive data types in Terraform, such as strings, numbers, and booleans, are often sufficient for many use cases. These data types are simple and can be used to represent basic values like names, numbers, and true/false values. However, there may be situations where you need to represent more complex data structures in your Terraform configuration, such as lists of objects or nested maps. For these cases, you can use complex data types like lists, maps, and objects. Complex data types allow you to represent more advanced data structures and can be useful in situations where you need to repeat a block of code multiple times, nest data structures, or validate the data type and structure of a variable.
Let's list a few reasons why you might need to use complex data types in Terraform with examples:
Repetition: If you need to repeat a block of code multiple times, with each iteration having slightly different values, you can use lists or maps to represent the data for each iteration. For example, suppose you want to create multiple Azure resource groups, with each resource group having a different name and location. You can use a list or map to store the names and locations of each resource group, and then use the count
or for_each
arguments to iterate over the list or map and create the resource groups. The count
argument allows you to specify the number of times a block of code should be repeated, while the for_each
argument allows you to iterate over a map or list and access the values for each element.
Here is an example of using a list to create multiple Azure resource groups with different names and locations with count
:
# List of resource group names and locations
variable "resource_groups" {
type = list(object({
name = string
location = string
}))
default = [
{
name = "rg-exmaple-neu-001"
location = "north europe"
},
{
name = "rg-exmaple-weu-001"
location = "west europe"
}
]
}
resource "azurerm_resource_group" "example" {
count = length(var.resource_groups)
name = var.resource_groups[count.index].name
location = var.resource_groups[count.index].location
}
The same example can also be done by a for_each
loop as below with a map(object) type variable:
variable "resource_groups2" {
type = map(object({
location = string
}))
default = {
"rg-exmaple-neu-002" = {
location = "north europe"
},
"rg-exmaple-weu-002" = {
location = "west europe"
},
}
}
resource "azurerm_resource_group" "example2" {
for_each = var.resource_groups2
name = each.key
location = each.value.location
}
Nesting: If you need to represent data structures that are nested, such as a list of maps or an object within an object, you can use complex data types like lists, maps, and objects. For example, suppose you want to create a list of Azure virtual machines, where each virtual machine has its own name, location, size, and data disk layout. You can use a list of objects to represent this data structure, with each object containing the name, location, size, and data disk of a single virtual machine. This allows you to nest the data for each virtual machine within the list, making it easy to access and manage.
# Map of objects for virtual machines
variable "vms" {
type = map(object({
location = string
resource_group_name = string
data_disks = list(object({
name = string
disk_size_gb = number
}))
}))
default = {
"exmaple-vm-001" = {
location = "north-europe"
resource_group_name = "rg-example-neu-001"
data_disks = [
{
disk_size_gb = 100
name = "data-disk-001"
},
{
disk_size_gb = 200
name = "data-disk-002"
}
]
},
"exmaple-vm-002" = {
location = "west-europe"
resource_group_name = "rg-example-weu-001"
data_disks = [
{
disk_size_gb = 100
name = "data-disk-001"
}
]
}
}
}
Validation: Complex data types can also be useful for validation, as you can use the type
argument to specify the exact data type and structure that is expected for a given variable. This can help ensure that your configuration is syntactically correct and that you are using the correct data types for your resources. For example, if you specify that a variable is a list of strings, Terraform will validate that the values in the list are strings and will return an error if any values are of a different type. This can help prevent errors and ensure that your configuration is correct before you apply it.
Dynamic Blocks
In Terraform, we organize our infrastructure using blocks. These blocks can contain other blocks, and the number of blocks can be dynamic depending on the requirements. As an example, consider the azurerm_virtual_network
resource. The official documentation states that the subnet
argument can be specified multiple times to create multiple subnets.
Below is an example taken from the official documentation of the azurerm_virtual_network
resource, with a few minor modifications.
resource "azurerm_virtual_network" "example" {
name = "example-network"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
address_space = var.adress_space
dns_servers = var.dns_servers
subnet {
name = var.subnet1.name
address_prefix = var.subnet1.address_prefix
}
subnet {
name = var.subnet2_name
address_prefix = var.subnet2_address_prefix
}
}
In this example, we have defined two subnets. But what if we need to have two subnets in our development environment and three subnets in our production environment? This is where dynamic blocks come in handy. With dynamic blocks, you can provide the flexibility to have at least one subnet, but also allow for additional subnets to be added as needed. You can implement this functionality using dynamic blocks in the following way:
provider "azurerm" {
features {}
}
variable "address_space" {
type = list(string)
default = ["10.0.0.0/16"]
}
variable "dns_servers" {
type = list(string)
default = ["10.0.0.4", "10.0.0.5"]
}
resource "azurerm_resource_group" "example" {
name = "rg-dynamic-blocks-001"
location = "North Europe"
}
variable "default_subnet_name" {
type = string
default = "subnet1"
}
variable "default_subnet_address_prefix" {
type = string
default = "10.0.1.0/24"
}
variable "additional_subnets" {
description = "list of values to assign to subnets"
type = list(object({
name = string
address_prefix = string
}))
default = []
}
resource "azurerm_virtual_network" "example" {
name = "example-network"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
address_space = var.address_space
dns_servers = var.dns_servers
# Required subnet
subnet {
name = var.default_subnet_name
address_prefix = var.default_subnet_address_prefix
}
# Optinal subnet(s)
dynamic "subnet" {
for_each = var.additional_subnets
iterator = subnet
content {
name = subnet.value.name
address_prefix = subnet.value.address_prefix
}
}
}
In this example, we establish a virtual network and within that network resource, we define one mandatory subnet and any additional optional subnets using a loop with a dynamic block. This approach allows us to create different numbers of subnets per environment while using the same resource structure, which makes the code more readable for reviewers. Additionally, by keeping the default value as "none", we can skip providing a value to the "additional_subnets" variable if we do not intend to create any additional subnets.
Last updated