skip to content

Terraform Variables by Example

Take a closer look at Terraform variable types and how to use them for complex operations.

Terraform Variables

Although I touched briefly on Terraform variables in my Getting Started with Terraform post, I wanted to take a deeper dive in to the different types of variables Terraform supports, as well as look at how to use them for complex operations.

Primitive vs Complex Variables

Terraform has two main types of variables: primitive and complex. Primitive variables are simple values like strings, numbers, and booleans. Complex variables, as the name implies, support more complex data structures like lists, maps, and objects.

Terraform variables support optional attributes that can be used to provide additional information about the variable. The most common attributes associated with variables include:

  • description: A description of the variable. This is useful for general documentation as well as when sharing code with other users.
  • type: The type of the variable. This helps ensure the variable is used correctly for the resources being provisioned.
  • default: A default value for the variable if none is provided by the user.

To see a full list of Terraform variable attributes, check out the official Variables documentation.

Primitive Variables

  • string: A string is a sequence of characters surrounded by double quotes.
variable "ibmcloud_region" {
    description = "The VPC region where resources will be deployed."
    type = string
    default = "us-south"
}

variable "project_prefix" {
  description = "Prefix to add to all deployed resources"
  type = string
  default = "poc-lab"
}
  • number: A number is a numeric value. Terraform supports both integer and floating point numbers.
variable "address_count" {
    description = "The number of addresses assigned to the VPC subnet."
    type = number
    default = 128
}
  • boolean: A boolean is a value that can be either true or false.
variable "create_public_gateway" {
    description = "Indicates whether a VPC public gateway should be created for a subnet."
    type = bool
    default = true
}

Complex Variables

  • list: A list is a collection of values surrounded by square brackets and seperated by commas. Values in a list can be arbitrary expressions.
variable "tags" {
    description = "A list of tags to be applied to the VPC subnet."
    type = list(string)
    default = ["environment:dev", "region:us-south"]
}
  • map: A map is collection of key/value pairs. These can be useful for selecting values based on predefined parameters such as the instance profile sizes or cloud regions.
variable "preset_configs" {
    description = "Instance profile to use for virtual instances."
    type = map
    default = {
        "small" = "bx2-2x8"
        "medium" = "bx2-4x16"
        "large" = "bx2-8x32"
    }
}

If I wanted to deploy a bx2-4x16 instance, I would use the following code in my Terraform configuration: profile = var.preset_configs["medium"]

  • object: An object is a collection of key/value pairs. Each key in the object must be a string, but each value can be of a different variable type. Objects are surrounded by curly braces and each key/value pair is separated by a comma.
variable "default_configs" {
  description = "Set of default configs for VPC environment."
  type = list(object({
    name     = string
    zone     = string
    number_of_instances = number
    create_public_gateway = bool
  }))
  default = [
    {
      name = "dev"
      zone = "us-south-1"
      number_of_instances = 2
      create_public_gateway = true
    },
    {
      name = "prod"
      zone = "us-south-2"
      number_of_instances = 4
      create_public_gateway = false
    }
  ]
}

Using count with Primitive Variables

By default, a Terraform resource block configures one infrastructure object. To create multiple instance using the same resource declaration, you can use the count meta-argument. The count meta-argument accepts an integer value that specifies the number of instances to create.

variable "bucket_count" {
    description = "The number of COS buckets to create for Flowlog collectors."
    type        = number
    default     = 5
}

resource "ibm_cos_bucket" "flowlogs" {
  count                = var.bucket_count
  bucket_name          = "flowlogs-collector-${count.index}"
  resource_instance_id = ibm_resource_instance.cos_instance.id
  region_location      = "us-south"
  storage_class        = "smart"
}

Objects used with count start at 0 and increment by 1 for each additional instance. In the example above for instance, the first bucket created would be named flowlogs-collector-0, the second would be named flowlogs-collector-1, and so on.

Using for_each with Complex Variables

