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
orfalse
.
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:
- Custom Variable Validation - Learn how to validate input variables using custom condition checks.
- Terraform Functions - Use Terraforms built-in functions to transform and combine values and outputs.