While primitive variables rely on count to provision multiple resources, the for_each meta-argument can be used with complex variables to accomplish the same thing. The for_each meta-argument accepts a map or set of key/value pairs.

variable "security_group_rules" {
  description = "List of security group rules to set on the consul instances"
  default = [
    {
      name      = "consul_tcp_dns_in"
      direction = "inbound"
      remote    = "0.0.0.0/0"
      tcp = {
        port_min = 8600
        port_max = 8600
      }
    },
    {
      name      = "consul_udp_dns_in"
      direction = "inbound"
      remote    = "0.0.0.0/0"
      udp = {
        port_min = 8600
        port_max = 8600
      }
    },
    {
      name      = "consul_udp_in"
      direction = "inbound"
      remote    = "0.0.0.0/0"
      udp = {
        port_min = 8301
        port_max = 8302
      }
    }
  ]
}

resource "ibm_is_security_group_rule" "additional_tcp_rules" {
  for_each = {
    for rule in var.security_group_rules : rule.name => rule if lookup(rule, "tcp", null) != null
  }
  group      = ibm_is_security_group.consul_security_group.id
  direction  = each.value.direction
  remote     = each.value.remote
  ip_version = lookup(each.value, "ip_version", null)

  tcp {
    port_min = each.value.tcp.port_min
    port_max = each.value.tcp.port_max
  }
}

The example above would create 3 security group rules from the same resource block. The for_each meta-argument is used to iterate over the security_group_rules variable.

Using dynamic with Complex Variables

A dynamic block acts much like a for expression, but produces nested blocks instead of a complex typed value. In the following example I am using a dynamic block to deploy worker nodes in to each zone specified in the worker_zones variable. The key is the zone name and the value is the subnet ID the worker node will use when provisioned.

variable "worker_zones" {
  description = "A zone to subnet-id mapping of where IKS VPC worker nodes will be deployed."
  type        = map(string)
  default     = {
    "us-south-1" = "xxxx-2a9f1e1d-xxxx-xxxx-8d9c-1234567890ab"
    "us-south-2" = "xxxx-2a9f1e1d-1d95-4a9d-xxxx-1234567890ab"
    "us-south-3" = "0717-2a9f1e1d-xxxx-4a9d-xxxx-1234567890ab"
  }
}

resource "ibm_container_vpc_cluster" "iks_cluster" {
  name                            = var.cluster_name
  vpc_id                          = var.vpc_id
  flavor                           = var.worker_pool_flavor
  worker_count                    = var.worker_nodes_per_zone
  resource_group_id               = data.ibm_resource_group.group.id
  kube_version                    = var.kube_version
  wait_till                       = var.wait_till
  disable_public_service_endpoint = var.disable_public_service_endpoint
  tags                            = var.tags

  dynamic "zones" {
    for_each = var.worker_zones
    content {
      name      = zones.key
      subnet_id = zones.value
    }
  }

Without using dynamic blocks, the above example would look like this:

resource "ibm_container_vpc_cluster" "iks_cluster" {
  name                            = var.cluster_name
  vpc_id                          = var.vpc_id
  flavor                           = var.worker_pool_flavor
  worker_count                    = var.worker_nodes_per_zone
  resource_group_id               = data.ibm_resource_group.group.id
  kube_version                    = var.kube_version
  wait_till                       = var.wait_till
  disable_public_service_endpoint = var.disable_public_service_endpoint
  tags                            = var.tags

 zones {
    name = "us-south-1"
    subnet_id = "xxxx-2a9f1e1d-xxxx-xxxx-8d9c-1234567890ab"
  }
  zones {
    name = "us-south-2"
    subnet_id = "xxxx-2a9f1e1d-1d95-4a9d-xxxx-1234567890ab"
  }
  zones {
    name = "us-south-3"
    subnet_id = "0717-2a9f1e1d-xxxx-4a9d-xxxx-1234567890ab"
  }

Next Steps:

Now that you’ve learned a bit more about Terraform variables, I would recommand reading the following posts: